Aller au contenu
Infrastructure as Code medium

Terraform AWS, Backend S3 et terraform_remote_state

35 min de lecture

logo terraform

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.

  • Créer le bucket de state, dans un bootstrap séparé
  • Pourquoi un bloc backend n'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
  • 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 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.

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 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" {}
}
prod.s3.tfbackend
bucket = "mon-org-terraform-state"
key = "producer/terraform.tfstate"
region = "eu-west-3"
use_lockfile = true
encrypt = true
Fenêtre de terminal
terraform init -backend-config=prod.s3.tfbackend

Un même dossier sert alors plusieurs environnements, en ne changeant que le fichier passé à l'init.

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.

Fenêtre de terminal
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" }
}
}

Un aws s3 ls prouverait qu'un objet existe ; il ne dit pas quelle configuration Terraform a réellement retenue.

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 :

Fenêtre de terminal
terraform plan -lock-timeout=0s
Error: Error acquiring the state lock
Error message: operation error S3: PutObject, https response error

Code 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.

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.

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/ : AUCUN
objets dans le bucket : ['producer/terraform.tfstate']

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.

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.

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.

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.

SymptômeCause probableCorrection
Error: Variables not allowedune variable dans le bloc backendconfiguration partielle plus -backend-config
deux applies concurrents s'écrasentuse_lockfile absent : le verrou est un opt-inl'ajouter, puis le prouver avec un .tflock déposé
Error acquiring the state lockun verrou est détenu, ou orphelin-lock-timeout en CI ; sinon retirer l'objet .tflock
Error: Backend configuration changedbackend modifié sans réinitialiserinit -migrate-state pour emporter l'état, -reconfigure pour l'abandonner
Unable to find remote statela stack amont n'a pas été appliquéel'appliquer ; defaults ne couvre pas ce cas
un identifiant traîne dans .terraform/passé via -backend-configle passer par variables d'environnement
le backend ne répond pas hors AWSendpoints, use_path_style ou un skip_* manquantcompléter le jeu d'arguments S3 compatible

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.

  • Le bucket de state se crée dans un bootstrap séparé, au state local assumé.
  • Un bloc backend n'accepte aucune valeur nommée : Error: Variables not allowed, code 1.
  • La configuration partielle en fichier *.s3.tfbackend est la réponse, et la forme KEY=VALUE est déconseillée pour un secret.
  • use_lockfile = true est un opt-in : sans lui, un .tflock présent est purement ignoré.
  • Le verrouillage natif date de Terraform 1.10 ; DynamoDB est déprécié.
  • .terraform/terraform.tfstate porte 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 rm habituel est à vide.
  • defaults comble un output manquant, jamais un état absent.
  • s3:ListBucket est requis ; s3:DeleteObject ne l'est pas sur le state.
  • Hors AWS, tout tient à endpoints, use_path_style et aux skip_*, avec un support « best effort ».

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/.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn