Lección 6 — Déployer

Planifier dev et prod

7 min de lectura

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

Le plan affichait « 1 to change ». L'ingénieur a validé sans dérouler le détail : c'était un -/+ sur le bucket d'archive, suite au renommage d'une clé de map. Un « change » qui est en réalité une destruction suivie d'une recréation, sur un bucket de journaux d'audit. Le plan est le seul moment où l'on voit ce que Terraform va faire avant qu'il le fasse ; cette leçon apprend à le lire, puis compare les plans réels de dev et de prod produits par le même code.

Le mécanisme

flowchart LR
  CODE["code + tfvars"] --> PLAN["terraform plan"]
  ETAT[("état distant")] --> PLAN
  API["API Google<br/>(refresh)"] --> PLAN
  PLAN -- "-out=dev.tfplan" --> FICHIER["plan figé"]
  FICHIER --> REVUE["revue humaine<br/>+ politiques (projet suivant)"]
  FICHIER --> APPLY["terraform apply dev.tfplan<br/>exactement ces actions"]

Un plan compare trois choses : le code, l'état, et la réalité lue par le provider (le refresh). Les symboles à connaître :

SymboleSensÀ vérifier
+créationnoms, labels, région
~modification sur placel'attribut qui change, et pourquoi
-/+destruction puis recréation# forces replacement : quelle donnée est perdue
-destructiontoujours justifiée dans la description de la PR
(known after apply)valeur calculée par l'APInormal pour un id, suspect pour un nom
(write-only attribute)valeur envoyée, jamais stockéeles secrets, leçon 3

Un plan non sauvegardé n'est qu'un aperçu : entre le plan et l'apply, le code ou la réalité peuvent changer. En CI, on écrit le plan dans un fichier (-out), on le fait relire, et apply exécute ce fichier et rien d'autre.

La mise en œuvre

make state-up    # bucket d'état (leçon 2)
make plan        # dev : init sur le bucket, puis plan
make plan-prod   # prod : même code, autre tfvars, autre préfixe d'état

La prod de Logi-Express vit dans un projet fictif, logi-ia-prod. Le plan fonctionne quand même : aucune source de données (data) ne lit le projet, le provider n'a besoin que des identifiants pour lire l'état et rafraîchir des ressources qui n'existent pas encore. C'est un bon test de la portabilité du code : rien ne dépend du projet du lab.

L'image de prod est un digest factice passé par la ligne de commande (-var image=…@sha256:000…) : sans image, le service n'apparaîtrait pas dans le plan (leçon 5).

Résultat obtenu (2026-10-16)

Plan de dev, sur un projet vide (identifiant masqué) :

$ make plan
  # module.platform.module.buckets.google_storage_bucket.this["archive"] will be created
  # module.platform.module.identities.google_project_iam_member.this["run/roles/aiplatform.user"] will be created
  # module.platform.module.identities.google_project_iam_member.this["run/roles/logging.logWriter"] will be created
  # module.platform.module.identities.google_project_iam_member.this["run/roles/monitoring.metricWriter"] will be created
  # module.platform.module.identities.google_service_account.this["run"] will be created
  # module.platform.module.logging.google_logging_metric.errors will be created
  # module.platform.module.logging.google_logging_project_sink.archive will be created
  # module.platform.module.logging.google_storage_bucket_iam_member.sink_writer will be created
  # module.platform.module.registry.google_artifact_registry_repository.this will be created
  # module.platform.module.secrets.google_secret_manager_secret.this["ticketing-token"] will be created
  # module.platform.module.secrets.google_secret_manager_secret_iam_member.accessors["ticketing-token/run"] will be created
  # module.platform.module.secrets.google_secret_manager_secret_version.this["ticketing-token"] will be created
  # (+ random_id.suffix)
Plan: 13 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + archive_bucket = (known after apply)
  + registry_url   = "europe-west1-docker.pkg.dev/<projet>/lab-pia-dev-images"
  + service_name   = "lab-pia-dev-triage"

Pas de service en dev à ce stade : pas d'image. Plan de prod : Plan: 18 to add, soit les 13 mêmes ressources, plus le réseau (3), le service et son invoker. Les écarts relevés dans les deux plans réels :

Attribut (plan réel)devprod
bucket d'archive force_destroy / deletion_policytrue / DELETEfalse / PREVENT
règle de cycle de vie age7365
registre immutable_tags(bloc absent)true
service ingressINGRESS_TRAFFIC_ALL (au 2e apply)INGRESS_TRAFFIC_INTERNAL_ONLY
service min_instance_count / max_instance_count0 / 21 / 10
service deletion_protectionfalsetrue
label envdevprod

Chaque ligne de ce tableau vient d'une seule source : local.protect ou une valeur de terraform.tfvars. Aucune n'est un if dans une racine.

Ce qui casse en production

  • Le -/+ caché dans un « change ». Renommer une clé de for_each (archive → archives) ou changer un attribut immuable (location d'un bucket) détruit la ressource. Diagnostic : chercher forces replacement dans le plan. Parade : un bloc moved { from = …["archive"] to = …["archives"] } pour un renommage ; pour un attribut immuable, une migration explicite.
  • Plan valide, apply refusé. Le plan ne vérifie pas les droits d'écriture ni les quotas : Error 403: Permission 'iam.serviceAccounts.create' denied n'apparaît qu'à l'apply. Parade : planifier avec le compte de déploiement réel, pas avec un compte personnel plus large.
  • Plan non vide juste après un apply. Une différence que l'API normalise (bloc vide, casse d'une région, ordre d'une liste) revient à chaque plan. Vu sur ce lab : le bloc docker_config du registre (leçon 3). Parade : corriger le code pour qu'il décrive ce que l'API renvoie ; un ignore_changes seulement si la valeur est réellement gérée ailleurs.
  • (known after apply) sur un nom. Le nom d'un bucket dépend d'un random_id : il n'est connu qu'à l'apply, et tout ce qui en dépend aussi (le filtre du sink, par exemple). Acceptable pour un suffixe ; à éviter pour un nom qu'une autre équipe doit connaître à l'avance.

La règle de décision

Plan sauvegardé (-out) pour tout ce qui touche un environnement partagé ; apply du fichier relu, jamais un apply qui recalcule. Lisez d'abord le résumé, puis chaque - et -/+, puis les ~ sur les ressources IAM. Comparez dev et prod en lisant les tfvars, pas en lisant le code : si un écart n'apparaît dans aucun tfvars, c'est qu'il est caché dans une condition.

Pratique

Le plan d'une PR affiche « Plan: 1 to add, 0 to change, 1 to destroy » sur google_storage_bucket.this["archive"]. La PR renomme seulement la clé archive en audit-logs. Que demandez-vous ?

Juste après un apply réussi, le plan suivant annonce encore « 1 to change » sur le dépôt Artifact Registry, toujours le même attribut. Quelle est la cause la plus probable ?

Exercice. Produisez un plan de prod sauvegardé, puis affichez uniquement les actions de destruction ou de remplacement qu'il contient, en une commande.

Voir la solution
cd files
terraform -chdir=infra/envs/prod plan -input=false -out=$PWD/prod.tfplan \
  -var image=europe-west1-docker.pkg.dev/logi-ia-prod/lab-pia-prod-images/triage@sha256:$(printf '0%.0s' {1..64})
terraform -chdir=infra/envs/prod show -json $PWD/prod.tfplan \
  | jq -r '.resource_changes[] | select(.change.actions | index("delete")) | "\(.change.actions|join("/")) \(.address)"'

Le JSON du plan donne actions : ["delete"], ["delete","create"] ou ["create","delete"] pour un remplacement. Sur un plan initial, la sortie est vide. C'est la base des politiques du projet suivant. make clean supprime les *.tfplan.

  • Je lis un plan en commençant par les destructions et les remplacements.
  • Je sauvegarde un plan et j'applique ce fichier, pas un nouveau calcul.
  • J'explique chaque écart dev/prod par une valeur de tfvars ou par local.protect.
  • Je reconnais une différence permanente due à une normalisation de l'API.
Siguiente