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
- Les mots du sujet
- Ce que le pipeline fabrique, étape par étape
- Le mécanisme de la panne
- Ce qui s'est réellement passé
- La correction : une étagère pour ce qui stagne
- Qui pose ces étiquettes, et quand
- Le correctif a eu son propre défaut
- Appliquer un changement de rétention
- Ce qu'il faut retenir
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.
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.
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.
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¶
| Nº | 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.