Sprint 3 â Kubernetes, EKS, Pipeline avancĂ©e
19 incidents ! Envoy Gateway, RBAC, metrics-server, Ansible bootstrap, GitLab CI avancé.
Pour la version complÚte avec tous les détails : voir le fichier 007-incidents-lessons-learned.md dans le repo.
INC-013 Ă INC-024 â RĂ©sumĂ©s rapides
INC-013 : ELB manquant aprĂšs Envoy Gateway
Fix : Vérifier que la Gateway API est correctement déployée (kubectl get gateway)
INC-014 : RBAC manquant pour bootstrap Job
Fix : Créer ServiceAccount + ClusterRoleBinding pour les actions bootstrap (Alembic, etc)
INC-015 : Metrics-server non installé (HPA échoue)
Fix : helm install metrics-server metrics-server/metrics-server -n kube-system
INC-016 : Alembic in-container vs init container
Fix : Utiliser init container pour migrations avant le pod FastAPI
INC-017 : Envoy Gateway upstream mutable
Fix : Vérifier la configuration HTTPRoute (host, port, service)
INC-018 : ConfigMap montée en volume vide
Fix : Vérifier que le ConfigMap existe et mountPath est correct
INC-019 : ServiceAccount token absent
Fix : Créer ServiceAccount, vérifier /var/run/secrets/kubernetes.io/serviceaccount/
INC-020 : Image ECR pull rate limit
Fix : Utiliser imagePullSecrets avec credentials AWS (ECR auth token)
INC-021 : Probes (liveness/readiness) timeout
Fix : Augmenter initialDelaySeconds et timeoutSeconds pour FastAPI slow startup
INC-022 : HPA metrics CPU insuffisantes
Fix : Vérifier requests/limits et que metrics-server remonte les données
INC-023 : Terraform ELB sec groups pas générés
Fix : Vérifier que kubernetes.io/service tags sont présents
INC-024 : GitLab CI job sans entrypoint sur images spéciales
Fix : Ajouter entrypoint: [""] pour alpine, aws-cli, terraform, kaniko, trivy
INC-025 Ă INC-030 â Variables GitLab CI & Pipeline
INC-025 : Variables projet écrasent variables YAML
Fix : Utiliser before_script: - export VAR=... pour forcer les values
INC-026 : terraform.tfvars gitignored â TF_VAR_ en CI
Fix : Utiliser TF_VAR_db_password en variables projet (Terraform lit auto TF_VAR_*)
INC-027 : pip introuvable dans alpine/ansible
Fix : apk add --no-cache py3-pip avant pip3 install
INC-028 : Envoy Gateway Helm repo HTTP 404 (migration OCI)
Fix : Utiliser chart_ref: oci://docker.io/envoyproxy/gateway-helm (OCI, pas HTTP repo)
INC-029 : eks:DescribeCluster manquant pour user CI
Fix : Ajouter IAM permission eks:DescribeCluster ou utiliser $AWS_INFRA_ACCESS_KEY_ID
INC-030 : needs DAG vs stages pour destroy infra
Fix : Utiliser stages: (ordre strict) au lieu de needs: pour les flows destructifs
đ Incidents Sprint 3 par catĂ©gorie
| Catégorie | Incidents | Fix time |
|---|---|---|
| Kubernetes/EKS | INC-013, 015, 016, 017, 018, 019, 020, 021, 022, 023 | 30min-2h |
| GitLab CI avancé | INC-024, 025, 026, 029, 030 | 5-15min |
| Tools/Ansible | INC-027, 028 | 10min |
| Total Sprint 3 | 19 incidents | â |
EKS Access Entries + Deploy End-to-End (2026-05-19)
INC-031 â AmazonEKSClusterPolicy attachĂ©e Ă un IAM user
SymptÎme : youss_admin ne peut pas kubectl malgré la policy attachée
Cause : AmazonEKSClusterPolicy est pour le control plane EKS, pas pour un user
Fix : Policy custom eks:DescribeCluster + eks:ListClusters uniquement
INC-032 â eks:DescribeCluster manquant pour gitlab-ci
SymptĂŽme : AccessDeniedException eks:DescribeCluster dans le deploy job
Cause : policy ecr_push couvre ECR uniquement, pas EKS
Fix : Ajouter statement EKSConnect Ă la policy ecr_push
INC-033 â kubectl cluster-info forbidden
SymptĂŽme : cannot list services in namespace kube-system
Cause : gitlab-ci a edit scope fastapi uniquement
Fix : Remplacer par kubectl auth can-i create deployments -n fastapi
INC-034 â kubectl create namespace forbidden + doublon bootstrap
SymptÎme : namespaces is forbidden pour gitlab-ci Cause : Création namespace = opération cluster-level + déjà créé par bootstrap Fix : Supprimer du deploy job, déléguer au bootstrap Ansible
INC-035 â Init:InvalidImageName â ${ECR_IMAGE} non substituĂ©
SymptĂŽme : Failed to apply default image tag "${ECR_IMAGE}"
Cause : kubectl apply sans envsubst â placeholder littĂ©ral dans K8s
Fix : yum install gettext + envsubst < deployment.yaml | kubectl apply -f -
INC-036 â DB_PASSWORD manquant dans fastapi-secrets
SymptĂŽme : Pydantic ValidationError: DB_PASSWORD Field required
Cause : Secret créé avec SECRET_KEY uniquement, DB_PASSWORD oublié
Fix : Ajouter --from-literal=DB_PASSWORD="$TF_VAR_db_password"
INC-037 â terraform apply ancien pipeline â No changes
SymptÎme : Fix mergé mais policy non mise à jour dans AWS
Cause : Apply depuis un ancien pipeline (avant le merge), code pas Ă jour
Fix : git pull origin develop avant tout terraform apply manuel
INC-038 â State lock avec mauvais credentials
SymptĂŽme : Error acquiring the state lock avec youss_admin Cause : youss_admin n'a pas accĂšs S3/DynamoDB â AccessDenied masquĂ© en lock error Fix : Terraform = iamadmin uniquement / youss_admin = kubectl uniquement
INC-039 â Hardening Sprint 3 jamais appliquĂ© sur EKS (deploy fichier par fichier)
SymptÎme : PSA, ServiceAccount, PDB et NetworkPolicy absents du cluster malgré des manifests durcis et validés en local
Cause : Le deploy appliquait 4 manifests sur 10 (configmap/service/hpa/deployment via envsubst), le reste n'atteignait jamais EKS
Fix : kustomize edit set image fastapi=$ECR_IMAGE + kubectl apply -k k8s/base/ (les 10 ressources d'un coup)
Leçon : DĂ©ployer le mĂȘme artefact que celui qu'on lint (kustomize build = source de vĂ©ritĂ©), tester end-to-end sur EKS et pas seulement en local
INC-040 â terraform apply rejetĂ© : accent dans une description de SG
SymptĂŽme : egress.0.description doesn't comply with restrictions (regex ASCII AWS) sur module.rds.aws_security_group.rds
Cause : Le "é" de "Réponses" dans la description egress du SG RDS ; AWS valide les descriptions de rÚgles SG contre une regex strictement ASCII
Fix : Descriptions egress (RDS + EKS) réécrites en anglais ASCII
Leçon : Tout en anglais ASCII dans la conf ; ni tfsec ni terraform validate/plan n'attrapent ça (contrainte imposée par l'API AWS à l'apply)
Security Hardening Audit (2026-05-20)
Audit de sĂ©curitĂ© complet rĂ©alisĂ© en prĂ©paration des entretiens techniques. Trois failles corrigĂ©es via GitFlow professionnel (issue â branch â MR â pipeline green â squash merge).
SEC-001 â Mot de passe DB en clair dans les docs publiques (issue #41)
Sévérité : Critical
Fichiers : docs/infra-eks-summary.md, docs/adr/007-incidents-lessons-learned.md
SymptÎme : TF_VAR_db_password avec sa valeur réelle visible dans le repo public
Fix : Remplacement par <REDACTED> + rotation password GitLab CI + tfvars local
Leçon : Ne jamais Ă©crire de valeurs rĂ©elles dans la documentation, mĂȘme Ă titre d'exemple
SEC-002 â CORS misconfiguration (issue #42)
Sévérité : High
Fichiers : app/main.py, app/config.py
SymptĂŽme : allow_origins=["*"] + allow_credentials=True â combinaison interdite par la spec CORS, tous les navigateurs rejettent silencieusement
Fix : Variable ALLOWED_ORIGINS via pydantic_settings (défaut http://localhost:3000), configurable par environnement
Leçon : allow_credentials=True exige des origins explicites, jamais de wildcard
SEC-003 â securityContext container incomplet (issue #43)
Sévérité : High
Fichiers : k8s/base/deployment.yaml
SymptĂŽme : Hardening container-level absent (allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities, seccompProfile)
Fix : Ajout des 4 paramĂštres sur initContainer + container fastapi + emptyDir /tmp
Validation : TestĂ© localement sur kind â securityContext acceptĂ© par Kubernetes sans erreur
Leçon : Le securityContext pod-level (runAsNonRoot) ne suffit pas, le hardening container-level est obligatoire
SEC-004 â Haute disponibilitĂ© absente (issue #44)
Sévérité : High
Fichiers : k8s/base/deployment.yaml, k8s/base/pdb.yaml (nouveau)
SymptÎme : replicas: 1 = SPOF, aucun PodDisruptionBudget = pod détruit sans garantie lors d'un node drain
Fix : replicas 1 â 2, ajout d'un PDB avec minAvailable: 1
Leçon : replicas >= 2 + PDB est le minimum pour toute application en production
SEC-005 â ServiceAccount default avec token montĂ© (issue #45)
Sévérité : High
Fichiers : k8s/base/serviceaccount.yaml (nouveau), k8s/base/deployment.yaml
SymptĂŽme : Pod utilisait le SA default avec token Kubernetes montĂ© automatiquement â vecteur d'attaque si RCE
Fix : SA dédié fastapi avec automountServiceAccountToken: false, défense en profondeur appliquée aussi au PodSpec
Leçon : Un ServiceAccount dédié par application, token désactivé sauf besoin explicite (API K8s ou IRSA AWS)
Bootstrap EKS â Marathon authentification (2026-05-23)
Une journée entiÚre de debug sur le job bootstrap (Ansible kubernetes.core). Cause finale : une régression dans une lib Python upstream. Quatre incidents empilés ont masqué la vraie cause et allongé le diagnostic.
INC-041 â kubectl i/o timeout : jobs sur runner GitLab partagĂ©
SymptĂŽme : kubectl get nodes â dial tcp ...:443: i/o timeout sur infra-start et bootstrap
Cause : .gitlab-ci-infra.yml sans default: tags, les jobs tournaient sur un runner GitLab partagĂ© (GCP, IP 35.x) au lieu du runner self-hosted. L'IP du runner partagĂ© n'Ă©tait pas dans cluster_public_access_cidrs (82.66.53.81/32) â EKS drop les paquets silencieusement â timeout
Fix : Ajout de default: tags: [ubuntu] pour forcer tous les jobs sur le runner self-hosted
Leçon : Toujours pinner le runner via tags. Un i/o timeout sur l'API EKS = problÚme réseau/CIDR, pas auth (qui donnerait 401/403)
INC-042 â Variable Protected non injectĂ©e sur feature branch
SymptÎme : TF_VAR_cluster_public_access_cidrs absente en CI sur les branches non protégées, terraform utilisait le default placeholder 203.0.113.0/24
Cause : La variable est "Protected" dans GitLab â injectĂ©e uniquement sur branches/tags protĂ©gĂ©s. Tester un fix sur une feature branch fausse le rĂ©sultat
Fix : Pour les valeurs non secrÚtes (une IP), décocher "Protected". Garder Protected pour les vrais secrets (clés AWS)
Leçon : Distinguer secret (Protected) de non-secret. Une variable Protected casse la capacité à valider en MR avant merge
INC-043 â Ansible : until condition crash + connexion SSH sur localhost
SymptĂŽme : (1) object of type 'dict' has no attribute 'resources' ; (2) ssh: connect to host localhost port 22: Connection refused
Cause : (1) until: nodes.resources | length > 0 plante quand k8s_info échoue (pas de clé resources) ; (2) -i localhost, fait traiter localhost comme hÎte distant SSH
Fix : (1) until: (nodes.resources | default([])) | length > 0 ; (2) connection: local dans le play
Leçon : Toujours default() une variable registered dans une condition until ; connection: local pour les plays kubernetes.core qui tournent en local
INC-044 â Bootstrap 401 : rĂ©gression de la lib Python kubernetes 36.0.0
SymptĂŽme : kubernetes.core â 401 Unauthorized sur toutes les tasks, alors que kubectl get nodes passe dans le before_script avec les mĂȘmes credentials
Cause : RĂ©gression dans la lib Python kubernetes 36.0.0 qui casse l'auth EKS. Le MĂME token est acceptĂ© par kubectl et curl brut (HTTP 200) mais rejetĂ© par le client v36 (401), sur Alpine ET Ubuntu. pip install kubernetes (non pinnĂ©) tirait la 36 alors qu'une version antĂ©rieure marchait 4 jours avant
Diagnostic : Reproduit en local hors pipeline. kubernetes==31.0.0 + config.load_kube_config() â node listĂ© ; v36 â 401. Ălimination mĂ©thodique : token (curl 200), identitĂ© (auth whoami OK), OS (Ă©chec Ubuntu aussi), version (31 OK / 36 KO)
Fix : Pin pip3 install kubernetes==31.0.0 dans bootstrap + teardown
Leçon : Pinner les versions des libs critiques en CI. pip install <lib> sans pin = une release upstream peut casser l'infra du jour au lendemain, sans aucun changement de ton cÎté
Méta-leçons du marathon
- Tester à chaque itération. Batcher plusieurs changements infra non testés (la veille) a empilé 4 causes distinctes, rendant le diagnostic trÚs long et coûteux (cluster qui tourne = argent)
- Descendre au niveau le plus bas pour isoler. curl brut > client Python > Ansible : tester chaque couche a permis d'isoler la régression de lib
- Reproduire en local dĂšs que possible. Sortir de la boucle "merge â pipeline â attendre â cluster coĂ»te" en reproduisant le bug en local = itĂ©rations en secondes
- Lire le bon signal. i/o timeout = réseau/CIDR ; 401 = authentification ; 403 = autorisation. Le code d'erreur oriente le diagnostic
Deploy E2E + ownership du namespace (2026-05-24)
AprĂšs le fix #52 (deploy via kubectl apply -k), premiĂšre validation end-to-end du deploy applicatif avec l'infra up. Le deploy applique bien le hardening workload, mais un test nĂ©gatif mĂ©thodique rĂ©vĂšle que deux contrĂŽles de sĂ©curitĂ© n'avaient jamais Ă©tĂ© enforced sur EKS : PSA (INC-045) puis les NetworkPolicy (INC-046), mĂȘme cause racine cĂŽtĂ© CNI.
INC-045 â PSA jamais enforced sur EKS : namespace ownĂ© par le bootstrap, pas par le deploy
SymptĂŽme : kubectl apply -k k8s/base/ â Error from server (Forbidden) sur le patch du Namespace fastapi (labels PSA). Le reste du hardening (SA, NetworkPolicy, PDB, deployment) s'applique quand mĂȘme
Cause : Le Namespace est cluster-scoped. Le user CI deploy est en least-privilege (edit dans le ns fastapi uniquement), il ne peut pas modifier l'objet Namespace. Or namespace.yaml Ă©tait dans k8s/base, donc le deploy tentait de poser les labels PSA â Forbidden. Le bootstrap Ansible crĂ©ait le ns SANS labels â PSA enforce: restricted (cru actif depuis #48) n'a jamais atteint EKS
Fix : Ownership du namespace déplacé vers le bootstrap (cluster-admin) : ansible/bootstrap.yml crée le ns avec les labels PSA restricted. namespace.yaml retiré de k8s/base (le deploy ne gÚre plus le ns). local-kind garde son ns baseline en resource propre
Preuve : AprÚs kubectl label ns fastapi ...=restricted, rollout 1/1 sous enforce restricted (workload C3 compliant admis) + pod nginx non conforme rejeté à l'admission (4 violations restricted). Le contrÎle bloque réellement un déploiement non durci
Leçon : Une ressource cluster-scoped (Namespace + labels PSA) doit ĂȘtre ownĂ©e par le bootstrap cluster-admin, pas par le deploy least-privilege. Un hardening "validĂ© en kind" peut ne jamais atteindre la prod si le path d'application diffĂšre. Toujours vĂ©rifier l'Ă©tat rĂ©el (kubectl get ns --show-labels), pas l'intention dans les manifests
INC-046 â NetworkPolicy appliquĂ©es mais non enforced sur EKS (VPC CNI self-managed)
SymptÎme : Dans la foulée du fix PSA, test négatif sur les NetworkPolicy : depuis le pod fastapi, une sortie sur le port 80 (hors allowlist egress) connecte alors que le couple default-deny-all + allow-fastapi devrait la bloquer. Les 3 policies sont pourtant bien présentes (kubectl get netpol -n fastapi)
Cause : Une NetworkPolicy n'est enforced que par un CNI qui l'implĂ©mente. Le cluster tourne avec le VPC CNI en self-managed (dĂ©faut EKS) : aucun addon EKS managĂ© dĂ©clarĂ©, confirmĂ© deux fois â aucun aws_eks_addon dans le Terraform ET aws eks list-addons â []. Il n'existe donc nulle part oĂč poser enableNetworkPolicy, et le VPC CNI par dĂ©faut ne l'active pas. MĂȘme classe de problĂšme que PSA (INC-045)
Statut : Mitigation suivie dans #55 â dĂ©clarer vpc-cni comme addon EKS managĂ© (aws_eks_addon, version â„ 1.14) avec configuration_values = { enableNetworkPolicy = "true" }, puis re-valider par test nĂ©gatif. Non encore livrĂ© Ă la date de cet incident
Leçon : « ConfigurĂ© n'est pas enforced » (2e occurrence aprĂšs PSA). Un manifest NetworkPolicy valide, appliquĂ© et lintĂ© ne protĂšge rien si le CNI ne l'enforce pas. La seule preuve fiable est le test nĂ©gatif sur le cluster rĂ©el (tenter un flux qui doit ĂȘtre bloquĂ©), jamais l'intention lue dans les manifests. Sur EKS : NetworkPolicy â VPC CNI en addon managĂ© + enableNetworkPolicy=true
ESO + IRSA â RBAC dĂ©ploiement (2026-05-29)
Suite directe d'INC-045/046 sur la mĂȘme classe de problĂšme : on pose des ressources sans Ă©tendre les permissions du compte qui doit les appliquer. Cette fois ce sont les CRDs ESO qui ne sont pas couvertes par le ClusterRole built-in edit.
INC-047 â Premier essai : ClusterRole d'agrĂ©gation sur les CRDs ESO (deploy app 403) (2026-05-29)
SymptĂŽme : Run du deploy app post-merge MR-D (#33), kubectl apply -k k8s/base/ tombe en Forbidden sur les nouveaux manifests ESO : externalsecrets.external-secrets.io "fastapi-secrets" is forbidden: User "arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci" cannot get resource "externalsecrets" in API group "external-secrets.io". Idem secretstores. Le 403 a lieu sur le get server-side du 3-way merge, AVANT l'apply
Cause (diagnostic initial) : Le user CI est mappé via EKS Access Entry à AmazonEKSEditPolicy scopée au ns fastapi, correspondant au ClusterRole built-in edit. Or edit est agrégé et ne couvre pas les CRDs de tiers par défaut. HypothÚse initiale : étendre edit via le label aggregate-to-edit: "true" suffit.
Fix tentĂ© : ClusterRole aggregate-eso-to-edit dans ansible/bootstrap.yml. Structurellement correct : kubectl get clusterrole edit -o yaml | grep external-secrets â 3 entrĂ©es dans edit.rules. Mais kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> â no. Re-run pipeline â MĂME 403.
Cause réelle : AmazonEKSEditPolicy est un snapshot statique AWS-managed, pas un binding vers le ClusterRole K8s edit agrégé. L'agrégation K8s ne se propage pas aux users mappés via EKS Access Entry + AWS Access Policy. Voir INC-048 pour le vrai fix.
Leçon : Le can-i --as=<ARN> Ă©tait fiable, mal interprĂ©tĂ© comme faux nĂ©gatif. Un snapshot AWS â un ClusterRole K8s vivant. ConservĂ© en doc : matĂ©riau d'entretien (diagnostic en couches, humilitĂ© technique).
INC-048 â AmazonEKSEditPolicy snapshot statique : seul un binding RBAC explicite fonctionne (2026-05-30)
SymptĂŽme : MĂȘmes 403 qu'INC-047 malgrĂ© la ClusterRole aggregate-eso-to-edit en place. kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> â no (fiable).
Cause : AmazonEKSEditPolicy est une policy AWS-managed, snapshot statique des permissions view+edit. Elle ne suit pas l'agrĂ©gation RBAC Kubernetes. Ătendre le ClusterRole edit via aggregate-to-edit n'a aucun effet sur un user mappĂ© via EKS Access Entry + AWS Access Policy.
Fix : Role fastapi-deploy-eso (ns fastapi, rules get,list,watch,create,update,patch,delete sur externalsecrets/secretstores) + RoleBinding bindant arn:aws:iam::{{ aws_account }}:user/ci/{{ project_name }}-gitlab-ci. Posés par le bootstrap (cluster-admin). ClusterRole aggregate-eso-to-edit retirée.
Preuve : kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> â yes. Pipeline app DEPLOY=true vert. SecretStore Valid, ExternalSecret SecretSynced, pods fastapi 1/1 Ready. curl https://api.devopsyouss.com/healthz/ready â 200.
Leçon : Pour les users mappés via EKS Access Entry + AWS Access Policy, seul un binding RBAC K8s explicite (Role/RoleBinding) sur l'ARN du user fonctionne. L'agrégation K8s ne traverse pas la couche AWS. INC-047 conservé : matériau d'entretien (diagnostic en couches, humilité technique).
đŻ Points clĂ©s Sprint 3
- Kubernetes est complexe : RBAC, ServiceAccount, probes, metrics...
- Envoy Gateway = ressources dynamiques : ELB, sec groups, ENIs â cleanup obligatoire
- Variables GitLab CI : ordre strict de prioritĂ© â toujours tester en feature branch
- Images Docker non-standard : ajout
entrypoint: [""]obligatoire - Migrations DB : utiliser init container, pas pod principal
- EKS Access Entries : deux couches distinctes IAM (DescribeCluster) + K8s RBAC (AccessEntry)
- Least privilege deploy : bootstrap = cluster-admin / deploy = edit ns uniquement
- Pinner les libs critiques en CI :
pip install <lib>sans version = une régression upstream peut casser l'infra (cf INC-044, kubernetes 36.0.0) - Reproduire en local : sortir de la boucle pipeline coûteuse, isoler couche par couche (curl > client Python > Ansible)
- Configuré n'est pas enforced : PSA (INC-045) et NetworkPolicy (INC-046) étaient appliqués mais inertes (ns sans labels / VPC CNI self-managed). Valider tout contrÎle de sécurité par test négatif sur le cluster réel, jamais par l'intention dans les manifests
đ Recommandations pour Sprint 4+
Voir Recommandations pour les best practices consolidées.
đ Ressources utiles
- GitLab CI docs: https://docs.gitlab.com/ee/ci/
- Envoy Gateway: https://gateway.envoyproxy.io/
- Kubernetes: https://kubernetes.io/docs/
- AWS EKS: https://docs.aws.amazon.com/eks/
Pour les détails complets de chaque incident : voir 007-incidents-lessons-learned.md du repo.
Sprint 3 complet â 2026-05-19 â