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>.tflockavec une précondition « n'existe pas encore ». Si l'objet existe, GCS répond412 Precondition Failedet 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
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.
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 | |
|---|---|---|
| Backend | un seul, mêmes identifiants pour tous | un par dossier, identifiants séparés possibles |
| Différences dev/prod | terraform.workspace == "prod" ? … : … dans le code | un terraform.tfvars par dossier |
| Risque principal | apply sur le mauvais workspace, rien ne le montre dans le code | duplication des racines (limitée ici à 40 lignes) |
| Revue de code | on ne voit pas quel environnement change | le 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: trueLe 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 :
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
applytué (CI annulée, portable fermé) laissedefault.tflock. Symptôme : toutes les opérations suivantes échouent avecError acquiring the state locket unCreatedancien. 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 rmmalheureux, 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, puisterraform planpour vérifier. - Mauvais backend après un changement. Changer
-backend-configsans-reconfigureni-migrate-statefait échouer l'init (« Backend configuration changed »). Parade : savoir lequel des deux on veut ;make initutilise-reconfigureparce qu'il ne migre jamais d'état. - Validation qui réveille le backend.
terraform init -backend=falsedans un dossier déjà initialisé garde l'ancienne configuration ;validatea tenté de lire un état dans un bucket supprimé pendant ce lab. Parade : la ciblemake testutilise 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 ?
Le verrou protège contre une écriture concurrente : on ne le lève qu'après avoir vérifié que personne n'écrit. Un apply interrompu a pu créer des ressources sans les enregistrer : le plan qui suit le montre. -lock=false contourne la protection sans rien résoudre ; supprimer le bucket détruit l'état.
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 ?
Les workspaces partagent la configuration de backend : quiconque peut lire l'état de dev peut lire celui de prod. Le backend GCS supporte bien les workspaces ; la question est l'isolation, pas la compatibilité.
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
-reconfigureou-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.