Lección 5 — Exploiter

Détecter la dérive

7 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Le mardi, la finance change à la main le label cost-center du bucket d'archive, de lab à finance, pour rattacher son coût à son budget. Le jeudi, un déploiement sans rapport remet lab : l'apply montrait bien ~ cost-center = "finance" -> "lab", noyé dans 40 lignes. Le rapport de coûts du mois est faux et personne ne sait pourquoi. La dérive (drift), c'est l'écart entre l'infrastructure réelle et le code. Elle n'est pas toujours une faute ; elle doit toujours être vue, et tranchée, par quelqu'un qui sait qu'il tranche.

Le mécanisme

flowchart TB
  CRON["Chaque matin ouvré (CI)"] --> D["drift.sh envs/dev<br/>plan -detailed-exitcode -lock=false"]
  D -- "0" --> OK["Conforme"]
  D -- "2" --> T["Ticket « dérive » + extrait du plan"]
  D -- "1" --> E["Erreur : le job échoue"]
  T --> Q{"Le changement manuel était-il voulu ?"}
  Q -- "non" --> A["terraform apply : le code a raison"]
  Q -- "oui" --> C["PR : le code adopte la valeur"]

terraform plan -detailed-exitcode rend 0 (aucun changement), 2 (changements à faire) ou 1 (erreur). Le script distingue les trois : confondre une erreur d'authentification avec « pas de dérive » est le défaut classique d'une détection qui ne détecte plus rien depuis des semaines.

Deux choix dans scripts/drift.sh :

  • Plan normal, pas -refresh-only. Le mode -refresh-only signale tout ce qui a changé côté API, y compris des normalisations sans effet (un null devenu {}), d'où de fausses alertes juste après un apply. Le plan normal ne signale que les écarts qui provoqueraient une action.
  • -lock=false. La détection est en lecture seule : elle ne doit ni bloquer un apply en cours ni être bloquée par lui.

La mise en œuvre

files/scripts/drift.sh (extrait)
terraform -chdir="$dir" plan -detailed-exitcode -input=false -lock=false -no-color > "$out" 2>&1
rc=$?
case $rc in
  0) echo "drift: aucune dérive ($dir)" ;;
  2) echo "drift: DÉRIVE détectée ($dir)"
     grep -E '^\s+[~+-] |^  # |^Plan:' "$out" | grep -vE '# \(.*hidden\)' ;;
  *) echo "drift: erreur (code $rc)"; tail -20 "$out" ;;
esac
exit $rc

make e2e prouve la chaîne sur une infrastructure réelle : il crée le socle dev (sans service Cloud Run : aucune image à construire), vérifie l'absence de dérive, change un label à la main avec gcloud, attend un code 2, réconcilie par apply, puis attend un code 0.

make e2e PROJECT=<votre-projet> TICKETING_TOKEN=<jeton>
make cloud-down PROJECT=<votre-projet> TICKETING_TOKEN=<jeton>

En CI, le job derive appelle le script directement et pas par make : make transforme tout code non nul en 2, ce qui confondrait erreur et dérive.

Résultat obtenu (2026-10-16)

Le premier make e2e de ce jour a échoué, et c'était une vraie trouvaille :

Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
drift: DÉRIVE détectée (infra/envs/dev)
  ~ update in-place
  # module.platform.module.registry.google_artifact_registry_repository.this will be updated in-place
  ~ resource "google_artifact_registry_repository" "this" {
      + docker_config {
          + immutable_tags = false
Plan: 0 to add, 1 to change, 0 to destroy.
make: *** [Makefile:93 : e2e] Erreur 2

Une dérive juste après un apply, sans aucune intervention : le module déclarait docker_config { immutable_tags = false }, et l'API ne renvoie pas ce bloc quand la valeur est fausse. Chaque détection aurait sonné tous les matins ; au bout d'une semaine, plus personne ne lit les tickets. Correction dans le module artifact-registry des deux projets : le bloc n'est posé que lorsqu'il vaut true (dynamic "docker_config"). Puis un cycle complet, depuis un projet vide :

$ make e2e    # 53 s
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
state_bucket = "lab-ttg-tfstate-2b66a5"
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
drift: aucune dérive (infra/envs/dev)
Pas de dérive après apply.
Modification manuelle : gcloud storage buckets update gs://lab-ttg-dev-archive-592e47 --update-labels=cost-center=finance
drift: DÉRIVE détectée (infra/envs/dev)
  ~ update in-place
  # module.platform.module.buckets.google_storage_bucket.this["archive"] will be updated in-place
  ~ resource "google_storage_bucket" "this" {
      ~ effective_labels            = {
          ~ "cost-center"                = "finance" -> "lab"
      ~ terraform_labels            = {
          ~ "cost-center"                = "finance" -> "lab"
Plan: 0 to add, 1 to change, 0 to destroy.
Dérive détectée (code 2), comme attendu.
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
drift: aucune dérive (infra/envs/dev)
E2E OK : dérive corrigée.

$ make cloud-down
Destroy complete! Resources: 13 destroyed.
Destroy complete! Resources: 2 destroyed.
Aucune ressource terraform-teste-et-gouverne restante.

Un cloud-down antérieur du même jour avait affiché RESTE Comptes de service : lab-ttg-dev-run@… juste après « Destroy complete! » : la liste IAM est à cohérence différée. Le compte était bien supprimé (le cycle suivant l'a recréé sous le même nom). cloud-down revérifie désormais trois fois, à 15 s d'intervalle, avant d'échouer.

Ce qui casse en production

  • La dérive fantôme. Un écart permanent dû à une normalisation de l'API (ci-dessus). Symptôme : une dérive sur le même attribut juste après chaque apply. Parade : le code décrit ce que l'API renvoie ; lifecycle { ignore_changes = [...] } seulement pour un attribut réellement géré ailleurs (par exemple un label posé par l'outil de FinOps), avec un commentaire.
  • La détection aveugle. Le compte de la CI perd un droit de lecture : le plan échoue (code 1), un script naïf (if plan; then OK) conclut « pas de changement » des semaines durant. Parade : trois codes distincts, et le job échoue sur 1.
  • La réconciliation à l'aveugle. Un apply automatique sur dérive écrase une correction d'urgence faite la nuit en astreinte. Parade : jamais d'apply automatique sur dérive ; un ticket, une décision humaine.
  • La dérive hors du code. Une ressource créée à la main n'apparaît dans aucun plan : ce n'est pas de la dérive pour Terraform. Parade : l'inventaire par label et préfixe (make cloud-status), ou l'inventaire des ressources du projet comparé à l'état.

La règle de décision

Détection quotidienne sur chaque environnement partagé, en lecture seule, avec trois codes de sortie. Toute dérive se conclut par l'un de deux gestes : apply (le code a raison) ou PR (le changement manuel avait raison). Une dérive qui revient après chaque apply est un défaut du code, à corriger avant de brancher l'alerte.

Pratique

Le job de dérive est vert tous les matins depuis trois semaines. Un collègue remarque que le compte de service de la CI a été supprimé il y a un mois. Comment est-ce possible, et que corrigez-vous ?

La dérive signale chaque matin « ~ labels.owner = "équipe-data" -> null » sur un bucket. L'outil FinOps de l'entreprise pose ce label sur tous les buckets. Que faites-vous ?

Exercice. Étendez la détection à la prod dans le job derive du pipeline, sans qu'une dérive de dev masque une erreur de prod.

Voir la solution
pire=0
for e in dev prod; do
  scripts/drift.sh infra/envs/$e >> drift.txt 2>&1
  rc=$?
  [ $rc -eq 1 ] && pire=1                     # une erreur l'emporte sur tout
  [ $rc -eq 2 ] && [ $pire -ne 1 ] && pire=2
done
echo "rc=$pire" >> "$GITHUB_OUTPUT"
cat drift.txt
[ "$pire" -ne 1 ]

make init initialise déjà dev et prod. La prod a son propre compte de lecture en entreprise : un second google-github-actions/auth avant la boucle, ou deux jobs. L'ordre de priorité (erreur > dérive > conforme) garantit qu'une dérive de dev ne cache pas une prod injoignable.

  • Je détecte la dérive avec plan -detailed-exitcode et je distingue erreur et dérive.
  • Je provoque, détecte et réconcilie une dérive sur une vraie ressource.
  • Je reconnais une dérive fantôme et je la corrige dans le code.
  • Je tranche une dérive par apply ou par PR, jamais automatiquement.