Skip to content

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).

INC-049 — Faux CRITICAL tfsec sur develop : rĂ©pertoire de build sale + sĂ©mantique des ignores (2026-06-01)

SymptĂŽme : Triage tfsec HIGH (#64). Le job tfsec-scan reste failed sur develop (1 CRITICAL aws-eks-no-public-cluster-access-to-cidr) alors que le pipeline de la MR passe au vert avec le mĂȘme code. Irreproductible en local (git archive propre → 0 critical). Cause : Le runner self-hosted rĂ©utilise son rĂ©pertoire de build : les pipelines infra (terraform apply) y laissent des fichiers non-trackĂ©s (.terraform/, lock, tfvars). Sur develop, tfsec scanne cet arbre sale, ne rĂ©sout pas var.cluster_public_access_cidrs → assume 0.0.0.0/0 → CRITICAL. La MR tourne dans un rĂ©pertoire propre, d'oĂč le vert. En plus, valeur non rĂ©solue → finding attribuĂ© au bloc vpc_config, donc l'#tfsec:ignore posĂ© sur l'attribut ne le couvrait pas. Fix : GIT_CLEAN_FLAGS: -ffdx sur le job tfsec-scan (arbre propre, ne scanne que l'IaC committĂ©e) + ignores EKS regroupĂ©s en bloc contigu au niveau de la ressource (cascade vers vpc_config). Vrais fixes #64 : ECR IMMUTABLE, IAM rds:DescribeDBInstances + eks:DescribeCluster scopĂ©s Ă  l'ARN. Preuve : Pipeline develop aprĂšs merge → job tfsec-scan vert (0 critical, 0 high). Leçon : "MR verte mais merge rouge" = problĂšme environnemental, pas le code ; ne pas s'acharner sur le code. Sur runner self-hosted rĂ©utilisĂ©, forcer GIT_CLEAN_FLAGS: -ffdx pour tout scan statique. Un ignore tfsec sur un finding attribuĂ© au bloc doit ĂȘtre au niveau de la ressource, pas de l'attribut.

INC-050 — ECR IMMUTABLE casse le promote (latest + re-push SHA) : passage au promote par digest (2026-06-03)

SymptĂŽme : AprĂšs l'apply de #64 (image_tag_mutability = IMMUTABLE), le pipeline app Ă©choue sur build-candidate : TAG_INVALID: The image tag '<SHA>-candidate' already exists in the 'fastapi-eks/fastapi' repository and cannot be overwritten because the tag is immutable. DĂ©couvert en re-testant un deploy sur un commit dĂ©jĂ  buildĂ© (aucun nouveau commit poussĂ© sur develop depuis). Cause : Un repo ECR immutable interdit de rĂ©-Ă©crire un tag existant. Trois points de casse, tous tolĂ©rĂ©s silencieusement en MUTABLE : (1) :latest poussĂ© par promote-image est mutable par nature → Ă©choue Ă  chaque run aprĂšs le premier ; (2) :$SHA-candidate et (3) :$SHA re-poussĂ©s Ă  l'identique au re-run d'un commit. DĂ©faut sous-jacent mis en lumiĂšre : promote-image faisait un rebuild kaniko, pas un retag → l'image promue (:$SHA) n'Ă©tait pas l'image scannĂ©e par Trivy (le candidate). Fix : (1) tag candidate unique par pipeline candidate-$SHA-$CI_PIPELINE_ID (plus de collision, artefact jetable) ; (2) promote-image réécrit en retag par digest (aws ecr batch-get-image du candidate → put-image sur :$SHA), l'image dĂ©ployĂ©e == l'image scannĂ©e ; (3) retrait de :latest (non consommĂ© par le deploy, incompatible immutable) ; (4) guard d'idempotence (skip si :$SHA dĂ©jĂ  prĂ©sent) ; (5) rĂšgle de lifecycle ECR expirant les candidate-* Ă  3 jours. Aucune permission IAM ajoutĂ©e (ecr:BatchGetImage + ecr:PutImage dĂ©jĂ  dans ecr-push). Preuve : Pipeline app DEPLOY=true vert de bout en bout (build → scan → promote → deploy), pods fastapi 1/1, re-run du mĂȘme commit → promote idempotent (skip), plus aucun TAG_INVALID. Leçon : Un durcissement (ECR immutable) rĂ©vĂšle un couplage cachĂ© du pipeline. Sur un repo immutable : jamais de tag mutable (latest), artefacts intermĂ©diaires en tags uniques, et le promote doit ĂȘtre un retag par digest (pas un rebuild) pour garantir que l'image scannĂ©e est celle dĂ©ployĂ©e. Tester un durcissement infra avec l'infra rĂ©ellement up : "mergĂ© vert" ne prouve pas "ne casse rien Ă  l'exĂ©cution".

INC-051 — ExternalDNS : flag boolĂ©en invalide + piĂšge ownership CNAME (2026-06-03)

SymptĂŽme : Pod ExternalDNS en CrashLoopBackOff dĂšs le dĂ©marrage : level=fatal msg="flag parsing error: unexpected false". Puis, aprĂšs fix, logs "All records are already up to date" en boucle alors que le CNAME modifiĂ© Ă  la main dans Cloudflare n'est jamais corrigĂ©. Cause — partie 1 (crash) : Le flag boolĂ©en --cloudflare-proxied ne supporte pas la syntaxe =false dans ExternalDNS v1.15.0. Or false est la valeur par dĂ©faut (DNS-only, pas de proxy Cloudflare) : l'argument Ă©tait inutile. Cause — partie 2 (ownership) : "All records are already up to date" ne signifie pas forcĂ©ment que tout fonctionne. Ici ExternalDNS ne possĂšde pas le CNAME : il n'a pas de TXT de propriĂ©tĂ© (heritage=external-dns,external-dns/owner=fastapi-eks) car le CNAME existait dĂ©jĂ  avant son installation. Il ignore donc les dĂ©rives sur ce record. Fix — partie 1 : Suppression du bloc extraArgs: [--cloudflare-proxied=false] dans les Helm values du bootstrap. Lire les valeurs par dĂ©faut avant d'ajouter des flags. Fix — partie 2 : Supprimer le CNAME dans Cloudflare. ExternalDNS le dĂ©tecte manquant, le recrĂ©e et pose simultanĂ©ment le TXT de propriĂ©tĂ©. Il est dĂ©sormais "owner" du record et corrige toute dĂ©rive en moins de 60s (policy: sync). Preuve : Test nĂ©gatif 1 : CNAME supprimĂ© → ExternalDNS CREATE CNAME + TXT. Test nĂ©gatif 2 : CNAME modifiĂ© Ă  la main → ExternalDNS DELETE + CREATE CNAME + UPDATE TXT en moins de 60s. curl https://api.devopsyouss.com/healthz/ready → {"status":"ok"}. Leçon : (1) Lire les valeurs par dĂ©faut avant d'ajouter des flags boolĂ©ens. (2) "All records are already up to date" peut vouloir dire "rien Ă  gĂ©rer" (ownership absent) plutĂŽt que "tout est correct". Toujours valider avec un test nĂ©gatif (supprimer/modifier le record DNS et vĂ©rifier la correction automatique).


INC-052 — pip install --prefix sautait les paquets dĂ©jĂ  prĂ©sents dans la base image (2026-06-09)

SymptĂŽme : pip check dans l'image runtime (dĂ©tectĂ© via la skill docker-image-lab) : packaging 24.x is required by gunicorn 26.0.0 but packaging is not installed. L'image dĂ©marrait quand mĂȘme car CMD utilisait uvicorn, masquant le problĂšme. Cause : pip install --prefix=/install (stage builder) ne rĂ©installe pas un paquet dĂ©jĂ  prĂ©sent dans l'image de base. packaging Ă©tait prĂ©installĂ© dans python:3.12-slim (builder), donc skippĂ© dans /install. Une fois copiĂ© dans le runtime (base image propre), packaging Ă©tait absent. Fix : Passage Ă  python -m venv /venv dans le builder. Un venv est un environnement Python entiĂšrement isolĂ© : pip installe toutes les dĂ©pendances sans tenir compte des paquets de la base image. Stage runtime : COPY --from=builder /venv /venv + ENV PATH="/venv/bin:$PATH". En bonus : gunicorn retirĂ© (requirements.in, requirements.txt, doc) car inutile sous K8s (scaling par replicas/HPA). .trivyignore purgĂ© (11 CVEs obsolĂštes de l'Ăšre 3.10-slim, 6 CVEs OS actuels documentĂ©s). Voir ADR 011 Amendement #67. Preuve : pip check → aucune erreur. 37 tests passĂ©s. Pipeline CI vert. curl https://api.devopsyouss.com/healthz/ready → {"status":"ok"} (validation EKS end-to-end, infra up, 2026-06-09). Leçon : pip install --prefix est un anti-pattern dans les builds multi-stage : son comportement dĂ©pend de l'environnement Python de la base image, ce qui le rend non dĂ©terministe et silencieusement cassĂ© selon la base. Toujours utiliser python -m venv pour isoler complĂštement les dĂ©pendances d'un stage.


🎯 Points clĂ©s Sprint 3

  1. Kubernetes est complexe : RBAC, ServiceAccount, probes, metrics...
  2. Envoy Gateway = ressources dynamiques : ELB, sec groups, ENIs → cleanup obligatoire
  3. Variables GitLab CI : ordre strict de prioritĂ© → toujours tester en feature branch
  4. Images Docker non-standard : ajout entrypoint: [""] obligatoire
  5. Migrations DB : utiliser init container, pas pod principal
  6. EKS Access Entries : deux couches distinctes IAM (DescribeCluster) + K8s RBAC (AccessEntry)
  7. Least privilege deploy : bootstrap = cluster-admin / deploy = edit ns uniquement
  8. 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)
  9. Reproduire en local : sortir de la boucle pipeline coûteuse, isoler couche par couche (curl > client Python > Ansible)
  10. 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
  11. "Up to date" peut masquer un problÚme d'ownership : ExternalDNS (INC-051) loguait "All records are already up to date" sans rien corriger car il ne possédait pas le CNAME (créé manuellement avant son installation). Toujours valider les opérateurs DNS/config par test négatif (supprimer/corrompre la ressource et vérifier la correction automatique)

📖 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 âœ