Lección 2 — Concevoir

État distant et environnements

7 min de lectura

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

Deux ingénieurs lancent terraform apply sur la prod à dix secondes d'écart, chacun avec son terraform.tfstate local. Le second ne sait pas que le premier vient de créer un compte de service : il tente de le recréer, échoue, et son état local ne correspond plus à rien. L'état Terraform est la mémoire de l'infrastructure ; il doit être unique, partagé, verrouillé pendant une écriture, et récupérable après une erreur.

Le mécanisme

L'état (state) associe chaque adresse Terraform (module.platform.module.buckets.google_storage_bucket.this["archive"]) à l'identifiant réel de la ressource. Le backend dit où il est stocké. Le backend gcs y ajoute deux choses :

  • un verrou : avant toute opération qui écrit l'état, Terraform crée un objet <prefix>/<workspace>.tflock avec une précondition « n'existe pas encore ». Si l'objet existe, GCS répond 412 Precondition Failed et Terraform s'arrête ;
  • le versioning du bucket (à activer vous-même) : chaque écriture de l'état crée une nouvelle génération de l'objet, on peut revenir à la précédente.
sequenceDiagram
  participant A as "Ingénieur A"
  participant B as "Ingénieur B"
  participant G as "Bucket d'état GCS"
  A->>G: créer default.tflock (si absent)
  G-->>A: 200, verrou pris
  B->>G: créer default.tflock (si absent)
  G-->>B: 412 conditionNotMet
  Note over B: Error acquiring the state lock
  A->>G: écrire default.tfstate (nouvelle génération)
  A->>G: supprimer default.tflock

Le bucket d'état ne peut pas stocker son propre état pendant qu'on le crée. On le crée donc dans une racine à part, infra/bootstrap, qui garde son état en local (non versionné, voir .gitignore). Une fois le bucket créé, vous pouvez migrer cet état dedans avec terraform init -migrate-state ; le lab ne le fait pas, pour que make cloud-down puisse supprimer le bucket.

La mise en œuvre

files/infra/bootstrap/main.tf (extrait)
resource "google_storage_bucket" "state" {
  name                        = "${var.name_prefix}-tfstate-${random_id.suffix.hex}"
  location                    = var.region
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = var.force_destroy # lab : true ; en vrai : false

  versioning {
    enabled = true
  }

  lifecycle_rule {
    action { type = "Delete" }
    condition {
      num_newer_versions = 20
      with_state         = "ARCHIVED"
    }
  }
}

Les environnements déclarent un backend partiel : le nom du bucket n'est pas écrit dans le code, il est passé à l'init. Le même code sert ainsi à plusieurs organisations ou projets d'état.

files/infra/envs/dev/main.tf (extrait)
backend "gcs" {
  prefix = "plateforme-ia/dev"
}
make state-up    # crée le bucket
make init        # terraform init -reconfigure -backend-config=bucket=<sortie de bootstrap>

Dossiers d'environnement plutôt que workspaces

Deux façons courantes d'avoir dev et prod :

Workspaces CLI (terraform workspace select prod)Dossiers envs/dev, envs/prod
Backendun seul, mêmes identifiants pour tousun par dossier, identifiants séparés possibles
Différences dev/prodterraform.workspace == "prod" ? … : … dans le codeun terraform.tfvars par dossier
Risque principalapply sur le mauvais workspace, rien ne le montre dans le codeduplication des racines (limitée ici à 40 lignes)
Revue de codeon ne voit pas quel environnement changele chemin du fichier modifié le dit

Ce projet utilise des dossiers, parce que la prod de Logi-Express vit dans un autre projet Google Cloud, avec un autre compte de service de déploiement. La documentation Terraform le dit elle-même : les workspaces CLI partagent un backend et ne conviennent pas aux déploiements qui exigent des identifiants et des contrôles d'accès séparés. Les workspaces restent utiles pour des copies éphémères d'un même environnement (une pile par branche de test, par exemple).

Dans ce lab, dev et prod partagent un bucket (préfixes différents) pour rester dans un seul projet. En entreprise, le bucket d'état de prod vit dans le projet de prod, et seul le compte de service de la CI de prod peut l'écrire.

Résultat obtenu (2026-10-15)

Création réelle du bucket, puis description :

$ make state-up
state_bucket = "lab-pia-tfstate-8f4e94"

$ gcloud storage buckets describe gs://lab-pia-tfstate-8f4e94
labels:
  formation-lab: plateforme-ia-modulaire
  goog-terraform-provisioned: 'true'
  managed-by: terraform
  usage: tfstate
location: EUROPE-WEST1
public_access_prevention: enforced
soft_delete_policy:
  retentionDurationSeconds: '604800'
uniform_bucket_level_access: true
versioning_enabled: true

Le label goog-terraform-provisioned est ajouté par le provider google (attribution activée par défaut). La suppression douce (soft delete) de 7 jours est la valeur par défaut des nouveaux buckets : un objet d'état supprimé reste récupérable une semaine, et facturé pendant ce temps.

Le verrou, provoqué en lançant deux plan simultanés :

Deux plans simultanés : le second se heurte au verrou (sortie réelle)

Après quelques opérations, les générations accumulées :

      5 gs://lab-pia-tfstate-8f4e94/plateforme-ia/dev/default.tflock#<génération>
      5 gs://lab-pia-tfstate-8f4e94/plateforme-ia/dev/default.tfstate#<génération>

Le verrou aussi est versionné : chaque prise de verrou laisse une génération archivée. C'est la règle num_newer_versions = 20 qui empêche cette accumulation de grossir indéfiniment.

Ce qui casse en production

  • Verrou orphelin. Un apply tué (CI annulée, portable fermé) laisse default.tflock. Symptôme : toutes les opérations suivantes échouent avec Error acquiring the state lock et un Created ancien. Diagnostic : vérifier qu'aucune exécution n'est en cours (historique CI). Parade : terraform force-unlock <ID>, jamais avant d'avoir vérifié.
  • État corrompu ou écrasé. Un terraform state rm malheureux, ou un état poussé depuis une vieille branche. Parade : lister les générations (gcloud storage ls -a gs://…/default.tfstate), copier la bonne génération par-dessus, puis terraform plan pour vérifier.
  • Mauvais backend après un changement. Changer -backend-config sans -reconfigure ni -migrate-state fait échouer l'init (« Backend configuration changed »). Parade : savoir lequel des deux on veut ; make init utilise -reconfigure parce qu'il ne migre jamais d'état.
  • Validation qui réveille le backend. terraform init -backend=false dans un dossier déjà initialisé garde l'ancienne configuration ; validate a tenté de lire un état dans un bucket supprimé pendant ce lab. Parade : la cible make test utilise un dossier de travail séparé (TF_DATA_DIR=.terraform-ci).

La règle de décision

État distant dès qu'il y a plus d'une personne ou une CI. Un bucket d'état par frontière de sécurité (souvent : par projet de prod), versioning activé, accès en écriture réservé au compte de déploiement. Dossiers d'environnement dès que les environnements ont des identifiants différents ; workspaces seulement pour des copies jetables d'un même environnement.

Pratique

Une CI a été annulée en plein apply il y a 40 minutes. Depuis, tous les plans échouent sur « Error acquiring the state lock ». Quelle est la bonne séquence ?

La prod de votre client vit dans un projet séparé, déployée par un compte de service distinct de celui du dev. Un collègue propose des workspaces CLI dev/prod sur un même backend. Que répondez-vous ?

Exercice. Un terraform state rm a retiré par erreur le bucket d'archive de l'état de dev. Écrivez les commandes pour restaurer la génération précédente de l'état.

Voir la solution
B=gs://<bucket-d-etat>/plateforme-ia/dev/default.tfstate
gcloud storage ls -a -l "$B"                     # générations, avec date
gcloud storage cp "$B#<génération-précédente>" "$B"   # la remet en version courante
terraform -chdir=infra/envs/dev plan             # doit afficher "No changes"

Faites-le quand personne d'autre n'écrit l'état. Alternative sans manipuler le bucket : terraform import (ou un bloc import {}) de la ressource retirée.

  • Je crée un bucket d'état versionné, privé, avec une règle de rétention des versions.
  • Je configure un backend partiel et je sais quand utiliser -reconfigure ou -migrate-state.
  • Je sais lever un verrou orphelin sans risque et restaurer une génération d'état.
  • J'argumente dossiers d'environnement contre workspaces selon les identifiants.
Siguiente