ADR 031 — Le garde-fou de teardown prévient au lieu de détruire (2026-08-31)¶
Statut¶
Accepté, écrit le 2026-08-31. Issue #174, Sprint 7 (Remédiation & Fiabilité). Question tranchée le 2026-08-29, document rédigé le 2026-08-31.
Le numéro 031 était réservé depuis l'ADR 032, écrite avant celle-ci parce que
184 est passée en premier.¶
Cette ADR porte la décision, pas le mécanisme. Le module Terraform et son premier déclenchement réel sont livrés séparément : #174 ne se ferme pas sur ce fichier (décision 4).
Contexte¶
Ce que le garde-fou est censé faire¶
Le schedule GitLab infra_stop cloud (id 4253819, cron 0 21 * * *
Europe/Paris, input action=stop) doit détruire l'infrastructure si une session
se termine sans teardown. Le cluster oublié coûte le plan de contrôle EKS, les
nœuds, le NAT Gateway et RDS, toute la nuit.
C'est un filet, pas la procédure normale : le teardown de fin de session est
un geste explicite, joué depuis .gitlab-ci-infra.yml.
Ce qu'il fait réellement : rien, depuis le 2026-08-06¶
Mesuré le 2026-08-31 par l'API GitLab, sur les pipelines de source schedule :
- 24 nuits sur 25 depuis le 06/08 sortent en
failedavec zéro job, et aucune erreur YAML ; - l'unique exception est le 16/08 (
2764181089,success, 2 jobs, 14 s et 17 s) — un test joué en basculant volontairementci_config_pathsur la config infra, pas un fonctionnement spontané ; - revérifié job par job sur les deux dernières nuits,
2803659043(30/08) et2802305127(29/08) :jobs: 0dans les deux cas.
Cause immédiate. ci_config_path vaut .gitlab-ci.yml (vérifié le 31/08).
Sous cette configuration, le bloc workflow: accepte bien
$CI_PIPELINE_SOURCE == "schedule", mais aucun job ne peut correspondre :
infra-stopn'existe que dans.gitlab-ci-infra.yml, et l'inputaction=stopporté par le schedule n'est déclaré que là ;- le seul job schedule-only de la config app,
registry-scan, exige$VULN_SCAN == "true", positionné par le schedule de 8 h et par lui seul.
GitLab crée donc un pipeline vide, et un pipeline vide est failed, pas
skipped. D'où un rouge nocturne sans message d'erreur, qui ressemble à un
incident et n'en est pas un.
La cause racine n'est pas la règle CI¶
Pour que le teardown de 21 h parte, trois conditions doivent être vraies en même temps :
ci_config_pathpointe sur.gitlab-ci-infra.yml;- le runner
.112est allumé — tous les jobs portent le tagubuntu, ESXi éteint laisse le pipelinependingindéfiniment (#159) ; - le schedule est actif.
Ces trois conditions sont du même type que l'oubli contre lequel le garde-fou protège. Qui oublie de détruire son infrastructure a probablement aussi laissé la configuration sur app, ou éteint l'hyperviseur en partant.
C'est le schéma déjà nommé dans la leçon de SEC-006 : une règle qui dépend de la mémoire d'un humain au bon moment est un vœu, pas un contrôle.
La contrainte structurelle¶
Un seul ci_config_path est actif à la fois. Le scan CVE quotidien (8 h,
config app) et le teardown du soir (config infra) vivent dans deux fichiers
différents : ils s'excluent par construction. Laisser la configuration sur
infra pour réparer le garde-fou couperait le scan CVE, silencieusement et de la
même façon — zéro job, failed, pas d'erreur.
Aucun réglage à l'intérieur d'un des deux fichiers ne réconcilie les deux.
L'alerting existant ne peut pas couvrir ce cas¶
Alertmanager (ADR 016) tourne dans le cluster. Un contrôle dont la seule raison d'être est de dire « un cluster est resté debout » ne peut pas être hébergé par ce cluster : quand il n'y a rien à signaler il tourne, et quand il faudrait signaler quelque chose il tourne aussi, mais personne n'a de raison de le regarder. Même famille de dépendance que #154, où Prometheus perd son historique avec le cluster qui le porte.
Le garde-fou doit vivre hors de ce qu'il surveille.
Décision¶
1. Le garde-fou prévient, il ne détruit plus¶
C'est la question posée par #174, et voici la réponse.
Détruire proprement exige l'état Terraform, donc le pipeline, donc le runner,
donc les trois préconditions ci-dessus. Prévenir n'exige rien de tout ça : un
appel eks:ListClusters répond, depuis n'importe où.
Un garde-fou qui prévient toujours vaut mieux qu'un garde-fou qui détruit parfois.
Conséquence directe et assumée : le teardown redevient explicitement un geste manuel de fin de session. Le mécanisme automatique ne prétend plus le remplacer, il rattrape l'oubli. Ce n'est pas un recul : depuis le 06/08, c'est déjà le geste manuel qui a tout fait, y compris après la journée live du 23/08.
2. Le détecteur est EventBridge → Lambda → SNS, dans la stack persistent¶
- EventBridge Scheduler déclenche à 23 h
Europe/Paris, soit deux heures après l'heure nominale du teardown. Cette marge laisse passer une session qui finit tard sans crier au loup. - Une Lambda appelle
eks:ListClusters. Liste non vide → publication sur SNS. - Un sujet SNS notifie par courriel.
Trois propriétés qui motivent ce montage :
- il ne dépend ni du runner
.112(#159), ni deci_config_path, ni de GitLab. Aucune des trois conditions défaillantes n'y a de prise ; - il vit dans la stack
persistent, comme CloudTrail (#187) et le garde-fou de coût (#195). Un garde-fou qui disparaît au teardown ne garde rien — et celui-ci doit précisément fonctionner les soirs où le teardown n'a pas eu lieu ; - il réutilise
var.cost_alert_email, l'adresse déjà servie au budget de support étendu. Un cluster oublié est un incident de coût : c'est la même question et la même boîte aux lettres. #184 vient de faire passer les variables CI de 16 à 15, en ajouter une pour la même adresse serait une régression.
3. Le détecteur répond à une seule question, et on l'écrit¶
La question est : « existe-t-il un cluster EKS vivant à 23 h ? »
Il ne répond pas « le compte AWS est propre ». Ne sont pas vus :
- un NLB orphelin — il vit hors Terraform (ADR 017), c'est le piège qui a motivé le contrôle manuel en huit points ;
- une instance RDS ou un NAT Gateway survivant à un
destroypartiel ; - des ENIs orphelins (INC-011).
La vérification de fin de session reste le contrôle manuel en huit points, joué
sous iamadmin. Le détecteur ne la remplace pas.
C'est assumé, pour une raison précise : le cluster est le poste de dépense dominant, et c'est la seule ressource dont l'existence à 23 h signe à coup sûr « la session n'a pas été fermée ». Un NLB seul peut être un résidu à nettoyer ; un cluster debout est un oubli. Élargir le détecteur est une issue séparée, pas un prérequis à celle-ci.
4. Le schedule de 21 h n'est supprimé qu'après un déclenchement réel observé¶
Leçon d'INC-066 : une prescription non vérifiée documente la panne au lieu de l'empêcher.
Tant que le nouveau mécanisme n'a pas notifié une fois pour de vrai — cluster monté, alerte reçue, ou Lambda invoquée à la main sur un cluster vivant — le schedule cassé reste en place.
Son pipeline rouge de chaque soir est aujourd'hui le seul signe visible que le garde-fou ne fait rien. On ne supprime pas le symptôme avant d'avoir le remplaçant, et on ne remplace pas un contrôle non exercé par un autre contrôle non exercé.
Conséquences¶
- #174 n'est pas fermée par cette ADR. Restent le module Terraform, l'apply
sur la stack
persistent, l'exercice du mécanisme, puis la suppression du schedule4253819. La case « décision tranchée par écrit » de l'issue est en revanche remplie par ce fichier. - Le rouge nocturne persiste jusqu'à cette suppression. Il est laissé exprès. Le voir dans l'historique des pipelines n'est pas une anomalie à diagnostiquer, c'est l'état documenté ici.
- Une alerte de plus dans un projet où une alarme sonne déjà sans réponse. Le scan de registre de 8 h échoue chaque matin et personne n'y répond (INC-072). La différence tient à la fréquence attendue : cette alerte-ci ne doit jamais partir. Un signal annuel est lu, un signal quotidien devient du décor. C'est une propriété à entretenir : si elle se met à partir toutes les nuits, elle sera morte comme les autres, et le vrai correctif sera alors le geste de teardown, pas le seuil.
- Le détecteur coûte quelques centimes par mois : une Lambda invoquée une fois par jour et un sujet SNS restent dans les paliers gratuits ou juste au-dessus. Le supplément de support étendu mesuré en mai et juin était de 18 $ par mois (INC-072) : l'ordre de grandeur n'est pas discutable.
- Un rôle IAM d'exécution de plus dans la stack
persistent, portanteks:ListClusterset la publication SNS. À citer dans #189 (durcissement Terraform) quand elle sera jouée. - L'exclusion des deux
ci_config_pathn'est pas résolue. Elle est contournée : le garde-fou sort de GitLab, donc il ne subit plus l'exclusion. Le scan de 8 h et le teardown continuent de s'exclure pour tout le reste.
Alternatives écartées¶
Désactiver le schedule après un teardown vérifié, le réactiver au montage
(option 2 de l'issue). Coût nul, et une propriété séduisante : un schedule actif
en fin de session devient lui-même l'alarme, son état cessant d'être
décoratif. Écartée comme garde-fou parce qu'elle dépend du runner et du bon
geste au bon moment — exactement la mémoire humaine qu'il s'agit de remplacer, et
elle ne dit rien à personne quand elle échoue. Conservée comme geste de
fermeture de session, à côté de la bascule de ci_config_path, où elle a sa
valeur.
Fusionner les deux configurations CI en une seule pilotée par rules
(option 3). C'est la seule qui traite la cause racine, et le mécanisme existe
déjà : .gitlab-ci.yml fait un include: - local: pour la doc. Écartée pour le
risque : les deux fichiers déclarent chacun workflow:, stages et default, et
.gitlab-ci-infra.yml a ses propres spec:inputs. Une erreur casse tout le
pipeline, ou pire, fait partir un teardown au mauvais moment. Reste
souhaitable, à rouvrir avec #179 (découpage du .gitlab-ci.yml), pas dans une
issue dont le livrable est un filet de sécurité.
Supprimer le schedule et assumer un teardown purement manuel (option 4). Plus honnête que l'état actuel, où le garde-fou existe sur le papier et ne protège rien. Écartée parce qu'elle laisse zéro filet. La décision 1 en retient la moitié franche — le teardown est manuel — et ajoute le filet indépendant que l'option 4 supprimait.
Une simple alerte de budget AWS (variante fruste de l'option 1). Écartée sur la mesure de #195 : un budget dit combien on a dépensé, avec plusieurs heures de retard sur la consommation, jamais qu'un cluster tourne cette nuit. INC-072 a montré que cinq mois de dépassement peuvent passer quatre alarmes de coût sans qu'aucune ne pose la bonne question.
Notifier Slack plutôt qu'un courriel. Le webhook vit dans Secrets Manager et il faudrait le servir à la Lambda : un secret de plus en circulation pour un gain nul, l'alerte étant rare et sans urgence à la minute. Le canal Slack existant est par ailleurs alimenté par Alertmanager, donc par le cluster, ce qui ramène la dépendance que la décision 2 supprime.
Le correctif à ne PAS faire (proposé puis retiré le 2026-08-16 après
objection) : conditionner la règle schedule du workflow: à
$VULN_SCAN == "true", pour que le schedule de 21 h ne crée aucun pipeline au
lieu d'en créer un vide. Deux lignes, le rouge nocturne disparaît, et le
garde-fou cassé devient silencieux au lieu d'être réparé. C'est le même geste
que masquer un test qui échoue.
Références¶
- Issue #174 (cadrage : détruire ou prévenir), #175 (le
CLAUDE.mddu dépôt décrivait un comportement que le schedule n'a pas), #159 (SPOF du runner.112), #154 (Prometheus meurt avec le cluster), #179 (découpage du.gitlab-ci.yml), #187 (CloudTrail), #189 (durcissement Terraform), #195 (garde-fou de coût) - ADR 016 (Alertmanager vit dans le cluster), ADR 017 (le NLB vit hors Terraform), ADR 032 (réservation du numéro 031)
- INC-011 (ENIs orphelins après
terraform destroy), INC-066 (une prescription non vérifiée documente la panne), INC-072 (quatre alarmes, aucune lue), SEC-006 (une règle qui dépend de la mémoire d'un humain est un vœu) .gitlab-ci-infra.yml(jobinfra-stop,spec:inputs),.gitlab-ci.yml(blocworkflow:, jobregistry-scan)terraform/persistent/main.tf,terraform/modules/cost-guardrail/- Schedule GitLab
infra_stop cloud, id4253819 docs/validation-runbook.mdsection 0 (reprise après montage)