Aller au contenu
L'état distant et le verrouillage

L'état distant et le verrouillage

200 Pratiquer ⏱ 1 h 15 terraformopentofuscaleways3iac

À la fin, vous saurez

  • Expliquer pourquoi un état partagé doit être distant et verrouillé, et ce qu'un verrou protège réellement
  • Configurer le backend s3 sur Object Storage de Scaleway, avec le verrouillage natif et sans identifiant dans le code
  • Migrer un état local vers un backend distant, et vérifier que la migration est complète
  • Choisir entre espaces de travail et répertoires pour séparer les environnements
  • Protéger le bucket d'état : accès restreint, versionnement, chiffrement
  • Diagnostiquer un verrou bloqué et savoir quand force-unlock est légitime

Prérequis

Testé avec random 3.9.1 terraform 1.16.1 , vérifié le 5 octobre 2026

Pourquoi

À la leçon 6, l'état vivait dans un fichier terraform.tfstate, à côté du code, sur le poste de la personne qui avait lancé apply. Tant qu'une seule personne travaille sur l'infrastructure de Signalements, cela tient. Le jour où Camille et Dominique travaillent toutes les deux sur le dépôt signalements-iac, trois accidents deviennent possibles, et chacun est déjà arrivé à des équipes réelles.

Le fichier qui n'est pas partagé. Camille crée le réseau privé et la base. L'état qui les décrit est sur son poste. Dominique clone le dépôt, lance terraform plan : Terraform ne trouve aucun état, conclut que rien n'existe, et propose de tout créer. Si Dominique applique, au mieux la création échoue parce qu'un nom est déjà pris ; au pire, l'infrastructure est créée en double, et la facture avec elle.

Le fichier partagé par Git. Pour éviter le premier accident, quelqu'un propose de commiter terraform.tfstate. C'est pire. L'état contient en clair les mots de passe de base de données, les clés générées, les jetons ; les pousser dans Git les copie dans chaque clone, pour toujours (voir la section Sécurité de la leçon 6). Et Git ne sait pas fusionner deux états : chaque apply produit un nouveau fichier, et deux personnes qui appliquent depuis deux branches finissent par écraser l'une le travail de l'autre.

Les deux apply simultanés. Même avec un fichier partagé au bon endroit, Camille et Dominique peuvent lancer apply à la même minute. Chacune lit l'état, calcule son plan, modifie l'infrastructure, puis réécrit l'état. La seconde écriture efface la première : des ressources existent dans le cloud sans plus figurer nulle part. Le prochain plan propose de les recréer.

La réponse tient en deux mots : l'état doit être distant (un seul exemplaire, au même endroit pour tout le monde et pour le pipeline) et verrouillé (une seule opération qui écrit à la fois). C'est le rôle des backends.

Les concepts

Le backend

Un backend est l'endroit où Terraform stocke l'état, et la façon dont il y accède. Sans configuration, c'est le backend local : un fichier sur le disque. Les autres backends rangent l'état ailleurs, par le réseau. Le choix se déclare dans le bloc terraform, une seule fois par configuration :

terraform {
  backend "s3" {
    # paramètres du backend
  }
}

Trois propriétés du bloc backend surprennent la première fois :

  • Il n'accepte pas de variables. Le backend est lu par terraform init, avant que les variables, les sources de données et les fournisseurs n'existent. On ne peut pas écrire bucket = var.bucket. On contourne par la configuration partielle, présentée plus bas.
  • Il est lu à l'initialisation seulement. Changer un paramètre du backend exige de relancer terraform init. Les autres commandes le détectent et refusent de travailler tant que ce n'est pas fait.
  • Il ne gère pas de ressource. Le bucket qui reçoit l'état doit exister avant terraform init : on ne peut pas le créer avec la configuration dont il stocke l'état. C'est le problème de l'œuf et de la poule, que la pratique règle par un amorçage à la main.

Les backends que l'on rencontre

BackendOù vit l'étatVerrouillageQuand le choisir
localun fichier sur le disqueverrou du système de fichiers, sur la machine seulementapprendre, essais jetables
s3un objet dans un bucket compatible S3fichier .tflock par écriture conditionnelle (ou DynamoDB, déprécié)le cas le plus courant, y compris chez Scaleway
pgune table PostgreSQLverrous consultatifs (advisory locks) de PostgreSQLune base PostgreSQL existe déjà, et l'on veut éviter un bucket
httpderrière une API RESTselon le serveurGitLab, qui offre un backend d'état intégré à ses projets
azurerm, gcsAzure Blob Storage, Google Cloud Storagebaux ou verrous natifschez ces fournisseurs
cloud (ou remote)HCP Terraform ou Terraform Enterprisegéré par le serviceéquipes qui ont adopté l'offre de HashiCorp

Le backend pg mérite une ligne de plus, parce qu'il convient à une équipe qui a déjà une base PostgreSQL managée. D'après sa documentation, il range l'état dans une table d'un schéma créé automatiquement (terraform_remote_state par défaut), une ligne par espace de travail, et verrouille par des verrous consultatifs de PostgreSQL. Ces verrous disparaissent d'eux-mêmes quand la session s'interrompt : force-unlock n'est pas pris en charge, puisqu'il n'est jamais nécessaire. L'URL de connexion peut venir de la variable PG_CONN_STR, ce qui évite d'écrire le mot de passe dans le code.

Le verrou

Un verrou d'état (state lock) est une marque posée par Terraform au début de toute opération qui pourrait écrire l'état (plan, apply, destroy, import, state mv...), et retirée à la fin. Une seconde opération qui trouve la marque refuse de démarrer. Le verrou ne protège donc pas l'infrastructure elle-même, seulement la cohérence de l'état : rien n'empêche une personne de supprimer une instance dans la console pendant un apply.

Pendant des années, le backend s3 verrouillait grâce à une table DynamoDB, un service propre à AWS : pour stocker l'état chez un autre fournisseur, il fallait se passer de verrou ou garder un pied chez AWS. Deux évolutions ont changé la donne :

  • Les écritures conditionnelles. Une requête PutObject peut porter l'en-tête HTTP If-None-Match: * : le stockage n'écrit l'objet que s'il n'existe pas déjà, et refuse sinon. Deux clients qui tentent de créer le même objet au même instant : un seul réussit. C'est exactement l'opération atomique dont un verrou a besoin.
  • Le verrouillage natif de Terraform. Terraform 1.10 a ajouté au backend s3, à titre expérimental, l'argument use_lockfile. Terraform 1.11 l'a rendu stable, et a déprécié dans le même temps les arguments liés à DynamoDB. La documentation du backend s3 l'écrit en toutes lettres : le verrouillage par DynamoDB est déprécié et sera retiré dans une version mineure future. OpenTofu a introduit le même mécanisme dans sa version 1.10 ; sa documentation précise que son équipe ne prévoit pas, elle, de retirer DynamoDB.

Avec use_lockfile = true, Terraform crée à côté de l'état un objet du même nom suivi de .tflock (pour une clé prod/terraform.tfstate, l'objet prod/terraform.tfstate.tflock), avec une écriture conditionnelle, puis le supprime à la fin de l'opération.

Important

Scaleway et le verrouillage natif. Pendant longtemps, Object Storage de Scaleway n'acceptait pas les écritures conditionnelles, et le verrouillage natif n'y fonctionnait pas. Le journal des changements de Scaleway du 26 mai 2026 annonce l'ajout des écritures conditionnelles et la prise en charge du verrouillage de l'état Terraform ; la documentation « Using conditional writes » décrit les en-têtes If-None-Match et If-Match. Au 5 octobre 2026, use_lockfile = true est donc la configuration recommandée sur Object Storage. Si vous travaillez avec un autre fournisseur compatible S3, vérifiez qu'il accepte les écritures conditionnelles avant de compter sur ce verrou : HashiCorp précise que la prise en charge des stockages compatibles S3 est faite « au mieux », et que seul Amazon S3 est testé.

Un état par environnement

Signalements a deux environnements, preprod et prod. Ils doivent avoir chacun leur état : un apply en préproduction ne doit jamais pouvoir toucher la production. Deux façons de faire coexistent.

Les espaces de travail de la CLI (workspaces). Une même configuration, un même backend, plusieurs états nommés : terraform workspace new prod, puis terraform workspace select prod. Dans le backend s3, l'état de l'espace default va à la clé configurée, et chaque autre espace à env:/<nom>/<clé> (le préfixe env: se change par workspace_key_prefix). Dans le code, terraform.workspace donne le nom courant.

Des répertoires séparés. Un répertoire par environnement (environnements/preprod/, environnements/prod/), chacun avec sa configuration de backend, qui appelle le même code commun (sous forme de module, objet du cours Terraform avancé : modules, tests, état).

La documentation de Terraform tranche pour le cas de Signalements : les espaces de travail utilisent tous le même backend, donc les mêmes identifiants et les mêmes droits ; ils ne sont pas, écrit-elle, un mécanisme d'isolation adapté quand les déploiements exigent des identifiants et des contrôles d'accès différents. Or c'est précisément ce que l'on veut : le pipeline de préproduction ne doit pas pouvoir écrire l'état de production. Le cours retient donc un répertoire et une clé d'état par environnement, et réserve les espaces de travail aux copies temporaires d'un même environnement (une branche de test, par exemple).

Espaces de travailRépertoires séparés
Backend et identifiantspartagésun par environnement, droits distincts possibles
Environnement courantinvisible dans les fichiers, dépend de la commande workspace select précédenteécrit dans le chemin du répertoire
Différences entre environnementsconditions sur terraform.workspace dans le codefichiers de valeurs distincts
Risque typiqueapply dans le mauvais espaceduplication si le code commun n'est pas factorisé

L'état d'une autre configuration

Une configuration a parfois besoin d'une valeur produite par une autre : la configuration du réseau crée pn-signalements, celle de l'application doit y attacher ses instances. La source de données terraform_remote_state lit les sorties d'un autre état. Elle est commode, et sa documentation met en garde : même si elle n'expose que les sorties, son utilisateur doit avoir accès à tout l'état, secrets compris, et quiconque peut lire les sorties peut lire l'état entier par une requête directe au backend. Les alternatives recommandées publient la valeur ailleurs, avec ses propres droits ; chez Scaleway, le plus simple est souvent une source de données du fournisseur qui retrouve la ressource par son nom (data "scaleway_vpc_private_network", leçon 4), sans aucun accès à l'état des autres.

En pratique

L'équipe va stocker l'état de Signalements dans Object Storage, avec un bucket dédié, une application IAM dédiée, un verrou natif, et une clé d'état par environnement. Les commandes scw créent de vraies ressources, facturées à l'usage (quelques centimes pour un état de quelques kilo-octets) ; les sorties de terraform init vers le backend distant ne sont pas reproduites, faute d'avoir été exécutées pour ce cours, et sont décrites en prose.

Amorcer : le bucket et l'identité du backend

Le bucket d'état est créé à la main, une fois, en dehors de toute configuration Terraform : c'est l'amorçage. Créez-le dans le projet de production (leçon 1 du cours Scaleway en pratique), avec le versionnement activé dès la création :

$ scw object bucket create name=signalements-tfstate-<suffixe> \
    enable-versioning=true acl=private region=fr-par \
    project-id="$PROD" tags.0=role=etat-terraform
  • enable-versioning=true garde chaque version de l'état : si un apply raté ou une manipulation de terraform state corrompt l'état, la version précédente reste récupérable. C'est la protection la plus utile de cette leçon.
  • acl=private est la valeur par défaut ; l'écrire rend l'intention lisible.
  • Le nom d'un bucket est unique pour toute la région : le suffixe l'est aussi.

Créez ensuite une application IAM réservée au backend, avec une clé qui expire. Les jeux de permissions de Scaleway pour Object Storage sont fins ; pour lire et écrire l'état et son fichier de verrou, il faut lire, écrire et supprimer des objets :

$ APP_ETAT=$(scw iam application create name=signalements-tfstate \
    description="Backend Terraform de Signalements" -o json | jq -r .id)
$ scw iam policy create name=signalements-tfstate application-id="$APP_ETAT" \
    rules.0.project-ids.0="$PROD" \
    rules.0.permission-set-names.0=ObjectStorageObjectsRead \
    rules.0.permission-set-names.1=ObjectStorageObjectsWrite \
    rules.0.permission-set-names.2=ObjectStorageObjectsDelete \
    rules.0.permission-set-names.3=ObjectStorageBucketsRead
$ scw iam api-key create application-id="$APP_ETAT" expires-at=+90d \
    default-project-id="$PROD" description="Backend Terraform" -o json > .tmp/cle-etat.json

ObjectStorageObjectsDelete n'est pas un luxe : sans lui, Terraform pose le verrou mais ne peut pas le retirer, et chaque opération suivante échoue sur un verrou fantôme. Le champ default-project-id compte ici : pour Object Storage, c'est lui qui détermine dans quel projet la clé voit les buckets (leçon 7 du cours cloud). Ces droits portent sur tout le projet de production : pour les limiter au seul bucket d'état, ajoutez une politique de bucket (leçon 5 du cours cloud).

Déclarer le backend, sans secret

Dans environnements/prod/backend.tf :

terraform {
  backend "s3" {
    bucket       = "signalements-tfstate-<suffixe>"
    key          = "prod/terraform.tfstate"
    region       = "fr-par"
    use_lockfile = true

    endpoints = {
      s3 = "https://s3.fr-par.scw.cloud"
    }

    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
  }
}

Chaque argument a une raison :

  • bucket, key : l'objet qui contient l'état. La clé porte le nom de l'environnement ; la préproduction utilisera preprod/terraform.tfstate dans le même bucket, ou mieux, un bucket du projet de préproduction.
  • region = "fr-par" : le backend s3 exige une région ; il la transmet telle quelle au service.
  • use_lockfile = true : le verrou natif, par écriture conditionnelle.
  • endpoints.s3 : l'adresse du service S3 de Scaleway pour la région. Sans elle, Terraform s'adresserait à Amazon S3.
  • skip_credentials_validation : par défaut, le backend vérifie les identifiants auprès de l'API STS d'AWS, qui n'existe pas chez Scaleway.
  • skip_region_validation : fr-par n'est pas un nom de région AWS ; sans cette option, le backend le refuserait.
  • skip_requesting_account_id : le backend tente sinon de déterminer un numéro de compte AWS par les API IAM, STS ou de métadonnées.

Ces trois options skip_ sont celles que donne le tutoriel Terraform de Scaleway, et la documentation du backend les présente comme utiles pour les implémentations de l'API qui n'ont pas STS ni IAM. Deux autres existent, à connaître sans les ajouter par réflexe : skip_metadata_api_check (ne pas interroger le service de métadonnées d'EC2) et skip_s3_checksum (ne pas joindre de somme de contrôle aux envois, utile pour certains stockages compatibles S3). Quant à use_path_style, qui force les adresses de la forme https://<hôte>/<bucket>, il n'est pas nécessaire chez Scaleway, qui sert aussi la forme https://<bucket>.<hôte>.

Aucun identifiant dans ce fichier. Le tutoriel de Scaleway montre access_key et secret_key directement dans le bloc backend : ne le reproduisez pas, ce fichier est commité. Le backend s3 lit les identifiants comme le SDK d'AWS, en particulier dans les variables AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY, ou dans un profil du fichier ~/.aws/credentials (celui du cours cloud s'appelait scw-par) désigné par AWS_PROFILE :

$ export AWS_ACCESS_KEY_ID=$(jq -r .access_key .tmp/cle-etat.json)
$ export AWS_SECRET_ACCESS_KEY=$(jq -r .secret_key .tmp/cle-etat.json)

Note

Ces variables servent au backend, pas au fournisseur Scaleway, qui lit les siennes (SCW_ACCESS_KEY, SCW_SECRET_KEY, ou la configuration de la CLI scw). Deux identités distinctes, c'est voulu : celle qui lit l'état n'a pas à pouvoir créer des instances, et réciproquement.

La configuration partielle

Le même code doit servir à preprod et à prod, avec des buckets et des clés différents, mais le bloc backend n'accepte pas de variable. La configuration partielle laisse des arguments vides dans le code et les fournit à l'initialisation, par un fichier ou par la ligne de commande :

terraform {
  backend "s3" {
    region       = "fr-par"
    use_lockfile = true
    endpoints = {
      s3 = "https://s3.fr-par.scw.cloud"
    }
    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
  }
}
# backend-prod.hcl
bucket = "signalements-tfstate-<suffixe>"
key    = "prod/terraform.tfstate"
$ terraform init -backend-config=backend-prod.hcl

Le fichier backend-prod.hcl ne contient pas de secret, il se commite. Les identifiants, eux, restent dans l'environnement. Terraform garde la configuration résolue dans .terraform/terraform.tfstate (un fichier du répertoire de travail, à ne pas confondre avec l'état lui-même, et qui ne se commite pas) ; changer de fichier -backend-config exige un nouvel init.

Migrer un état local

L'état de la leçon 6 est encore local. Ajouter le bloc backend ne suffit pas : toute commande refuse de travailler tant que l'initialisation n'est pas refaite. Voici ce qu'affiche terraform plan dans ce cas ; la démonstration a été faite avec un backend local pointé vers un autre répertoire, pour obtenir de vraies sorties sans réseau, mais le message est le même pour s3 :

$ terraform plan -no-color

Error: Backend initialization required, please run "terraform init"

Reason: Initial configuration of the requested backend "local"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

Changes to backend configurations require reinitialization. This allows
Terraform to set up the new configuration, copy existing state, etc. Please
run
"terraform init" with either the "-reconfigure" or "-migrate-state" flags to
use the current configuration.

Deux options, à ne pas confondre :

  • -migrate-state copie l'état existant vers le nouveau backend. C'est ce que l'on veut ici.
  • -reconfigure ignore l'ancien backend et repart du nouveau tel quel. Utile quand le nouveau backend contient déjà le bon état ; désastreux si l'on croyait migrer, puisque l'infrastructure existante disparaît du point de vue de Terraform.
$ terraform init -migrate-state -no-color
Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "local" backend. No existing state was found in the newly
  configured "local" backend. Do you want to copy this state to the new "local"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.

Avec le backend s3, la question est la même, avec "s3" à la place du second "local". Trois vérifications après la migration :

  1. terraform plan doit répondre No changes. Un plan qui propose de tout créer signale un état vide : la migration n'a pas eu lieu, ou la clé est fausse.
  2. L'objet prod/terraform.tfstate doit apparaître dans le bucket (aws s3 ls s3://signalements-tfstate-<suffixe>/prod/ --profile scw-par).
  3. L'ancien fichier local est toujours là. Terraform ne le supprime pas : dans la démonstration, terraform.tfstate et terraform.tfstate.backup restent dans le répertoire après la migration. Supprimez-les (après avoir vérifié les deux points précédents), sinon quelqu'un finira par les lire, ou par les commiter.

Observer le verrou

Pour voir un verrou en action sans risque, lancez une opération longue dans un terminal et un plan dans un autre. Dans la démonstration, une ressource terraform_data avec un provisionneur sleep 8 occupe l'apply ; le plan lancé pendant ce temps échoue aussitôt :

$ terraform plan -no-color

Error: Error acquiring the state lock

Error message: resource temporarily unavailable
Lock Info:
  ID:        673fcb55-5720-8740-c804-ecbd7e721a8c
  Path:      ../etat-partage/signalements.tfstate
  Operation: OperationTypeApply
  Who:       camille@poste-camille
  Version:   1.16.1
  Created:   2026-10-05 10:55:48.476926261 +0000 UTC
  Info:      


Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

Lisez les champs : l'identifiant du verrou, l'opération qui le tient (OperationTypeApply), qui (utilisateur et machine), la version de Terraform et l'heure. Avec le backend s3, ces informations sont le contenu du fichier .tflock. Elles répondent à la seule question utile devant un verrou : l'opération qui le tient est-elle encore en cours ?

Deux options aident au quotidien : -lock-timeout=5m fait attendre Terraform jusqu'à cinq minutes que le verrou se libère, au lieu d'échouer aussitôt (utile en intégration continue, quand deux pipelines se suivent de près) ; -lock=false désactive le verrou, ce que le message déconseille avec raison.

Le verrou bloqué

Si un apply est interrompu brutalement (poste éteint, pipeline tué), le verrou peut rester en place. La commande terraform force-unlock <ID> le retire. Sa documentation précise qu'elle ne modifie pas l'infrastructure, seulement le verrou. La procédure prudente :

  1. Lire le message d'erreur : qui, quelle opération, depuis quand.
  2. Vérifier que l'opération est réellement terminée : le pipeline est-il arrêté ? La personne a-t-elle fermé son terminal ? Un verrou vieux de deux minutes est probablement légitime.
  3. Seulement ensuite, terraform force-unlock 673fcb55-5720-8740-c804-ecbd7e721a8c, puis un terraform plan pour constater dans quel état l'interruption a laissé l'infrastructure.

Retirer le verrou d'une opération encore en cours, c'est recréer exactement l'accident des deux apply simultanés que le verrou devait empêcher.

Sous le capot

Ce que contient le bucket. Pour un environnement, deux objets au plus : l'état (prod/terraform.tfstate), un document JSON réécrit en entier à chaque opération qui modifie l'infrastructure, et le verrou (prod/terraform.tfstate.tflock), présent seulement pendant une opération. Avec le versionnement activé, chaque réécriture de l'état crée une nouvelle version de l'objet ; la suppression du verrou crée un marqueur de suppression. L'historique complet des états reste donc consultable et restaurable (aws s3api list-object-versions).

Comment se prend le verrou. Terraform envoie un PutObject sur la clé .tflock avec l'en-tête If-None-Match: *. Si l'objet n'existe pas, le stockage l'écrit et répond par un succès : le verrou est pris. S'il existe, le stockage refuse l'écriture (code HTTP 412 Precondition Failed) : quelqu'un d'autre tient le verrou, et Terraform lit le contenu du fichier pour afficher le message vu plus haut. À la fin de l'opération, Terraform supprime l'objet. L'atomicité est garantie par le stockage lui-même : c'est lui, et non Terraform, qui arbitre entre deux requêtes simultanées. Un stockage qui ignorerait l'en-tête accepterait les deux écritures, et le verrou ne protégerait rien, sans aucun message d'erreur : d'où l'importance de vérifier la prise en charge avant de s'y fier.

Pourquoi l'état est réécrit en entier. Le format d'état n'a pas d'opération partielle : chaque écriture remplace l'objet. Deux apply concurrents sans verrou ne produisent donc pas un mélange de leurs changements, mais la victoire du dernier qui écrit. Le verrou existe pour cette raison.

Le numéro de série. Chaque état porte un champ serial, incrémenté à chaque écriture, et un lineage, identifiant fixé à la création de l'état. Terraform refuse d'écrire un état dont le lineage diffère de celui du backend, et s'inquiète d'un serial qui recule : ce sont les garde-fous qui empêchent de pousser par erreur l'état d'un autre projet (terraform state push) ou un état périmé.

Pièges courants

Le bucket d'état créé par la configuration qu'il héberge. Écrire une ressource scaleway_object_bucket pour le bucket d'état dans la même configuration crée une dépendance circulaire : il faut l'état pour créer le bucket, et le bucket pour l'état. Le bucket d'état s'amorce à la main (ou par une petite configuration à état local dédiée, que l'on ne touche plus ensuite).

Les identifiants dans le bloc backend. Ils finissent dans Git, et aussi dans .terraform/terraform.tfstate. Passez-les par l'environnement.

-reconfigure au lieu de -migrate-state. Le premier ne copie rien. Après lui, un plan qui propose de tout recréer n'est pas un bogue : c'est l'état vide que vous avez demandé.

Le verrou fantôme faute de droit de suppression. Une identité qui peut écrire mais pas supprimer pose des verrous qu'elle ne retire jamais. Le symptôme : la première opération réussit, toutes les suivantes échouent sur un verrou tenu par... vous-même.

Croire que use_lockfile verrouille partout. Un stockage compatible S3 qui ignore If-None-Match laisse passer deux écritures. Avant Scaleway mai 2026, c'était le cas ; chez d'autres fournisseurs, vérifiez.

terraform workspace select oublié. Avec les espaces de travail, l'environnement courant ne se voit pas dans les fichiers. Un apply lancé dans l'espace de la veille modifie le mauvais environnement. C'est l'une des raisons du choix des répertoires.

L'ancien terraform.tfstate local qui reste. Il n'est plus lu par Terraform, mais il contient des secrets et une vue périmée de l'infrastructure. Supprimez-le après migration, et gardez *.tfstate* dans le .gitignore.

Sécurité

L'état est le fichier le plus sensible du dépôt, alors qu'il n'est pas dans le dépôt. Il contient en clair tous les attributs des ressources, y compris les mots de passe générés, les clés privées créées par Terraform et les valeurs marquées sensitive (qui ne sont masquées qu'à l'affichage). Qui lit l'état lit les secrets de toute l'infrastructure qu'il décrit. Quatre mesures :

  • Un accès minimal. Seuls l'identité du pipeline et quelques administrateurs lisent le bucket d'état. Les développeurs qui ont seulement besoin de lancer un plan en lecture... ont en fait besoin de lire l'état : c'est une raison de faire tourner les plans dans le pipeline (leçon 12) plutôt que sur les postes.
  • Un bucket par environnement, dans le projet de l'environnement. L'identité de préproduction ne voit même pas l'état de production. Les règles de politique de Scaleway s'appliquent par projet : c'est la séparation la plus simple à obtenir.
  • Le versionnement, et éventuellement l'Object Lock. Le versionnement protège d'une corruption ou d'une suppression accidentelle. L'Object Lock (leçon 15 du cours Scaleway en pratique) empêche en plus de supprimer les anciennes versions pendant la durée de rétention, y compris pour un attaquant muni des clés ; il a un coût, puisque chaque version est conservée jusqu'à son échéance, et ne se justifie que si l'état fait partie de ce que vous voulez pouvoir reconstruire après une compromission.
  • Le chiffrement. Le chiffrement côté serveur protège des supports perdus, pas d'une identité légitime qui lit l'objet. OpenTofu propose depuis sa version 1.7 un chiffrement côté client de l'état et des plans : la configuration déclare, dans un bloc encryption du bloc terraform, un fournisseur de clé (une phrase de passe dérivée par PBKDF2, un KMS, OpenBao...) et une méthode (aes_gcm). Le backend ne voit alors qu'un objet chiffré. Sa documentation avertit sans détour : sans la clé, l'état est irrécupérable. Terraform n'a pas d'équivalent au 5 octobre 2026.

Le verrou n'est pas un contrôle d'accès. Il empêche deux écritures simultanées, pas une écriture non autorisée. Les droits passent par l'IAM et la politique du bucket.

terraform_remote_state donne tout l'état. Accorder à la configuration de l'application la lecture de l'état du réseau, c'est lui donner aussi les secrets du réseau. Préférez une source de données du fournisseur.

En production

  • Le modèle cible : un bucket d'état par application et par environnement, versionné, accessible au seul pipeline de déploiement et à un petit nombre d'administrateurs ; les plans tournent dans l'intégration continue, et personne n'applique depuis son poste en production (leçon 12).
  • Mesurez la taille de l'état. Un état de plusieurs mégaoctets ralentit chaque opération (il est lu et réécrit en entier) et signale une configuration trop grosse, à découper en plusieurs configurations, chacune avec son état. Le cours Terraform avancé : modules, tests, état traite ce découpage.
  • Sauvegardez hors du fournisseur. Le versionnement ne protège pas de la perte du compte. Une copie périodique de l'état, chiffrée, ailleurs, fait partie du plan de reprise (cours Sauvegarde, restauration et PRA).
  • Documentez la procédure de déverrouillage dans le runbook de l'équipe, avec la règle « jamais sans avoir vérifié que l'opération est terminée ». Un force-unlock hâtif à trois heures du matin est la cause classique d'un état désynchronisé.
  • Migration depuis DynamoDB. Les équipes qui verrouillaient avec DynamoDB peuvent, d'après la documentation, configurer les deux mécanismes à la fois le temps de la transition : Terraform prend alors les deux verrous. Une fois tous les clients mis à jour, on retire DynamoDB.

Exercices

1. Le plan qui veut tout créer (niveau 200). Après avoir ajouté un bloc backend "s3" et lancé terraform init -reconfigure, Dominique obtient un plan qui propose de créer toutes les ressources de production. Que s'est-il passé, et comment réparer sans rien détruire ?

Solution

-reconfigure a initialisé le nouveau backend sans copier l'état local : la clé distante est vide, donc Terraform croit que rien n'existe. Surtout ne pas appliquer. L'état local (terraform.tfstate) est encore sur le poste si personne ne l'a supprimé : relancer terraform init -migrate-state en revenant d'abord à l'ancienne configuration n'est pas nécessaire, il suffit de pousser l'état local vers le backend (terraform state push terraform.tfstate), puis de vérifier que terraform plan répond No changes. Si l'état local a disparu, il reste les versions du bucket (si un état y avait déjà été écrit) ou la reconstruction par import (leçon 11).

2. Configuration partielle (niveau 200). Écrivez le fichier backend-preprod.hcl et la commande d'initialisation pour la préproduction, sachant que son bucket d'état s'appelle signalements-tfstate-preprod-<suffixe>. Où vont les identifiants ?

Solution
bucket = "signalements-tfstate-preprod-<suffixe>"
key    = "preprod/terraform.tfstate"
$ terraform init -backend-config=backend-preprod.hcl

Les identifiants de l'application IAM du backend de préproduction vont dans AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY (ou un profil AWS_PROFILE), jamais dans le fichier .hcl. Comme le bucket est dans le projet de préproduction, cette identité n'a aucun droit sur celui de production.

3. Le verrou de 3 h 12 (niveau 200). À 9 h, un pipeline échoue sur Error acquiring the state lock. Le verrou a été posé à 3 h 12 par l'opération OperationTypeApply, depuis l'agent de CI d'un pipeline nocturne. Décrivez votre démarche.

Solution

Vérifier dans l'outil de CI ce qu'est devenu le pipeline de 3 h 12 : s'il tourne encore (un apply bloqué sur une ressource longue à créer), attendre ou l'arrêter proprement. S'il a été tué (délai maximal dépassé, agent perdu), l'opération est terminée, mais peut-être à moitié : lancer terraform force-unlock <ID>, puis terraform plan pour voir ce qui reste à faire, avant de relancer. Ensuite, comprendre pourquoi le pipeline a été tué (délai trop court pour une base de données ?) et noter l'incident.

4. Espaces de travail ou répertoires (niveau 200). Une équipe veut un environnement d'essai éphémère par demande de fusion, détruit à la fusion, en plus de preprod et prod. Que proposez-vous pour chacun ?

Solution

preprod et prod : des répertoires séparés, des buckets d'état dans leurs projets respectifs, des identités distinctes. Les environnements éphémères sont des copies d'un même environnement, avec les mêmes droits : un espace de travail par demande de fusion dans le backend de préproduction (terraform workspace new mr-42), détruit et supprimé à la fusion (terraform destroy, puis terraform workspace delete mr-42), convient bien, à condition que la configuration utilise terraform.workspace dans les noms des ressources pour éviter les collisions.

Récapitulatif

  • Un état partagé doit être distant (un seul exemplaire pour toute l'équipe et le pipeline) et verrouillé (une seule opération qui écrit à la fois). Jamais dans Git.
  • Le backend se déclare dans le bloc terraform, n'accepte pas de variables, se lit à init, et son bucket s'amorce à la main.
  • Le backend s3 fonctionne avec Object Storage de Scaleway : endpoints.s3, region = "fr-par", skip_credentials_validation, skip_region_validation, skip_requesting_account_id, et des identifiants dans l'environnement.
  • use_lockfile = true verrouille par écriture conditionnelle (If-None-Match), stable depuis Terraform 1.11 et présent dans OpenTofu 1.10 ; DynamoDB est déprécié chez Terraform. Object Storage de Scaleway le prend en charge depuis le 26 mai 2026.
  • init -migrate-state copie l'état, -reconfigure ne copie rien ; après migration, plan doit répondre No changes, et l'ancien fichier local se supprime.
  • Un environnement, un état, des droits distincts : les répertoires séparés plutôt que les espaces de travail pour preprod et prod.
  • L'état contient tous les secrets : accès minimal, bucket par environnement, versionnement, chiffrement côté client avec OpenTofu. force-unlock seulement après avoir vérifié que l'opération est terminée.

Pour aller plus loin

  • La documentation du backend s3 de Terraform, en particulier la section sur les permissions nécessaires et sur la migration depuis DynamoDB.
  • La page « Using conditional writes » de la documentation d'Object Storage de Scaleway, pour comprendre le mécanisme sur lequel repose le verrou.
  • La documentation du chiffrement de l'état d'OpenTofu, si votre état doit être illisible pour le fournisseur qui l'héberge (voir aussi la leçon 5 du cours Cloud souverain).
  • Le chapitre 3 de Terraform: Up & Running, qui raconte en détail les accidents décrits dans le Pourquoi.
  • La leçon suivante, sur l'ordre dans lequel Terraform crée, modifie et détruit les ressources.
Voir ma constellation →

Sources