Skip to content

L'image que prod ne trouve plus (rétention ECR multi-env)

Deux fois, le registre d'images a supprimé l'image que la production servait. Cette page explique le mécanisme, ce que le pipeline fabrique exactement, et pourquoi la correction livrée le 2026-08-15 n'est pas un réglage de plus.

En trois phrases

Le registre garde les N dernières images. Les trois environnements y déposaient des images du même type, alors que dev en pousse une à chaque merge et que prod reste figé pendant des semaines : dev a donc fini par pousser l'image de prod hors du registre, deux fois.

Monter N de 5 à 30, puis à 100, n'a fait que repousser la date. Un compteur mesure un rapport de cadences, pas un volume.

La correction donne une étagère à part aux environnements dont le tag stagne, prod et staging. La cadence de dev ne peut plus atteindre celle de prod, quel que soit le nombre de merges.

Les mots du sujet

À lire une fois, tout le reste s'appuie dessus.

Terme Ce que c'est
ECR Elastic Container Registry, le registre d'images de conteneur d'AWS. L'entrepôt où le pipeline dépose les images et d'où le cluster les récupère. Deux dépôts ici : fastapi-eks/fastapi et fastapi-eks/frontend.
image Le paquet qui contient l'application prête à tourner. Identifiée de façon absolue par son digest, une empreinte calculée sur son contenu.
tag Une étiquette collée sur un digest, pour le désigner par un nom lisible. Plusieurs tags peuvent pointer sur le même digest — c'est la base de la promotion. Le tag est une étiquette, pas l'image.
lifecycle policy La règle de ménage du registre : quelles images expirent, selon quel critère. C'est elle qui a supprimé l'image de prod. Elle ne sait rien du cluster.
overlay Le dossier de configuration propre à un environnement (k8s/overlays/fastapi/prod/). C'est là qu'est écrit le tag de l'image que cet env doit servir.
write-back Le job qui, après un build, réécrit dans Git le tag de l'overlay de dev. C'est ce qui fait avancer dev tout seul.
promotion Faire avancer staging ou prod : une MR recopie dans leur overlay un tag déjà construit et scanné. Jamais de reconstruction.
ArgoCD Le composant qui lit Git en continu et applique au cluster ce qui y est écrit. Si l'overlay dit « sers deployed-f289… », il l'exige — même si elle n'existe plus.

Ce que le pipeline fabrique, étape par étape

C'est la partie qui paraît compliquée. Elle tient en une idée : une seule image est construite, et elle change seulement d'étiquette.

MERGE SUR DEVELOP 1 · Build 2 · Scan Trivy 3 · Promote 4 · Write-back 5 · ArgoCD l'image est construite vulnérabilités pose un 2e tag écrit dans Git applique au cluster CE QUE PORTE L'IMAGE candidate-abc123-4711 + deployed-abc123 overlay dev : deployed-abc123 tag jetable, un par pipeline même digest, deuxième étiquette le tag entre dans Git, donc dans le déploiement STAGING ET PROD NE PASSENT PAR AUCUNE DE CES ÉTAPES Ils avancent uniquement par une MR de promotion, qui recopie dans leur overlay un tag existant. Aucune image n'est reconstruite : c'est la même, déjà scannée. Conséquence directe : leur tag ne bouge pas pendant des jours ou des semaines.
Une seule image par pipeline, deux étiquettes. Le tag candidate- sert le temps du scan. Le tag deployed- est ensuite posé sur le même digest : c'est celui qui entre dans Git et que le cluster ira chercher. Staging et prod, eux, n'avancent que sur décision humaine.

Pourquoi deux tags plutôt qu'un

Parce que la règle de ménage traite les deux différemment. Une image candidate- est un artefact intermédiaire : elle expire au bout de 3 jours. Une image deployed- est censée être servie par le cluster : elle est protégée de cette règle d'âge.

Cette protection avait été conçue pour un cas précis (#104) : une pause de la CI de plus de trois jours ne devait pas effacer l'image en service. Elle fonctionne parfaitement contre l'âge. Le problème est venu d'ailleurs.

Le mécanisme de la panne

La règle qui protège les images deployed- garde les N plus récentes. Toutes les images deployed- partagent donc la même étagère, quel que soit l'environnement qui les sert.

Or les cadences sont opposées, et c'est tout le problème :

Environnement Fait avancer son tag Rythme réel
dev le write-back, automatiquement à chaque merge sur develop
staging une MR de promotion quelques fois par sprint
prod une MR de promotion rarement, parfois jamais en 15 jours

Chaque image poussée par dev prend une place et fait glisser les autres vers la sortie. L'image de prod, elle, ne bouge pas — donc elle recule, place par place, jusqu'à tomber.

UNE SEULE FENÊTRE DE RÉTENTION, PARTAGÉE PAR LES TROIS ENVIRONNEMENTS · · · dev dev dev dev dev stg dev dev prod supprimée chaque merge insère ici L'image de prod n'a rien fait de mal : elle est simplement la plus ancienne, parce qu'elle est stable. Plus prod reste en place — ce qui est le comportement souhaité — plus elle approche de la sortie. Le 2026-08-11 : 11 poussées dans la seule journée.
La stabilité de prod est ce qui la condamne. Une règle par compteur classe les images par date de poussée. L'environnement le plus stable est donc toujours le premier candidat à la suppression — l'exact inverse de ce qu'on veut protéger.

Le détail qui rend la panne invisible

ArgoCD ne détecte rien. De son point de vue, le cluster est conforme à ce qui est écrit dans Git : l'overlay demande deployed-f289…, il exige deployed-f289…. L'écart n'est pas entre Git et le cluster, il est entre Git et le registre, et personne ne regardait là.

L'application reste affichée Synced / Progressing, indéfiniment, sans alerte.

Ce qui s'est réellement passé

Deux incidents, le même diagnostic, quinze jours d'écart.

Date Événement
2026-07-25 Le multi-environnement est livré. dev, staging et prod ont chacun leur tag. La règle de rétention, elle, n'est pas revue : elle datait du temps où l'image servie était toujours la dernière produite.
2026-07-27 INC-062 — le job de scan quotidien échoue sur MANIFEST_UNKNOWN. La veille au soir, cinq merges avaient suffi : le compteur valait 5. Cluster éteint, donc aucune panne visible, seulement un job rouge.
2026-07-28 Premier correctif : le compteur passe de 5 à 30, vérifié côté AWS, issue fermée. La leçon de l'incident écrivait pourtant : « élargir la fenêtre rend l'incident improbable sans le rendre impossible ».
2026-08-12 INC-065 — même panne, cluster allumé. Les pods de prod restent en Init:ImagePullBackOff. Les 30 places étaient pleines : quinze jours de merges.
2026-08-12 Contournement : prod repointée sur l'image de staging. Puis sursis assumé, compteur à 100, avec une date de revue au 2026-09-30 écrite dans la variable Terraform.
2026-08-15 Correctif structurel : une étagère par tag qui stagne.

Un piège de mesure qui a failli innocenter la vraie cause

Une première requête d'inventaire lisait imageTags[0], le premier tag de chaque image. Elle annonçait 14 images sur 30 places — de quoi écarter la règle de compteur et partir chercher ailleurs.

Or chaque image porte deux tags sur le même digest, et l'ordre du tableau n'est pas garanti. En lisant l'ensemble des tags, le compte réel était de 30 sur 30.

La correction : une étagère pour ce qui stagne

Le levier tient à une règle du registre, et il faut la connaître pour comprendre le reste : une image est attribuée à la première règle dont elle correspond au filtre. Les règles sont numérotées, et le plus petit numéro gagne.

Si l'image servie par prod porte une étiquette que seule prod utilise, et qu'une règle prioritaire réclame cette étiquette, alors cette image ne concourt plus jamais avec les images de dev.

AVANT · UNE ÉTAGÈRE APRÈS · UNE ÉTAGÈRE PAR TAG QUI STAGNE prod dev dev dev dev dev prod tombe : elle vieillit pendant que dev pousse tous : deployed-<sha> une seule étiquette, donc une seule file d'attente Le seul réglage disponible est la taille de l'étagère. 5 places → tombé en 1 soirée 30 places → tombé en 15 jours 100 places → tomberait plus tard La date change, la propriété reste. inuse-prod-<sha> règle nº 1 · 3 places inuse-staging-<sha> règle nº 2 · 3 places deployed-<sha> dev · 100 places les merges n'alimentent que celle-ci Prod et staging ont une étagère à eux, remplie par eux seuls. dev reste sur l'étagère commune : il n'en est jamais sorti. dev ne peut plus atteindre l'étagère de prod, quel que soit le nombre de merges.
Ce n'est pas la taille qui change, c'est le critère. À gauche, une seule file où la cadence la plus rapide décide du sort de la plus lente. À droite, des files indépendantes : la propriété qui produisait la panne n'existe plus, au lieu que son échéance soit repoussée.

L'ordre des règles porte du sens

Prod est la règle nº 1 et staging la nº 2, et ce n'est pas décoratif. Après une promotion, staging et prod référencent souvent la même image — la promotion recopie le tag de staging vers prod. Cette image porte alors inuse-prod- et inuse-staging-.

Le registre l'attribue à la première règle qui la réclame, donc à celle de prod : c'est la fenêtre la plus stable des deux qui la protège.

Les cinq règles, dans l'ordre

Filtre Critère Rôle
1 inuse-prod- 3 dernières protège l'image servie par prod
2 inuse-staging- 3 dernières idem pour staging
3 deployed- 100 dernières protège les images de dev, et sert de filet
4 candidate- 3 jours ménage des artefacts intermédiaires
5 tout le reste 5 dernières filet de sécurité

La dernière règle doit obligatoirement l'être : le registre exige que le filtre « tout le reste » ait la priorité la plus basse.

Pourquoi dev n'a pas d'étagère

Parce qu'il n'en a jamais eu besoin, et c'est la clé de toute l'affaire. Son tag est bumpé à chaque merge, donc son image est toujours la plus récente : une règle qui classe par date de poussée ne peut pas l'atteindre. C'est exactement pour ça que prod tombait et que dev ne tombait pas.

Une étagère se donne à ce qui stagne. La section « le correctif a eu son propre défaut » raconte ce qu'a coûté d'avoir oublié cette phrase pendant quelques heures.

Qui pose ces étiquettes, et quand

Un job tag-inuse-images tourne à chaque pipeline sur develop, après les deux jobs de promotion. Pour prod et staging, et pour chacun des deux dépôts, il pose inuse-<env>-<sha> sur le digest que cet environnement sert réellement.

Il ne construit rien et ne télécharge rien : il lit le manifeste d'une image existante et lui ajoute un tag, exactement comme le fait déjà l'étape de promotion. C'est pourquoi il ne demande aucune permission AWS supplémentaire.

Il lit le tag dans l'overlay de l'environnement, et c'est fiable précisément parce qu'il s'agit de prod et staging : eux n'avancent que par MR de promotion, donc l'overlay du commit testé dit déjà la vérité au moment où le job tourne.

Le job est aussi le contrôle qui manquait

S'il ne trouve pas en registre l'image qu'un overlay réclame, il échoue. C'est exactement ce que les deux incidents avaient réclamé : les deux fois, la panne a été découverte par un symptôme indirect — un job de scan rouge, puis un ImagePullBackOff au démarrage du cluster — faute de quoi que ce soit comparant les overlays au registre.

État relevé côté AWS le 2026-08-15

Deux terraform apply ont été nécessaires dans la journée : le premier a posé six règles, le second en a retiré une après la découverte racontée plus bas. Voici ce que porte réellement chacun des deux registres, lu depuis AWS et non depuis le dépôt :

1 tagged ['inuse-prod-']     imageCountMoreThan 3
2 tagged ['inuse-staging-']  imageCountMoreThan 3
3 tagged ['deployed-']       imageCountMoreThan 100
4 tagged ['candidate-']      sinceImagePushed 3
5 any                        imageCountMoreThan 5

Le correctif a eu son propre défaut

Livré le matin avec trois étagères, corrigé l'après-midi. C'est l'inventaire du registre qui l'a révélé :

images au total : 44
  candidate        44
  deployed         36
  inuse-dev         1
  inuse-prod        1
  inuse-staging     1

Rien d'anormal en apparence. Ce qui alerte, c'est de rapprocher ce inuse-dev 1 de la règle qui le gouvernait : 3 places.

Une règle inuse- est plus prioritaire que la règle deployed-. Or le registre attribue chaque image à la première règle qui la réclame. Une étagère inuse- ne s'ajoute donc pas à la protection existante : elle la remplace pour les images qu'elle réclame.

Sans étagère dev Avec étagère dev de 3 places
places pour une image de dev 100 3
durée de vie à 11 poussées/jour ~9 jours ~2 heures

Le danger n'était pas dev, c'était la promotion. Promouvoir vers staging recopie le tag d'une image de dev. Passé trois merges, cette image aurait été supprimée : la MR aurait pointé sur une image absente. Soit exactement la panne d'INC-062, rejouée dans l'autre sens — et cette fois causée par le correctif censé la fermer.

Ce que ce détour apprend

Une protection ne se distribue pas par symétrie, elle va à ce qui porte le risque. Trois environnements ne veulent pas dire trois règles : la règle appartenait aux tags qui stagnent, et dev n'en est pas un.

Sur la méthode : le défaut n'a été vu ni à la relecture, ni au plan Terraform, ni au job vert. Il est apparu en regardant l'état réel du registre après coup, puis en confrontant un chiffre d'inventaire à la règle qui le gouverne. Aucune de ces deux données n'était suspecte séparément.

Appliquer un changement de rétention

Le stack terraform/persistent/ porte les deux dépôts ECR et n'est appliqué par aucune CI. Merger une MR ne modifie donc pas la règle de ménage côté AWS.

terraform -chdir=terraform/persistent apply

Quatre variables sensibles n'ont pas de valeur par défaut et alimentent des secrets AWS Secrets Manager. Les relire depuis Secrets Manager avant d'appliquer donne un plan qui ne les touche pas — sans quoi un apply peut écraser le mot de passe de la base. La procédure complète est dans le runbook.

Deux surprises à la lecture du plan

« must be replaced » n'est pas alarmant ici. Le mot destroy porte sur la règle de ménage, jamais sur le dépôt ni sur les images — aws_ecr_repository n'apparaît pas dans le plan. Le provider AWS traite l'attribut policy comme non modifiable en place, donc toute retouche remplace la ressource, même un simple changement de chiffre.

Le diff se lit par position, pas par identité. Il semble dire que la règle deployed- « devient » inuse-prod-. En réalité des règles sont insérées en tête et tout glisse de quelques rangs : les anciennes réapparaissent plus bas, intactes.

Le contrôle qui tranche est toujours le même, après l'apply : lire la policy depuis AWS, pas depuis le dépôt.

aws ecr get-lifecycle-policy --repository-name fastapi-eks/fastapi \
  --query lifecyclePolicyText --output text | python3 -m json.tool

Ce qu'il faut retenir

Un correctif qui déplace un seuil sans changer la propriété qui produit la panne n'en règle que la date. Le premier correctif avait été validé sur le bon critère — la rétention est bien passée à 30, vérifié côté AWS — et fermé sur cette preuve. Mais le critère qui comptait était ailleurs : rien ne distinguait toujours l'image en service des autres.

Quand la leçon d'un incident écrit que le correctif retenu est un palliatif, c'est une prédiction datée, pas une précaution de style. Elle mérite une issue de suivi ouverte au moment même où l'on ferme l'incident.

Ce qu'aucun outil ne surveille finit par mordre. ArgoCD compare Git au cluster. Le registre, lui, n'était comparé à rien. La correction ne se contente pas de protéger les images : elle installe le contrôle qui manquait, à l'endroit exact de l'angle mort.

Un correctif mérite la même défiance que ce qu'il corrige. Le geste qui a payé tient en une phrase : confronter un chiffre observé à la règle qui le gouverne.


Sources : INC-062 et INC-065 (docs/incidents/sprints/sprint6.md), issue #150, MR !297, !300 et !301. Code : terraform/modules/ecr/, job tag-inuse-images dans .gitlab-ci.yml. Voir aussi Promotion multi-env.