Skip to content

ADR 007 — Incidents et Leçons Apprises

Contexte

Ce document recense tous les incidents rencontrés durant le projet fastapi-eks-devops, classés par sprint et ordre logique du pipeline. L'objectif est de capitaliser sur ces expériences pour améliorer les pratiques DevOps et DevSecOps.


SPRINT 0 — Fondations & CI/CD de base

INC-001 — Variable GitLab CI Protected sur feature branch

Contexte : Pipeline test stage SymptĂŽme :

pydantic_core.ValidationError: SECRET_KEY Field required

Cause :

Variable GitLab CI marquée "Protected"
Feature branch non protĂ©gĂ©e → variable non injectĂ©e

Fix :

Settings → CI/CD → Variables → SECRET_KEY
Protected : ❌ → Masked : ✅

Leçon :

Protected  = injecté uniquement sur branches protégées (main, develop)
Masked     = cache la valeur dans les logs → suffisant pour la sĂ©curitĂ©
Rùgle : CI/CD variables → Masked uniquement sauf secrets production

INC-002 — Pydantic BaseSettings prioritĂ© .env vs variables d'environnement

Contexte : Docker Compose networking SymptĂŽme :

Connection refused: localhost:5432

Cause :

Pydantic BaseSettings lit le fichier .env en priorité
DB_HOSTNAME=localhost dans .env écrase DB_HOSTNAME=db injecté par Docker

Fix :

# Créer un fichier .env.docker séparé
DB_HOSTNAME=db   # nom du service Docker Compose

Leçon :

Pydantic BaseSettings priorité :
1. Variables d'environnement (haute)
2. Fichier .env (basse par défaut)
→ En dev Docker : utiliser .env.docker
→ En CI/CD : pas de fichier .env (variables injectĂ©es)

INC-003 — Migration Alembic dupliquĂ©e

Contexte : Démarrage de l'application SymptÎme :

sqlalchemy.exc.ProgrammingError: DuplicateTable relation "users" already exists

Cause :

Migration c9e6377cb51d_new_migration.py recréait des tables
déjà créées par les migrations précédentes

Fix :

# Identifier et supprimer la migration problématique
rm alembic/versions/c9e6377cb51d_new_migration.py

Leçon :

Toujours vérifier l'historique Alembic avant d'ajouter une migration
alembic history --verbose
Ne jamais créer une migration qui recrée des tables existantes

SPRINT 1 — DevSecOps & Security Scanning

INC-004 — CVEs en cascade depuis une dĂ©pendance inutilisĂ©e

Contexte : Trivy filesystem scan SymptĂŽme :

17 HIGH CVEs détectés dans la pipeline

Cause :

http-tools → mitmproxy (outil de debug rĂ©seau)
jamais utilisé par FastAPI mais dans requirements.in
GénÚre une cascade de 12 CVEs HIGH via ses dépendances

Fix :

# Supprimer http-tools de requirements.in
# pip-compile → requirements.txt regenerĂ©
# 12/17 CVEs éliminés automatiquement

Leçon :

Auditer réguliÚrement les dépendances
Principle of Least Dependency : n'installer que le nécessaire
Commande utile : pip-audit, pip list --outdated

INC-005 — python-jose CVE CRITICAL (CVE-2024-33663)

Contexte : Trivy scan — algorithme JWT Symptîme :

CRITICAL: python-jose algorithm confusion with ECDSA keys

Cause :

python-jose 3.x vulnérable à l'algorithm confusion
Aucun patch disponible — projet peu maintenu

Fix :

# Migration python-jose → PyJWT
# requirements.in : python-jose → PyJWT>=2.12.0
# app/oauth2.py : from jose import JWTError → from jwt.exceptions import InvalidTokenError

Leçon :

Vérifier la santé des projets open source avant de les utiliser
CritĂšres : derniĂšre release, issues ouvertes, mainteneurs actifs
Outils : deps.dev, snyk advisor, PyPI stats

INC-006 — passlib incompatible bcrypt >= 4.0

Contexte : Tests pytest + pipeline CI SymptĂŽme :

AttributeError: module 'bcrypt' has no attribute '__about__'
ValueError: password cannot be longer than 72 bytes

Cause :

passlib 1.7.4 accĂšde Ă  bcrypt.__about__.__version__
Supprimé dans bcrypt 4.0.0
passlib abandonnĂ© depuis 2023 — incompatible

Fix :

# Migration passlib → bcrypt direct
# requirements.in : supprimer passlib, garder bcrypt (sans restriction <4)
# app/utils.py : utiliser bcrypt.hashpw() / bcrypt.checkpw() directement

Leçon :

Surveiller l'activité des dépendances critiques (auth, crypto)
Un projet abandonné = risque de sécurité + incompatibilités futures
Outils : Dependabot, Renovate pour les alertes automatiques

INC-007 — CVEs OS image de base Docker (python:3.10-slim)

Contexte : Trivy image scan (aprĂšs build) SymptĂŽme :

14 CVEs OS Debian 13.4 — status "affected" (pas de fix)
wheel et jaraco.context : Trivy détecte 2 versions

Cause :

python:3.10-slim installe wheel/jaraco.context via apt
dans /usr/lib/python3/dist-packages/ (version ancienne)
pip installe une version plus récente dans /usr/local/lib/
Trivy remonte les 2 versions → CVEs sur l'ancienne

Fix court terme :

Ajouter les CVEs dans .trivyignore avec justification
→ packages de build jamais utilisĂ©s Ă  runtime par FastAPI

Fix long terme :

# Multi-stage build (Sprint 4)
FROM python:3.12-slim AS builder
# installer toutes les dépendances

FROM python:3.12-slim AS runtime
# copier uniquement /usr/local/lib/python3.12
# → Ă©limine tous les packages systĂšme de build

Leçon :

Les images Docker héritent des vulnérabilités de l'OS de base
Multi-stage build = meilleure pratique pour réduire la surface d'attaque
Alternatives : distroless, chainguard images

SPRINT 2 — Infrastructure Terraform AWS

INC-008 — Terraform state lock S3 non libĂ©rĂ©

Contexte : terraform plan/apply SymptĂŽme :

Error: Error acquiring the state lock
PreconditionFailed: At least one of the pre-conditions did not hold

Cause :

Terraform interrompu (Ctrl+C, redémarrage container)
→ fichier .tflock reste dans S3
→ prochaine opĂ©ration bloquĂ©e

Fix :

# Supprimer le lock manuellement
aws s3 rm s3://BUCKET/fastapi-eks/ephemeral/.tflock
# ou
terraform force-unlock LOCK_ID

Leçon :

Ne jamais interrompre un terraform apply/destroy
Utiliser tmux pour les longues opérations :
tmux new-session -d -s terraform 'terraform apply -auto-approve'
tmux attach -t terraform

INC-009 — Ressources AWS hors state Terraform (ECR, IAM)

Contexte : Restructuration modules → persistent/ephemeral Symptîme :

RepositoryAlreadyExistsException: ECR repository already exists
EntityAlreadyExists: IAM Role already exists

Cause :

Ressources créées avec l'ancien state (terraform/main/)
Nouveau state (terraform/persistent/) ne les connaĂźt pas
Terraform essaie de les recrĂ©er → conflit AWS

Fix :

# En dev : supprimer et recréer
aws ecr delete-repository --repository-name fastapi-eks/fastapi --force
aws iam delete-role --role-name fastapi-eks-eks-cluster-role

# En prod : toujours terraform import
terraform import module.ecr.aws_ecr_repository.fastapi fastapi-eks/fastapi

Leçon :

Terraform ne connaĂźt que ce qui est dans SON state
RĂšgle d'or : jamais restructurer sans terraform import en production
En dev : terraform destroy → restructurer → terraform apply

INC-010 — EKS Node Group bloquĂ© sans NAT Gateway (27+ min)

Contexte : terraform apply — EKS module Symptîme :

module.eks.aws_eks_node_group.main: Still creating... [27m36s elapsed]

Cause :

create_nat_gateway=false → nodes dans subnet privĂ©
→ pas d'accùs internet
→ nodes ne peuvent pas :
  - s'enregistrer auprĂšs de l'API EKS
  - puller les images systĂšme (kube-proxy, CoreDNS, aws-node)
  - accéder aux APIs AWS (ECR, CloudWatch, S3)

Fix :

# terraform/ephemeral/terraform.tfvars
create_nat_gateway = true  # TOUJOURS true avec EKS

Leçon :

EKS nodes nécessitent impérativement un accÚs internet sortant
→ NAT Gateway obligatoire pour les nodes en subnet privĂ©
→ Sans NAT : node group bloquĂ© indĂ©finiment en CREATING
Variable create_nat_gateway=false : uniquement pour tests VPC/RDS seuls

INC-011 — ENIs orphelins bloquant la suppression des subnets

Contexte : terraform destroy SymptĂŽme :

Error: deleting EC2 Subnet: DependencyViolation
The subnet has dependencies and cannot be deleted

Cause :

ENIs créés par EKS et RDS dans les subnets privés
→ appartiennent aux services AWS
→ pas supprimables directement par l'utilisateur
→ AWS les libùre automatiquement mais cela prend 10-15 min

Fix :

# Attendre 10-15 min aprĂšs suppression EKS/RDS
# Vérifier les ENIs
aws ec2 describe-network-interfaces \
  --filters "Name=vpc-id,Values=$VPC_ID" \
  --query "NetworkInterfaces[].{ID:NetworkInterfaceId,Status:Status}" \
  --output table
# Relancer terraform destroy quand ENIs libérés

Leçon :

Ne jamais interrompre terraform destroy
Si bloqué : attendre 10-15 min et réessayer
Solution long terme : vérifier ENIs avant destroy dans aws-stop.sh

INC-012 — ECR et IAM dĂ©truits quotidiennement avec ephemeral

Contexte : aws-stop.sh → terraform destroy Symptîme :

Pipeline CI plantée le lendemain
AWS credentials invalides (IAM user supprimé)
ECR repository introuvable

Cause :

ECR et IAM dans le mĂȘme workspace que VPC/EKS/RDS
→ terraform destroy tout supprime
→ credentials GitLab CI invalides
→ pipeline plante au stage build

Fix :

Restructuration en 2 workspaces :
terraform/persistent/ : ECR + IAM (jamais détruits)
terraform/ephemeral/  : VPC + EKS + RDS (daily destroy)

Leçon :

Séparer les ressources par cycle de vie :
- Persistantes (ECR, IAM, S3) : apply une fois
- ÉphĂ©mĂšres (EKS, RDS, VPC) : apply/destroy quotidien
Scripts aws-start.sh et aws-stop.sh pour automatiser

SPRINT 3 — DĂ©ploiement EKS & Pipeline CI/CD

INC-013 — AWS CLI v2 incompatible Alpine Linux (Docker CI)

Contexte : Pipeline CI — stage build Symptîme :

Failed to load Python shared library libpython3.14.so.1.0:
dladdr1: symbol not found

Cause :

docker:24 image = Alpine Linux (musl libc)
AWS CLI v2 = compilé pour glibc
→ incompatibilitĂ© fondamentale musl vs glibc
MĂȘme avec gcompat, Python 3.14 bundlĂ© ne fonctionne pas

Fix :

# Remplacer Docker-in-Docker par Kaniko
image:
  name: gcr.io/kaniko-project/executor:v1.23.2-debug
  entrypoint: [""]
# Kaniko authentifie ECR nativement via variables AWS
# Pas d'AWS CLI nécessaire

Leçon :

Docker-in-Docker + AWS CLI Alpine = incompatible
Kaniko = alternative moderne, pas de daemon Docker, pas de privileged mode
Avantages Kaniko : ECR auth native, cache layers, sécurisé

INC-014 — Variables GitLab CI Protected bloquant le build ECR

Contexte : Pipeline CI — Kaniko push ECR Symptîme :

error: checking push permissions: 401 Unauthorized

Cause :

AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY marqués "Protected"
Feature branches non protĂ©gĂ©es → variables non injectĂ©es
→ Kaniko sans credentials → 401 ECR

Fix :

Settings → CI/CD → Variables
AWS_ACCESS_KEY_ID     : Protected ❌ / Masked ✅
AWS_SECRET_ACCESS_KEY : Protected ❌ / Masked ✅

Leçon :

Protected = branches protégées uniquement (main, develop)
Masked = cache dans les logs (suffisant pour sécurité CI)
Rùgle : credentials AWS CI/CD → Masked uniquement
Credentials de production → Protected + Masked + environment scoped

INC-015 — Kaniko cache servant vieilles couches aprùs mise à jour deps

Contexte : Trivy image scan aprĂšs build SymptĂŽme :

CVE-2026-23949 jaraco.context 5.3.0 (fixed: 6.1.0)
CVE-2026-24049 wheel 0.45.1 (fixed: 0.46.2)
MĂȘme aprĂšs mise Ă  jour de requirements.txt

Cause :

Kaniko --cache=true → sert les anciennes couches pip
mĂȘme si requirements.txt a changĂ©
→ Trivy dĂ©tecte les vieilles versions dans l'image

Fix :

# Forcer rebuild complet
- /kaniko/executor
    --cache=false    # ← pas --no-cache (flag Docker, pas Kaniko)
    --destination $IMAGE_CANDIDATE

Leçon :

--no-cache n'existe pas dans Kaniko → utiliser --cache=false
AprÚs mise à jour de dépendances : toujours --cache=false
En production : remettre --cache=true pour les performances

INC-016 — Classic ELB créé par Envoy Gateway hors state Terraform

Contexte : aws-stop.sh → terraform destroy Symptîme :

Error: deleting EC2 Subnet: DependencyViolation
ENI ELB af50eba3... in-use
Security Group k8s-elb-* bloquant le VPC

Cause :

kubectl apply -k k8s/overlays/gateway-api/
→ Envoy Gateway Controller crĂ©e automatiquement :
  - Classic ELB
  - ENIs dans les subnets publics
  - Security Group k8s-elb-*
Ces ressources sont hors du state Terraform
→ terraform destroy ne les supprime pas
→ bloquent la suppression VPC/subnets

Fix :

# Supprimer dans le bon ordre AVANT terraform destroy
kubectl delete gateway --all -n fastapi
kubectl delete httproute --all -n fastapi
sleep 60  # laisser AWS supprimer ELB et ENIs

# Si ENIs encore présents
aws elb delete-load-balancer --load-balancer-name NOM_ELB
aws ec2 delete-security-group --group-id sg-xxxxx

# Puis terraform destroy
tfd

Leçon :

Kubernetes peut créer des ressources AWS hors state Terraform :
→ ELB (Envoy Gateway, Ingress ALB Controller)
→ ENIs (EKS, RDS, ELB)
→ Security Groups (k8s-elb-*)
→ EBS Volumes (StorageClass)
→ Route53 records (ExternalDNS)

RĂšgle : supprimer via kubectl AVANT terraform destroy
aws-stop.sh doit nettoyer les ressources Kubernetes en premier

INC-017 — POST /users/ requiert authentification JWT

Contexte : Test API FastAPI SymptĂŽme :

POST /users/ → 401 Not Authenticated
Impossible de créer le premier utilisateur sans token

Cause :

Bug de design dans app/routers/user.py
Depends(oauth2.get_current_user) sur create_user
L'inscription nĂ©cessite d'ĂȘtre dĂ©jĂ  authentifiĂ© → paradoxe

Fix :

# Supprimer le Depends sur create_user
@router.post("/")
def create_user(
    user: schemas.UserCreate,
    db: Session = Depends(get_db)
    # ← supprimer user_id: int = Depends(oauth2.get_current_user)
):

Leçon :

Les endpoints publics (inscription, health check) ne doivent pas
requérir d'authentification
Code review obligatoire avant merge :
→ vĂ©rifier que les endpoints ouverts sont intentionnels
→ vĂ©rifier que les endpoints protĂ©gĂ©s le sont correctement

INC-018 — Kaniko --no-cache inexistant

SymptĂŽme :

Error: unknown flag: --no-cache

Cause : --no-cache est un flag Docker, pas Kaniko. Fix : Utiliser --cache=false Leçon : Kaniko a sa propre API — ne pas confondre avec Docker CLI.


INC-019 — Trivy docker login inutile sur ECR

SymptĂŽme :

/bin/sh: docker: not found

Cause : Image Trivy sans Docker → docker login impossible. Fix : Supprimer le before_script docker login. Trivy s'authentifie nativement avec ECR via AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. Leçon : Trivy supporte ECR nativement via variables AWS — pas besoin de docker login.


INC-020 — amazon/aws-cli entrypoint bloque les scripts CI

SymptĂŽme :

aws: [ERROR]: Found invalid choice 'sh'

Cause : amazon/aws-cli a entrypoint: ["aws"] — GitLab passe les scripts directement à la CLI AWS. Fix :

image:
  name: amazon/aws-cli:latest
  entrypoint: [""]

Leçon : Toujours vérifier l'entrypoint des images non-standard. Ajouter entrypoint: [""] pour reset.


INC-021 — hashicorp/terraform entrypoint bloque les scripts CI

SymptĂŽme :

Terraform has no command named "sh"

Cause : hashicorp/terraform a entrypoint: ["terraform"]. Fix :

image:
  name: hashicorp/terraform:1.15
  entrypoint: [""]

Leçon : MĂȘme pattern qu'INC-020. Images officielles HashiCorp ont des entrypoints custom.


INC-022 — Permissions IAM manquantes pour Terraform EKS

SymptĂŽme :

AccessDenied: not authorized to perform: iam:TagRole
AccessDenied: not authorized to perform: iam:ListInstanceProfilesForRole

Cause : Policy terraform_ci trop restrictive — manque plusieurs actions IAM requises par Terraform lors de la crĂ©ation de rĂŽles EKS avec tags. Fix : Ajouter dans la policy :

iam:TagRole, iam:UntagRole, iam:TagPolicy
iam:ListRolePolicies, iam:ListAttachedRolePolicies
iam:GetPolicyVersion, iam:ListPolicyVersions
iam:ListInstanceProfilesForRole
iam:ListRoleTags, iam:ListUserTags
iam:TagUser, iam:UntagUser

Leçon : Terraform nĂ©cessite plus de permissions IAM que prĂ©vu — dĂ©couverte itĂ©rative. En prod, utiliser IAM Access Analyzer pour identifier les permissions minimales.


INC-023 — CI_PIPELINE_SOURCE "web" vs include rules

SymptÎme : Pipeline vide lors d'un Run Pipeline manuel. Cause : CI_PIPELINE_SOURCE = "web" quand déclenché depuis l'UI GitLab. Include rules utilisant d'autres valeurs ne matchaient pas. Fix :

include:
  - local: .gitlab/ci/infra.yml
    rules:
      - if: '$CI_PIPELINE_SOURCE == "web"'
      - if: '$CI_PIPELINE_SOURCE == "schedule"'

Leçon : Toujours ajouter un job debug temporaire pour vérifier la valeur réelle de CI_PIPELINE_SOURCE.


INC-024 — spec:inputs + sĂ©parateur --- obligatoire

SymptĂŽme : Inputs non visibles dans l'UI Run Pipeline. Cause : spec: inputs: sans sĂ©parateur --- — GitLab ne peut pas parser la configuration. Fix :

spec:
  inputs:
    action:
      default: "status"
      options:
        - status
        - start
        - stop

---   # ← obligatoire !

include:
  ...

Leçon : Le séparateur --- est mandatory avec spec: inputs: dans GitLab CI.


INC-025 — Variables projet CI/CD prioritĂ© > variables fichier YAML

SymptĂŽme : AWS_ACCESS_KEY_ID dans le fichier YAML Ă©crasĂ© par la variable projet. Cause : Variables dĂ©finies dans Settings → CI/CD → Variables ont une prioritĂ© HAUTE et Ă©crasent les variables du fichier pipeline. Fix : Utiliser before_script avec export pour forcer les credentials :

before_script:
  - export AWS_ACCESS_KEY_ID=$AWS_INFRA_ACCESS_KEY_ID
  - export AWS_SECRET_ACCESS_KEY=$AWS_INFRA_SECRET_ACCESS_KEY

Leçon :

PrioritĂ© variables GitLab CI (haute → basse) :
1. Trigger/schedule variables
2. Settings → CI/CD → Variables (projet)
3. Variables fichier YAML
4. Variables groupe

INC-026 — terraform.tfvars gitignored → TF_VAR_ en CI

SymptĂŽme :

Error: No value for required variable — var.db_password

Cause : terraform.tfvars est dans .gitignore → pas disponible en CI/CD. Fix : Utiliser le prĂ©fixe TF_VAR_ dans les variables GitLab CI. Terraform lit automatiquement les variables d'environnement TF_VAR_*.

GitLab CI Variables :
TF_VAR_db_password = <REDACTED>  ← Masked ✅ (valeur stockĂ©e uniquement dans GitLab CI)

Leçon : Ne jamais committer les tfvars (secrets). Utiliser TF_VAR_ en CI ou AWS Secrets Manager.


INC-027 — pip / pip3 introuvable dans alpine/ansible

Contexte : Job bootstrap CI — installation des dĂ©pendances Python Ansible SymptĂŽme :

/bin/bash: line 193: pip: command not found
/bin/bash: line 193: pip3: command not found

Cause :

alpine/ansible:latest inclut Python mais pas pip/pip3
pip n'est pas disponible par défaut sur Alpine
pip3 non plus sans installation explicite

Fix :

- apk add --no-cache ... py3-pip   # ← ajouter py3-pip
- pip3 install kubernetes pyyaml --break-system-packages

Leçon :

Sur Alpine : pip n'existe pas, pip3 doit ĂȘtre installĂ© via apk add py3-pip
Ne pas supposer que pip/pip3 est présent dans une image Alpine
MĂȘme rĂšgle pour python3 : toujours installer explicitement

INC-028 — Envoy Gateway Helm repo HTTP introuvable (404)

Contexte : Ansible bootstrap — installation Envoy Gateway via Helm Symptîme :

Error: looks like "https://envoyproxy.io/charts" is not a valid chart repository
Error: looks like "https://gateway.envoyproxy.io/helm" is not a valid chart repository
failed to fetch .../index.yaml : 404 Not Found

Cause :

Envoy Gateway a migré vers OCI registry (Docker Hub)
Plus aucun repo Helm HTTP traditionnel disponible depuis v1.x
Les URLs https://envoyproxy.io/charts et https://gateway.envoyproxy.io/helm
ne servent plus d'index.yaml

Fix :

# Ansible bootstrap.yml — utiliser OCI directement
- name: Install Envoy Gateway
  kubernetes.core.helm:
    name: envoy-gateway
    chart_ref: oci://docker.io/envoyproxy/gateway-helm
    chart_version: v1.4.0
    # plus besoin de helm repo add

Leçon :

De plus en plus de projets Helm migrent vers OCI (Docker Hub, GHCR)
Avant d'utiliser un chart : vérifier la doc officielle pour l'URL courante
OCI = pas de helm repo add nécessaire, utiliser chart_ref: oci://...

INC-029 — eks:DescribeCluster manquant pour le user CI

Contexte : Job bootstrap — aws eks update-kubeconfig Symptîme :

AccessDeniedException: User arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci
is not authorized to perform: eks:DescribeCluster

Cause :

aws eks update-kubeconfig requiert eks:DescribeCluster
La policy IAM du user CI ne l'incluait pas
Le job bootstrap utilisait les credentials CI (AWS_ACCESS_KEY_ID projet)
au lieu des credentials infra → user sans permission EKS

Fix :

Ajouter *infra_credentials dans le before_script du job bootstrap :
- export AWS_ACCESS_KEY_ID=$AWS_INFRA_ACCESS_KEY_ID
- export AWS_SECRET_ACCESS_KEY=$AWS_INFRA_SECRET_ACCESS_KEY

Variables GitLab CI → priority haute Ă©crase les variables YAML
→ before_script export est obligatoire pour forcer les credentials infra

Leçon :

Les variables YAML (variables: section) sont écrasées par les variables projet
→ toujours utiliser before_script export pour les credentials sensibles
Voir INC-025 (priorité variables GitLab CI)

INC-030 — needs DAG vs stages pour un flow destroy infra

Contexte : Conception pipeline infra — ordering teardown → destroy Symptîme : (design, pas d'erreur runtime)

needs: - teardown sur infra-stop → risque de skip accidentel
Si teardown absent du pipeline → infra-stop peut dĂ©marrer sans cleanup
→ terraform destroy sans avoir libĂ©rĂ© ELBs → DependencyViolation

Cause :

needs exécute en DAG (graph) : flexible mais contournable
- peut skip des jobs dépendants si optional ou absent du pipeline
- allow_failure: true sur le job référencé bypass la garantie
Stages = ordre strict séquentiel : stage N+1 ne démarre jamais avant N

Fix :

# ❌ needs pour destroy = risque
infra-stop:
  needs: [ teardown ]   # contournable

# ✅ stages pour destroy = garanti
stages:
  - infra      # status + start
  - bootstrap  # aprĂšs start
  - teardown   # avant stop
  - destroy    # terraform destroy

# teardown (stage 3) TOUJOURS avant infra-stop (stage 4)
# rÚgle centrale, pas distribuée sur chaque job

Leçon :

needs = flexibilitĂ© DAG → pour des dĂ©pendances optionnelles ou parallĂšles
stages = ordre strict → pour les flows destructifs (destroy, drop, purge)
RĂšgle : ne jamais utiliser needs pour un flow infra destroy

SPRINT 3 — EKS Access Entries + Deploy End-to-End (2026-05-19)

INC-031 — AmazonEKSClusterPolicy attachĂ©e Ă  un IAM user

Contexte : Configuration youss_admin pour accĂšs kubectl depuis devcontainer SymptĂŽme :

youss_admin ne pouvait pas accéder au cluster malgré la policy attachée
AmazonEKSClusterPolicy = droits insuffisants pour kubectl

Cause :

AmazonEKSClusterPolicy est la policy du rĂŽle IAM du control plane EKS
Elle est prévue pour eks.amazonaws.com (service), pas pour un user humain
Attachée à un user, elle ne donne aucun droit kubectl utile

Fix :

# ❌ Mauvaise policy pour un user
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"

# ✅ Policy minimale pour gĂ©nĂ©rer un kubeconfig
resource "aws_iam_policy" "youss_admin_eks_connect" {
  policy = jsonencode({
    Statement = [{
      Action   = ["eks:DescribeCluster", "eks:ListClusters"]
      Effect   = "Allow"
      Resource = "*"
    }]
  })
}

Leçon :

Deux couches pour l'accĂšs EKS :
1. IAM (AWS)        → eks:DescribeCluster pour gĂ©nĂ©rer le kubeconfig
2. EKS Access Entry → AmazonEKSClusterAdminPolicy pour les droits K8s
AmazonEKSClusterPolicy ≠ permission kubectl → c'est pour le control plane

INC-032 — eks:DescribeCluster manquant pour gitlab-ci (deploy job)

Contexte : Job deploy dans .gitlab-ci.yml → aws eks update-kubeconfig Symptîme :

User arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci is not
authorized to perform: eks:DescribeCluster

Cause :

La policy ecr_push de gitlab-ci couvre uniquement ECR
aws eks update-kubeconfig nĂ©cessite eks:DescribeCluster mĂȘme si le user
a déjà un EKS Access Entry avec AmazonEKSEditPolicy
IAM et RBAC K8s sont deux couches indépendantes

Fix :

# Ajouter dans la policy ecr_push
{
  Sid      = "EKSConnect"
  Effect   = "Allow"
  Action   = ["eks:DescribeCluster"]
  Resource = "*"
}

Leçon :

EKS Access Entry = droits dans Kubernetes
eks:DescribeCluster = droit AWS pour atteindre le endpoint EKS
Les deux sont nécessaires, ils ne se substituent pas

INC-033 — kubectl cluster-info forbidden (namespace kube-system)

Contexte : before_script du deploy job — vĂ©rification connexion cluster SymptĂŽme :

Error from server (Forbidden): services is forbidden:
User "fastapi-eks-gitlab-ci" cannot list resource "services"
in API group "" in the namespace "kube-system"

Cause :

kubectl cluster-info liste les services dans kube-system
gitlab-ci a AmazonEKSEditPolicy scopé namespace fastapi uniquement
Pas d'accĂšs aux namespaces systĂšme

Fix :

# ❌ NĂ©cessite accĂšs kube-system
- kubectl cluster-info

# ✅ VĂ©rifie les droits dans le bon namespace
- kubectl auth can-i create deployments -n fastapi

Leçon :

Toujours tester les commandes de vérification avec le user CI
kubectl cluster-info = opĂ©ration cluster-wide → incompatible avec least privilege
kubectl auth can-i = vérification ciblée par namespace

INC-034 — kubectl create namespace forbidden + doublon bootstrap

Contexte : script deploy job — crĂ©ation namespace fastapi SymptĂŽme :

Error from server (Forbidden): namespaces is forbidden:
User "fastapi-eks-gitlab-ci" cannot create resource "namespaces"

Cause :

Création de namespace = opération cluster-level
AmazonEKSEditPolicy (namespace scope) ne couvre pas la création de namespaces
De plus, le namespace fastapi est déjà créé par ansible/bootstrap.yml

Fix :

# Supprimer du deploy job :
# kubectl create namespace fastapi --dry-run=client -o yaml | kubectl apply -f -
# kubectl apply -f k8s/base/namespace.yaml
# Le namespace est créé par bootstrap Ansible (cluster-admin)

Leçon :

Séparer les responsabilités :
- bootstrap (cluster-admin) → crĂ©er namespaces, installer helm charts
- deploy (edit ns fastapi)  → dĂ©ployer l'application uniquement

INC-035 — Init:InvalidImageName — ${ECR_IMAGE} non substituĂ©

Contexte : kubectl apply -f k8s/base/deployment.yaml sans envsubst SymptĂŽme :

Init:InvalidImageName
spec.initContainers{migrations}: Failed to apply default image tag
"${ECR_IMAGE}": invalid reference format

Cause :

deployment.yaml contient image: "${ECR_IMAGE}" (placeholder envsubst)
kubectl apply -f deployment.yaml applique le fichier tel quel
${ECR_IMAGE} littéral = nom d'image invalide pour Kubernetes
kubectl set image ne mettait Ă  jour que le container principal,
pas l'initContainer migrations

Fix :

# before_script — installer gettext pour envsubst
- yum install -y gettext --quiet

# script
- export ECR_IMAGE="$ECR_REGISTRY/$ECR_REPOSITORY:$CI_COMMIT_SHA"
- envsubst < k8s/base/deployment.yaml | kubectl apply -f - -n fastapi

Leçon :

envsubst substitue toutes les variables shell dans un fichier texte
Nécessite gettext (yum install) sur Amazon Linux
Remplace kubectl set image → une seule commande, tous les containers mis à jour

INC-036 — DB_PASSWORD manquant dans fastapi-secrets

Contexte : initContainer migrations → validation Pydantic Settings Symptîme :

pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
DB_PASSWORD
  Field required [type=missing]

Cause :

kubectl create secret fastapi-secrets ne créait que SECRET_KEY
DB_PASSWORD non inclus → Pydantic Settings Ă©choue au dĂ©marrage
TF_VAR_db_password existe dans GitLab CI/CD mais n'était pas passé au secret

Fix :

kubectl create secret generic fastapi-secrets \
  --from-literal=SECRET_KEY="$SECRET_KEY" \
  --from-literal=DB_PASSWORD="$TF_VAR_db_password" \
  -n fastapi \
  --dry-run=client -o yaml | kubectl apply -f -

Leçon :

TF_VAR_db_password = variable Terraform ET variable deploy → mĂȘme source
Lister tous les champs requis par Pydantic Settings avant de créer le secret

INC-037 — terraform apply sur ancien pipeline → No changes (fix non appliquĂ©)

Contexte : Apply du persistent stack pour activer eks:DescribeCluster sur gitlab-ci SymptĂŽme :

terraform plan : No changes. Infrastructure is up-to-date.
Policy ecr_push non mise à jour malgré le fix mergé dans develop

Cause :

Relance d'un ancien pipeline GitLab (avant le merge du fix)
Le job terraform apply de cet ancien pipeline utilisait l'ancien code
git pull origin develop non effectué avant l'apply manuel

Fix :

aws-vault exec iamadmin -- bash
cd terraform/persistent
git pull origin develop    # ← obligatoire avant apply manuel
terraform apply -auto-approve

Leçon :

Relancer un ancien pipeline ≠ appliquer le code actuel
Pour un apply manuel : toujours git pull avant terraform apply
"No changes" peut ĂȘtre trompeur si le code source n'est pas Ă  jour

INC-038 — State lock avec mauvais credentials (youss_admin)

Contexte : Tentative terraform apply depuis devcontainer avec youss_admin SymptĂŽme :

Error acquiring the state lock

Cause :

youss_admin n'a que eks:DescribeCluster + eks:ListClusters
S3 (state) et DynamoDB (lock) non accessibles
L'erreur ressemble à un vrai lock mais c'est en réalité un AccessDenied

Fix :

# Terraform → toujours avec iamadmin
aws-vault exec iamadmin -- bash

# youss_admin = kubectl uniquement
awsyouss  # → kubectl get nodes, kubectl logs...

Leçon :

"Error acquiring state lock" peut masquer un AccessDenied S3/DynamoDB
Deux profils, deux usages :
- iamadmin   → Terraform, AWS Console, opĂ©rations admin
- youss_admin → kubectl uniquement

INC-039 — Hardening Sprint 3 jamais appliquĂ© sur EKS (deploy fichier par fichier)

Contexte : Job deploy de .gitlab-ci.yml, aprĂšs livraison du hardening Sprint 3 (PSA, ServiceAccount, PDB, NetworkPolicy) SymptĂŽme :

Le hardening est dans les manifests, validé en local (kustomize build, kube-linter, kind),
mais sur EKS : namespace sans labels PSA, pas de SA fastapi, pas de PDB, pas de NetworkPolicy.
Le prochain deploy aurait cassé (deployment référence serviceAccountName: fastapi inexistant).

Cause :

Le deploy appliquait les manifests un par un :
- kubectl apply -f configmap.yaml / service.yaml / hpa.yaml
- envsubst < deployment.yaml | kubectl apply -f -
Soit 4 manifests sur 10. namespace.yaml (PSA), serviceaccount.yaml, pdb.yaml et les
3 networkpolicy-*.yaml n'Ă©taient JAMAIS appliquĂ©s → le durcissement n'atteignait pas le cluster.

Fix :

# before_script : installer kustomize (en plus de kubectl), retirer gettext/envsubst
curl -sSL ".../kustomize_v5.4.3_linux_amd64.tar.gz" | tar -xz && mv kustomize /usr/local/bin/

# script : injecter le nom+tag image puis appliquer TOUT le base d'un coup
cd k8s/base
kustomize edit set image fastapi="$ECR_IMAGE"   # remplace envsubst, met Ă  jour init + main container
cd "$CI_PROJECT_DIR"
kubectl apply -k k8s/base/                       # applique les 10 ressources (kustomize)

Leçon :

Valider le hardening en local ne suffit pas s'il n'est pas DÉPLOYÉ par le mĂȘme chemin.
Un deploy qui cherry-picke les fichiers laisse une partie du durcissement hors du cluster.
DĂ©ployer le mĂȘme artefact que celui qu'on lint : `kustomize build` = source de vĂ©ritĂ© unique.
Toujours tester end-to-end sur l'environnement cible (EKS), pas seulement en local (kind).
Suite (infra UP) : ces points "Ă  valider" se sont rĂ©vĂ©lĂ©s NON enforced — PSA (INC-045, corrigĂ©) et NetworkPolicy (INC-046, mitigation #55). « ConfigurĂ© n'est pas enforced » : valider par test nĂ©gatif sur le cluster.

INC-040 — terraform apply rejetĂ© : accent dans une description de security group

Contexte : Premier aws-start aprÚs le fix tfsec #50 (descriptions ajoutées aux rÚgles egress des SG RDS et EKS) SymptÎme :

Error: "egress.0.description" doesn't comply with restrictions
("^[0-9A-Za-z_ .:/()#,@\[\]+=&;{}!$*-]*$"): "Réponses intra-VPC uniquement"
  with module.rds.aws_security_group.rds

Cause :

AWS valide la description des rĂšgles de SG contre une regex strictement ASCII.
Le "Ă©" de "RĂ©ponses" (UTF-8) n'appartient pas Ă  l'ensemble autorisĂ© → rejet cĂŽtĂ© API.
Ni tfsec (scan sécurité), ni terraform validate (syntaxe/types), ni terraform plan
ne le détectent : la contrainte est imposée par l'API AWS au moment de l'apply.

Fix :

# Avant : description = "Réponses intra-VPC uniquement"
# AprĂšs : description = "Intra-VPC responses only"
# SG EKS aussi : "Communication ... vers nodes (intra-VPC)" -> "Control-plane to nodes (intra-VPC)"

Leçon :

Convention : tout en anglais ASCII dans la conf (valeurs envoyées aux APIs cloud :
descriptions, tags, noms). Les linters statiques ne couvrent pas la conformité des
valeurs aux contraintes d'API. Garde-fou possible : check pre-commit/CI rejetant
tout caractÚre non-ASCII dans les .tf (viable une fois tout passé en anglais).

INC-041 — kubectl i/o timeout : jobs exĂ©cutĂ©s sur un runner GitLab partagĂ© (2026-05-23)

Contexte : Marathon bootstrap EKS. infra-start et bootstrap échouent avant toute authentification. SymptÎme :

kubectl get nodes
dial tcp <api-eks>:443: i/o timeout

Cause :

.gitlab-ci-infra.yml n'avait pas de `default: tags`. Les jobs partaient sur un runner
GitLab partagé (GCP, IP 35.x) au lieu du runner self-hosted. L'IP du runner partagé
n'est pas dans cluster_public_access_cidrs (82.66.53.81/32) → EKS drop les paquets
silencieusement → i/o timeout (et non 401/403).

Fix :

default:
  tags: [ubuntu]   # force tous les jobs sur le runner self-hosted (IP autorisée)

Leçon :

Pinner le runner via `tags`. Un i/o timeout sur l'API EKS = problÚme réseau/CIDR,
pas un problĂšme d'auth (qui donnerait 401/403). Le code d'erreur oriente le diagnostic.

INC-042 — Variable GitLab "Protected" non injectĂ©e sur feature branch (2026-05-23)

Contexte : Test d'un fix de CIDR sur une feature branch pendant le marathon. SymptĂŽme :

TF_VAR_cluster_public_access_cidrs absente en CI → terraform retombe sur le placeholder
par défaut 203.0.113.0/24 (RFC 5737), le fix testé n'a aucun effet.

Cause :

La variable est "Protected" dans GitLab → injectĂ©e uniquement sur branches/tags protĂ©gĂ©s
(main, develop). Sur une feature branch non protégée, elle n'existe pas.

Fix :

Pour une valeur non secrÚte (une IP publique) : 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 un fix en MR avant merge. MĂȘme classe que INC-001.

INC-043 — Ansible : crash sur condition until + connexion SSH sur localhost (2026-05-23)

Contexte : Play bootstrap Ansible (kubernetes.core) attendant que les nodes soient prĂȘts. 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 un hĂŽte distant joint en SSH

Fix :

until: (nodes.resources | default([])) | length > 0   # (1) default() la variable registered
connection: local                                      # (2) play exécuté en local

Leçon :

Toujours `default()` une variable registered utilisée dans une condition `until`.
Pour les plays kubernetes.core qui tournent en local : connection: local.

INC-044 — Bootstrap 401 : rĂ©gression de la lib Python kubernetes 36.0.0 (2026-05-23)

Contexte : Boss final du marathon. kubernetes.core renvoie 401 alors que kubectl passe juste avant. SymptĂŽme :

kubernetes.client.exceptions.UnauthorizedException: (401) Unauthorized
# alors que `kubectl get nodes` dans le before_script passe avec les MÊMES credentials

Cause :

RĂ©gression dans la lib Python kubernetes 36.0.0 cassant 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 ; une version
antérieure marchait 4 jours avant.

Diagnostic (élimination méthodique) :

token    → curl brut HTTP 200 ✅ (token valide)
identitĂ© → kubectl auth whoami = bon user ✅
OS       → Ă©choue aussi sur Ubuntu ❌ (pas un problĂšme Alpine)
version  → kubernetes==31.0.0 ✅ / 36.0.0 ❌  ← LA cause
Reproduit en local hors pipeline pour itérer en secondes.

Fix :

- pip3 install kubernetes pyyaml --break-system-packages
+ pip3 install kubernetes==31.0.0 pyyaml --break-system-packages   # 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çon du marathon : tester à chaque itération (batcher 4 changements non testés a
empilé 4 causes), descendre au niveau le plus bas pour isoler (curl > client Python >
Ansible), reproduire en local pour sortir de la boucle pipeline coûteuse.

INC-045 — PSA jamais enforced sur EKS : namespace ownĂ© par le bootstrap, pas par le deploy (2026-05-24)

Contexte : PremiĂšre validation end-to-end du deploy applicatif (#52) avec l'infra up. SymptĂŽme :

kubectl apply -k k8s/base/
Error from server (Forbidden): namespaces "fastapi" is forbidden (patch des 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 → le deploy tentait de poser les labels PSA → Forbidden. Le bootstrap
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 Ansible (cluster-admin) : il 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 :

Sous enforce restricted : pod fastapi (C3 compliant) admis, rollout 1/1.
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 chemin d'application diffÚre. 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) (2026-05-24)

Contexte : Dans la foulée du fix PSA (INC-045), test négatif méthodique sur les NetworkPolicy. SymptÎme :

# depuis le pod fastapi, sortie sur le port 80 (hors allowlist egress) :
curl -m 5 http://example.com → 200   # devrait ĂȘtre bloquĂ© par default-deny + allow-fastapi
kubectl get netpol -n fastapi        # les 3 policies sont pourtant bien présentes

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
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 #55) :

resource "aws_eks_addon" "vpc_cni" {
  cluster_name         = ...
  addon_name           = "vpc-cni"     # version >= 1.14
  configuration_values = jsonencode({ 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.

INC-047 — Edit ClusterRole ne couvre pas les CRDs ESO (deploy app 403) (2026-05-29)

Contexte : Premier run du deploy app end-to-end aprÚs le merge de MR-D (#33), qui pose SecretStore + ExternalSecret dans k8s/base/. Le bootstrap (cluster-admin) avait installé ESO sans souci, mais le deploy (least-privilege) ne peut rien appliquer sur les CRDs. SymptÎme :

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" in the
namespace "fastapi"
# idem secretstores
# 403 sur le get server-side du 3-way merge, AVANT l'apply

Cause :

Le user CI est mappé via EKS Access Entry à AmazonEKSEditPolicy scopée au ns
fastapi (terraform/ephemeral/main.tf:115-125). Cette policy AWS-managée
correspond au ClusterRole built-in "edit", qui est AGRÉGÉ via le sĂ©lecteur
rbac.authorization.k8s.io/aggregate-to-edit: "true". Par défaut, edit couvre
Deployments / Services / ConfigMaps mais PAS les CRDs de tiers. Le chart Helm
external-secrets v2.5.0 n'installe pas de ClusterRole avec ce label.
MĂȘme classe que INC-045 (PSA / namespace) : ressources posĂ©es sans Ă©tendre
les permissions du compte qui doit les appliquer.

Fix tenté (MR !116) :

# Premier essai : ClusterRole d'agrĂ©gation — structurellement correcte
# mais sans effet sur les users mappés via AWS Access Policy (voir Cause réelle).
- name: Create aggregation ClusterRole to extend "edit" with ESO CRDs
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRole
      metadata:
        name: aggregate-eso-to-edit
        labels:
          rbac.authorization.k8s.io/aggregate-to-edit: "true"
      rules:
        - apiGroups: ["external-secrets.io"]
          resources: ["externalsecrets", "secretstores"]
          verbs: ["get","list","watch","create","update","patch","delete"]

Preuve (résultat réel) :

kubectl get clusterrole edit -o yaml | grep -A2 external-secrets
# → external-secrets.io APPARAÎT dans edit.rules (agrĂ©gation structurelle OK)
kubectl auth can-i create externalsecrets -n fastapi \
  --as=arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci
# → no  (rĂ©ponse fiable, mal interprĂ©tĂ©e comme faux nĂ©gatif sur le moment)
# Re-run pipeline DEPLOY=true : MÊME 403 sur SecretStore + ExternalSecret

Cause réelle (pivot vers INC-048) :

AmazonEKSEditPolicy n'est PAS un binding vers le ClusterRole built-in "edit"
agrĂ©gĂ©. C'est une policy AWS-managed, snapshot statique — elle ne suit PAS
l'agrĂ©gation RBAC Kubernetes. Étendre "edit" via aggregate-to-edit n'a aucun
effet sur les users mappés via EKS Access Entry + AWS Access Policy.
→ Vrai fix : binding RBAC explicite sur l'ARN. Voir INC-048.

Leçon :

AmazonEKSEditPolicy (et les AWS Access Policies) sont des snapshots statiques.
Le can-i --as=<ARN> était fiable dÚs le début.
ConservĂ© en doc : rĂ©cit honnĂȘte du premier essai, matĂ©riau d'entretien.

INC-048 — AmazonEKSEditPolicy snapshot statique : seul un binding RBAC explicite fonctionne (2026-05-30)

Contexte : Suite directe d'INC-047. Le premier fix (ClusterRole d'agrégation MR !116) n'a pas résolu le 403. Pivot vers un binding RBAC explicite sur l'ARN du user CI. SymptÎme :

# MĂȘmes 403 qu'INC-047 aprĂšs re-run du bootstrap avec aggregate-eso-to-edit
externalsecrets.external-secrets.io "fastapi-secrets" is forbidden: ...
# kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> → no

Cause :

AmazonEKSEditPolicy est une policy AWS-managed, snapshot statique des
permissions view+edit publiées par AWS. Elle NE SUIT PAS l'agrégation RBAC
Kubernetes. Étendre le ClusterRole "edit" via aggregate-to-edit n'a aucun
effet sur les users mappés via EKS Access Entry + AWS Access Policy.

Pour étendre les permissions d'un tel user : binding RBAC K8s EXPLICITE
(Role/RoleBinding) bindant directement son ARN.

Fix :

# ansible/bootstrap.yml — aprĂšs crĂ©ation du namespace fastapi
- name: Create Role for CI user on ESO CRDs in fastapi namespace
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: rbac.authorization.k8s.io/v1
      kind: Role
      metadata:
        name: fastapi-deploy-eso
        namespace: fastapi
      rules:
        - apiGroups: ["external-secrets.io"]
          resources: ["externalsecrets", "secretstores"]
          verbs: ["get","list","watch","create","update","patch","delete"]

- name: Create RoleBinding for CI user on ESO CRDs in fastapi namespace
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
        name: fastapi-deploy-eso
        namespace: fastapi
      subjects:
        - kind: User
          name: "arn:aws:iam::{{ aws_account.stdout }}:user/ci/{{ project_name }}-gitlab-ci"
          apiGroup: rbac.authorization.k8s.io
      roleRef:
        kind: Role
        name: fastapi-deploy-eso
        apiGroup: rbac.authorization.k8s.io
# Symétrique dans teardown.yml : Delete RoleBinding + Delete Role avant ESO uninstall.
# ClusterRole aggregate-eso-to-edit (INC-047) retirée du bootstrap et du teardown.

Preuve :

kubectl auth can-i create externalsecrets -n fastapi \
  --as=arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci
# → yes
# Pipeline app DEPLOY=true : stage deploy vert
kubectl -n fastapi get secretstore,externalsecret
# → SecretStore: Valid, ExternalSecret: SecretSynced
kubectl -n fastapi get pods
# → fastapi-* 1/1 Ready
curl -sS https://api.devopsyouss.com/healthz/ready
# → HTTP 200

Leçon :

AmazonEKSEditPolicy (et les AWS-managed Access Policies en général) sont des
snapshots statiques — ils ne suivent PAS l'agrĂ©gation RBAC K8s.

Pour étendre les permissions d'un user mappé via EKS Access Entry :
→ Binding RBAC EXPLICITE sur l'ARN du user (Role/RoleBinding dans le ns cible)
→ Jamais via aggregate-to-edit si le user est mappĂ© par une AWS Access Policy

Méta-leçon sur le diagnostic :
- INC-047 conservĂ© en doc : honnĂȘtetĂ© technique, matĂ©riau d'entretien
- Le can-i --as=<ARN> était fiable dÚs le début
- Quand un test d'autorisation retourne no, c'est no — ne pas le rationaliser
  comme faux négatif sans preuve

Recommandations Pipeline Infra

1. entrypoint: [""] obligatoire pour images non-standard
   (aws-cli, terraform, kaniko, trivy...)

2. CI_PIPELINE_SOURCE :
   push             → git push
   merge_request_event → MR
   web              → Run Pipeline UI
   schedule         → Schedule

3. spec: inputs: → sĂ©parateur --- obligatoire

4. Variables projet écrasent variables fichier
   → utiliser before_script export pour forcer

5. Séparer pipeline app et infra :
   .gitlab-ci.yml      → app (push/MR)
   .gitlab-ci-infra.yml → infra (web/schedule)

6. IAM Least Privilege :
   gitlab_ci       → ECR push uniquement
   gitlab_ci_infra → Terraform + infra status

7. terraform.tfvars → gitignored → TF_VAR_ en CI

Références

  • Sprint 3 — Pipeline CI/CD + Infra
  • Date : 2026-05-18

Recommandations Générales

Pipeline CI/CD

1. Variables CI : Masked (pas Protected) pour les branches feature
2. Kaniko plutĂŽt que Docker-in-Docker sur Alpine
3. Pattern candidate → scan → promote (jamais push sans scan)
4. --cache=false aprÚs mise à jour de dépendances
5. Trivy image scan APRÈS build, AVANT push final

Terraform

1. Ne jamais interrompre apply/destroy (tmux obligatoire)
2. Séparer ressources persistantes et éphémÚres
3. terraform import avant toute restructuration en production
4. State lock : vérifier .tflock dans S3 si blocage
5. create_nat_gateway=true obligatoire avec EKS

Kubernetes / EKS

1. Supprimer ressources K8s AVANT terraform destroy
   (ELB, Ingress, Gateway libĂšrent les ENIs)
2. ENIs orphelins : attendre 10-15 min ou chercher par VPC
3. EKS nodes : NAT Gateway obligatoire pour accĂšs internet
4. HPA : metrics-server obligatoire (pas installé par défaut sur EKS)
5. Alembic migrations : init container recommandé

Sécurité

1. DĂ©pendances abandonnĂ©es → migrer (passlib → bcrypt, python-jose → PyJWT)
2. Auditer réguliÚrement les dépendances (pip-audit, trivy)
3. Multi-stage build pour réduire CVEs image de base
4. .trivyignore : toujours commenter avec justification + date
5. Endpoints publics vs protégés : vérification systématique

Coûts AWS

1. ECR + IAM dans persistent/ → jamais dĂ©truits
2. EKS + RDS + VPC dans ephemeral/ → destroy chaque soir
3. aws-stop.sh : nettoyer ELB avant destroy (sinon blocage ENIs)
4. NAT Gateway : ~0.045$/h → destroy quand pas utilisĂ©
5. EKS Control Plane : ~0.10$/h → la ressource la plus coĂ»teuse

Références

  • Sprint 0 : INC-001, INC-002, INC-003
  • Sprint 1 : INC-004, INC-005, INC-006, INC-007
  • Sprint 2 : INC-008, INC-009, INC-010, INC-011, INC-012
  • Sprint 3 : INC-013, INC-014, INC-015, INC-016, INC-017
  • Sprint 3 (suite) : INC-018, INC-019, INC-020, INC-021, INC-022, INC-023, INC-024, INC-025, INC-026
  • Sprint 3 (2026-05-18) : INC-027, INC-028, INC-029, INC-030
  • Sprint 3 (2026-05-19) : INC-031, INC-032, INC-033, INC-034, INC-035, INC-036, INC-037, INC-038
  • Sprint 3 (2026-05-23) : INC-039, INC-040, INC-041, INC-042, INC-043, INC-044, INC-045, INC-046
  • Sprint 3 (2026-05-29) : INC-047
  • Sprint 3 (2026-05-30) : INC-048
  • Date : 2026-05-18