
Mettre son state dans un bucket S3 règle le partage entre opérateurs. Mais
deux points décident de la suite, et tous deux se mesurent : le
verrouillage n'est pas actif par défaut, et le bloc backend refuse
toute valeur nommée, ce qui impose la configuration partielle.
Tout ce qui suit a été exécuté sur Terraform v1.15.4, contre un émulateur S3
local, sans mobiliser le moindre compte AWS. Deux idées répandues n'y
survivent pas, celle du backend "s3" qui verrouillerait de lui-même, et
celle du terraform.tfstate local qu'il faudrait nettoyer après migration.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer le bucket de state, dans un bootstrap séparé
- Pourquoi un bloc
backendn'accepte aucune variable, et quoi faire à la place - Activer un verrou réellement effectif, et le prouver
- Vérifier quelle configuration Terraform a retenue, sans la deviner
- Partager des outputs entre deux configurations, et ce que cela coûte
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform). Le verrouillage S3 natif est arrivé en 1.10 ; les mesures viennent de 1.15.4.
- Un accès S3, réel ou émulé. Aucune des mesures de cette page n'a demandé de compte AWS.
Le bucket d'abord, dans un bootstrap séparé
Section intitulée « Le bucket d'abord, dans un bootstrap séparé »Le backend doit exister avant d'être utilisé : la configuration qui crée le bucket ne peut pas y ranger son propre state. On la garde donc à part, avec un state local assumé.
resource "aws_s3_bucket" "state" { bucket = "mon-org-terraform-state"}
resource "aws_s3_bucket_versioning" "state" { bucket = aws_s3_bucket.state.id
versioning_configuration { status = "Enabled" }}
resource "aws_s3_bucket_public_access_block" "state" { bucket = aws_s3_bucket.state.id
block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}Le versioning n'est pas décoratif : c'est lui qui permet de revenir à une version antérieure du state après une opération malheureuse.
Le bloc backend n'accepte aucune variable
Section intitulée « Le bloc backend n'accepte aucune variable »C'est la contrainte qui commande tout le reste, et elle surprend parce qu'elle contredit le reste du HCL. Mesuré :
terraform { backend "s3" { bucket = var.backend_bucket }}Error: Variables not allowed
on main.tf line 8, in terraform: 8: bucket = var.backend_bucket
Variables may not be used here.L'init sort en 1. La documentation l'énonce : « A backend block cannot
refer to named values (like input variables, locals, or data source
attributes). » Le backend est lu avant l'évaluation des variables, il ne peut
donc rien en connaître.
La configuration partielle, en fichier
Section intitulée « La configuration partielle, en fichier »La réponse officielle est de laisser le bloc incomplet et de fournir le reste
à l'init. La forme fichier vaut mieux que KEY=VALUE, que la documentation
déconseille pour un secret puisque l'historique du shell le conserve. Le
nommage recommandé est *.<backend>.tfbackend :
terraform { backend "s3" {}}bucket = "mon-org-terraform-state"key = "producer/terraform.tfstate"region = "eu-west-3"
use_lockfile = trueencrypt = trueterraform init -backend-config=prod.s3.tfbackendUn même dossier sert alors plusieurs environnements, en ne changeant que le
fichier passé à l'init.
Vérifier ce que Terraform a retenu
Section intitulée « Vérifier ce que Terraform a retenu »Ne le devinez pas : Terraform l'écrit. Le fichier
.terraform/terraform.tfstate porte la configuration de backend résolue, et
c'est la seule preuve fiable.
jq '{type: .backend.type, config: .backend.config}' .terraform/terraform.tfstateMesuré, avec un endpoint personnalisé :
{ "type": "s3", "config": { "bucket": "tf-state-sonde", "key": "producer/terraform.tfstate", "region": "eu-west-3", "use_path_style": true, "use_lockfile": true, "endpoints": { "s3": "http://localhost:4566" } }}Un aws s3 ls prouverait qu'un objet existe ; il ne dit pas quelle
configuration Terraform a réellement retenue.
Le verrouillage est un opt-in strict
Section intitulée « Le verrouillage est un opt-in strict »Voici le point le plus important de cette page. Déclarer un backend "s3" ne
verrouille rien. L'argument est use_lockfile, et il vaut false par
défaut. Terraform dépose alors un objet <clé>.tflock à côté du state, par
écriture conditionnelle.
La démonstration tient en deux essais. On dépose à la main un .tflock dans
le bucket, puis on planifie.
Avec use_lockfile = true :
terraform plan -lock-timeout=0sError: Error acquiring the state lock
Error message: operation error S3: PutObject, https response errorCode 1. Sans l'argument, et avec le même objet toujours présent :
random_pet.nom: Refreshing state...Code 0. Le verrou est donc purement ignoré, et deux applies concurrents se marchent dessus en silence.
Partager des outputs entre configurations
Section intitulée « Partager des outputs entre configurations »Une fois le state distant en place, une autre configuration peut en lire les outputs racine :
data "terraform_remote_state" "producer" { backend = "s3"
config = { bucket = "mon-org-terraform-state" key = "producer/terraform.tfstate" region = "eu-west-3" }}
output "nom_amont" { value = data.terraform_remote_state.producer.outputs.nom}Mesuré : le mécanisme est intégré, servi par
terraform.io/builtin/terraform, et la valeur suit l'amont. Après avoir
changé une entrée du producer puis l'avoir réappliqué, le consumer rejoué est
passé de relaxed-pony à notable-lemur. Une valeur recopiée à la main,
elle, serait restée figée.
Les pièges mesurés
Section intitulée « Les pièges mesurés »Il n'y a pas de state local à nettoyer
Section intitulée « Il n'y a pas de state local à nettoyer »Beaucoup de guides font suivre la migration d'un
rm -rf .terraform terraform.tfstate terraform.tfstate.backup. Mesuré, une fois
le backend S3 initialisé, le répertoire ne contient aucun terraform.tfstate
local : cette commande est un rm à vide, et elle entretient l'idée fausse
qu'une copie subsisterait.
fichiers terraform.tfstate* dans producer/ : AUCUNobjets dans le bucket : ['producer/terraform.tfstate']defaults ne rattrape pas un état absent
Section intitulée « defaults ne rattrape pas un état absent »L'argument defaults de terraform_remote_state fournit une valeur de repli
« in case the state file is empty or lacks a required output ». La nuance est
importante, et elle se mesure : sur un état qui existe mais auquel il manque
l'output, le repli joue. Sur une clé inexistante, il ne sauve rien :
Error: Unable to find remote state
No stored state was found for the given workspace in the given backend.Code 1. defaults gère donc une interface incomplète, pas une stack
absente : la parade au « l'amont n'a pas encore été appliqué » reste
d'appliquer l'amont.
Les permissions ne sont pas celles qu'on croit
Section intitulée « Les permissions ne sont pas celles qu'on croit »La liste officielle commence par s3:ListBucket sur le bucket, « at a
minimum, this must be able to list the path where the state is stored ».
S'ajoutent ensuite s3:GetObject et s3:PutObject sur l'objet de state.
Le contre-pied est net : s3:DeleteObject n'est pas requis sur le fichier de
state, « as Terraform does not delete it », puisqu'il l'écrase. Il l'est en
revanche sur l'objet .tflock, qu'il faut bien pouvoir retirer.
Publier un output, c'est publier tout l'état
Section intitulée « Publier un output, c'est publier tout l'état »La documentation ne recommande pas, elle interdit : « Don't use
terraform_remote_state if any of the resources in your configuration work with
data that you consider sensitive. » La raison est que lire les outputs suppose un
accès complet au snapshot d'état.
Elle ouvre d'ailleurs sa page par une alternative : « We recommend using the
tfe_outputs data source […] more secure because it does not require full access
to workspace state to fetch outputs. » À défaut, un magasin de paramètres
dédié joue le même rôle, en ne publiant que la valeur voulue.
Hors d'AWS, tout repose sur quelques arguments
Section intitulée « Hors d'AWS, tout repose sur quelques arguments »Faire fonctionner ce backend contre MinIO, Ceph ou un émulateur demande un jeu
d'arguments précis : le bloc endpoints (dont endpoints.s3),
use_path_style, puis skip_credentials_validation,
skip_requesting_account_id, skip_metadata_api_check, skip_region_validation
et skip_s3_checksum. C'est exactement ce jeu qui a permis toutes les mesures
de cette page.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Correction |
|---|---|---|
Error: Variables not allowed | une variable dans le bloc backend | configuration partielle plus -backend-config |
| deux applies concurrents s'écrasent | use_lockfile absent : le verrou est un opt-in | l'ajouter, puis le prouver avec un .tflock déposé |
Error acquiring the state lock | un verrou est détenu, ou orphelin | -lock-timeout en CI ; sinon retirer l'objet .tflock |
Error: Backend configuration changed | backend modifié sans réinitialiser | init -migrate-state pour emporter l'état, -reconfigure pour l'abandonner |
Unable to find remote state | la stack amont n'a pas été appliquée | l'appliquer ; defaults ne couvre pas ce cas |
un identifiant traîne dans .terraform/ | passé via -backend-config | le passer par variables d'environnement |
| le backend ne répond pas hors AWS | endpoints, use_path_style ou un skip_* manquant | compléter le jeu d'arguments S3 compatible |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab un state distant, verrouillé, et lu par une autre stack monte un bucket
sur un émulateur local, puis fait réparer un backend "s3" piégé : il déclare
bucket = var.backend_bucket et n'a aucun argument de verrouillage. Les tests
déposent eux-mêmes un .tflock et exigent que le plan échoue, puis le
retirent et exigent qu'il passe : une configuration sans use_lockfile tombe
donc précisément là où l'énoncé l'interdit. Il se joue hors ligne, sans compte
AWS.
À retenir
Section intitulée « À retenir »- Le bucket de state se crée dans un bootstrap séparé, au state local assumé.
- Un bloc
backendn'accepte aucune valeur nommée :Error: Variables not allowed, code 1. - La configuration partielle en fichier
*.s3.tfbackendest la réponse, et la formeKEY=VALUEest déconseillée pour un secret. use_lockfile = trueest un opt-in : sans lui, un.tflockprésent est purement ignoré.- Le verrouillage natif date de Terraform 1.10 ; DynamoDB est déprécié.
.terraform/terraform.tfstateporte la configuration résolue : c'est là qu'on vérifie, et c'est là que des identifiants atterrissent.- Après migration, il ne reste aucun state local : le
rmhabituel est à vide. defaultscomble un output manquant, jamais un état absent.s3:ListBucketest requis ;s3:DeleteObjectne l'est pas sur le state.- Hors AWS, tout tient à
endpoints,use_path_styleet auxskip_*, avec un support « best effort ».
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui casse en pratique : le verrou
qu'on croit actif, la variable refusée dans le bloc backend, et
l'identifiant qui atterrit dans .terraform/.
Mesuré sur 1.15.4
Un objet.tflock est déposé à la main dans le bucket, puis on planifie.Avec use_lockfile = true :Error: Error acquiring the state lock
Error message: operation error S3: PutObject, https response error
Code 1.Sans l'argument, le même objet toujours présent :random_pet.nom: Refreshing state...
Code 0. Le verrou est ignoré.Ce qu'il faut retenir
Le verrouillage natif S3 est arrivé en Terraform 1.10, et le verrouillage par table DynamoDB est déprécié. Sur un projet neuf,use_lockfile = true suffit, sans table annexe.Le message exact
terraform {
backend "s3" {
bucket = var.backend_bucket
}
}
Error: Variables not allowed
on main.tf line 8, in terraform:
8: bucket = var.backend_bucket
Variables may not be used here.
La raison
« A backend block cannot refer to named values (like input variables, locals, or data source attributes). »Terraform doit savoir où lire l'état avant de pouvoir évaluer quoi que ce soit. Ce n'est donc pas une limite arbitraire, c'est l'ordre des opérations.La réponse est la configuration partielle : un bloc incomplet, et le reste fourni à l'init.La forme recommandée
terraform {
backend "s3" {}
}
# prod.s3.tfbackend
bucket = "mon-org-terraform-state"
key = "producer/terraform.tfstate"
region = "eu-west-3"
use_lockfile = true
encrypt = true
terraform init -backend-config=prod.s3.tfbackend
Pourquoi le fichier plutôt que KEY=VALUE
La documentation déconseille la forme en ligne de commande pour des secrets : l'historique du shell les conserve.La limite à connaître
Cette technique protège le dépôt, pas le disque. Mesuré,.terraform/terraform.tfstate contient la configuration entièrement résolue. Les identifiants passent donc par des variables d'environnement, jamais par -backend-config.La commande
jq '{type: .backend.type, config: .backend.config}' .terraform/terraform.tfstate
Mesuré, avec un endpoint personnalisé :{
"type": "s3",
"config": {
"bucket": "tf-state-sonde",
"key": "producer/terraform.tfstate",
"region": "eu-west-3",
"use_path_style": true,
"use_lockfile": true,
"endpoints": { "s3": "http://localhost:4566" }
}
}
Pourquoi c'est le bon contrôle
Avec une configuration partielle, ces valeurs ne figurent dans aucun fichier.tf : elles ont été passées à l'init. Lire le HCL ne dit donc rien.C'est aussi ce qui permet de vérifier que use_lockfile est bien actif, plutôt que de le supposer.Mesuré après initialisation du backend
fichiers terraform.tfstate* dans producer/ : AUCUN
objets dans le bucket : ['producer/terraform.tfstate']
Pourquoi cela compte
La commande souvent recommandée,rm -rf .terraform terraform.tfstate terraform.tfstate.backup, entretient l'idée fausse qu'une copie locale du state subsisterait après migration.Elle ne fait rien sur le state, et supprimer .terraform/ oblige simplement à relancer un init.Ce qui compte vraiment
Activer le versioning du bucket, qui permet de revenir à une version antérieure du state, et vérifier la configuration retenue dans.terraform/terraform.tfstate.Le cas couvert
Un état qui existe, mais auquel il manque l'output demandé : le repli joue, l'apply sort en 0.Le cas NON couvert
Une clé de state inexistante :Error: Unable to find remote state
No stored state was found for the given workspace in the given backend.
Code 1.La conséquence
defaults gère une interface incomplète, pas une stack absente. La parade au « l'amont n'a pas encore été appliqué » reste d'appliquer l'amont, dans le bon ordre.C'est une nuance qui compte en CI, où l'on est tenté de croire que defaults rend la chaîne tolérante à un ordre quelconque.La liste officielle
s3:ListBucket sur le bucket vient en premier : « At a minimum, this must be able to list the path where the state is stored. »Puis s3:GetObject et s3:PutObject sur l'objet de state.Le contre-pied
«s3:DeleteObject is not required on the state file, as Terraform does not delete it. »Terraform écrase le state, il ne le supprime pas. En revanche, la permission de suppression est nécessaire sur l'objet .tflock, qu'il faut bien pouvoir retirer une fois l'opération terminée.Accorder un droit de suppression sur le state est donc une permission de trop.Les arguments qui font fonctionner le backend hors AWS
bucket = "tf-state"
key = "producer/terraform.tfstate"
region = "eu-west-3"
use_path_style = true
use_lockfile = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
skip_region_validation = true
endpoints = { s3 = "http://localhost:4566" }
skip_s3_checksum s'y ajoute pour certaines implémentations, « useful for some S3-Compatible APIs ».La réserve officielle
« Support for S3 Compatible storage providers is offered as best effort. HashiCorp only tests thes3 backend against Amazon S3. »C'est parfait pour apprendre et pour tester hors ligne, et cela n'engage personne en production.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Importer, déplacer, accepter la dérive : reprendre une ressource existante une fois le state partagé.
- Le backend S3 : la référence officielle :
use_lockfile,endpoints, lesskip_*et les permissions. - L'état distant : la référence officielle :
terraform_remote_state,defaultset l'avertissement de sécurité. - Le bloc backend : la référence officielle : l'interdiction des valeurs nommées et la configuration partielle.