
Un state local suffit tant que vous travaillez seul. Dès qu’une deuxième personne lance terraform apply depuis son poste, vous n’avez plus une vérité unique : chacun possède son propre fichier terraform.tfstate, les plans divergent et les risques d’écrasement augmentent. Un backend S3 existe précisément pour résoudre ce problème en centralisant le state dans AWS, tandis que terraform_remote_state permet à une autre configuration de lire les outputs publiés par cette première stack.
Dans ce guide, vous allez mettre en place un workflow simple et réaliste en trois blocs : un projet de bootstrap qui prépare le bucket S3 et la table de verrouillage, une configuration producer qui stocke son state dans ce backend distant, puis une configuration consumer qui lit ses outputs. Vous comprendrez surtout pourquoi ces trois blocs sont séparés : le backend Terraform doit être configuré avec des valeurs déjà connues, il ne peut pas dépendre d’une ressource créée dans la même configuration.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer un backend S3 dédié avec versioning, chiffrement et blocage de l’accès public
- Ajouter un verrouillage avec DynamoDB pour éviter les accès concurrents au state
- Initialiser une configuration Terraform directement sur un backend S3
- Partager des outputs entre deux configurations avec
terraform_remote_state - Comprendre l’enchaînement bootstrap → producer → consumer sans copier-coller aveugle
Pourquoi ce workflow existe
Section intitulée « Pourquoi ce workflow existe »Le state Terraform mémorise ce que Terraform a créé. Sans lui, Terraform ne peut pas comparer votre code avec la réalité AWS. En local, ce state reste un simple fichier sur votre machine. C’est pratique pour apprendre, mais insuffisant dès que plusieurs personnes interviennent.
Un backend S3 joue le rôle d’un classeur partagé : tout le monde lit et met à jour le même état central. La table DynamoDB sert de verrou, comme une pancarte “occupation en cours” posée sur le classeur pendant un apply. Enfin, terraform_remote_state agit comme un lecteur autorisé : il ne modifie rien, il vient seulement consulter les outputs publiés par une autre configuration.
Architecture du guide
Section intitulée « Architecture du guide »Vous allez manipuler trois répertoires distincts :
| Répertoire | Rôle | Pourquoi il existe |
|---|---|---|
bootstrap/ | Crée le bucket S3 et la table DynamoDB | Le backend doit exister avant d’être utilisé |
producer/ | Stocke son state dans S3 et publie des outputs | C’est la configuration principale |
consumer/ | Lit les outputs du producer | Simule une seconde équipe ou un second projet |
Prérequis
Section intitulée « Prérequis »- Terraform >= 1.11 installé
- AWS CLI configuré avec un compte autorisé à créer un bucket S3 et une table DynamoDB
- Compréhension de base de
resource,dataetoutput
Préparation
Section intitulée « Préparation »Créez une arborescence claire pour ne pas mélanger les trois configurations :
mkdir -p ~/terraform-aws-state/{bootstrap,producer,consumer}cd ~/terraform-aws-state/bootstrapCette séparation est importante : elle vous aide à visualiser qu’un backend Terraform n’est pas “magique”. C’est une infrastructure comme une autre, avec son propre cycle de vie.
Étape 1 — Créer l’infrastructure du backend
Section intitulée « Étape 1 — Créer l’infrastructure du backend »Commencez par la configuration bootstrap. Son seul rôle est de créer le support du state distant : un bucket S3 pour stocker le state et une table DynamoDB pour empêcher deux apply simultanés.
Créez versions.tf :
terraform { required_version = ">= 1.11.0"
required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
provider "aws" { region = var.aws_region}Créez variables.tf :
variable "aws_region" { description = "AWS region" type = string default = "us-east-1"}
variable "project_slug" { description = "Prefix used to name backend resources" type = string default = "terraform-aws-state-demo"}Créez main.tf :
data "aws_caller_identity" "current" {}
locals { backend_bucket_name = "${var.project_slug}-${data.aws_caller_identity.current.account_id}" lock_table_name = "${var.project_slug}-locks"}
resource "aws_s3_bucket" "terraform_state" { bucket = local.backend_bucket_name
tags = { Name = local.backend_bucket_name ManagedBy = "terraform" Purpose = "terraform-backend" }}
resource "aws_s3_bucket_versioning" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id
versioning_configuration { status = "Enabled" }}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id
rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } }}
resource "aws_s3_bucket_public_access_block" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}
resource "aws_dynamodb_table" "terraform_locks" { name = local.lock_table_name billing_mode = "PAY_PER_REQUEST" hash_key = "LockID"
attribute { name = "LockID" type = "S" }
tags = { Name = local.lock_table_name ManagedBy = "terraform" Purpose = "terraform-locks" }}Créez outputs.tf :
output "backend_bucket_name" { description = "S3 bucket name used by the Terraform backend" value = aws_s3_bucket.terraform_state.bucket}
output "lock_table_name" { description = "DynamoDB table used to lock Terraform state" value = aws_dynamodb_table.terraform_locks.name}
output "aws_region" { description = "AWS region used by the backend" value = var.aws_region}Créez terraform.tfvars :
aws_region = "us-east-1"project_slug = "terraform-aws-state-demo"Pourquoi ces ressources sont séparées
Section intitulée « Pourquoi ces ressources sont séparées »Le bloc backend "s3" de Terraform est évalué avant que Terraform crée des ressources. C’est la raison pour laquelle vous ne pouvez pas écrire : “crée le bucket puis utilise-le comme backend” dans une seule configuration. Le bootstrap prépare l’infrastructure d’abord, puis les autres projets l’utilisent.
Vérification du bootstrap
Section intitulée « Vérification du bootstrap »-
Initialisez et validez la configuration :
Fenêtre de terminal terraform initterraform validateterraform plan -
Appliquez la configuration :
Fenêtre de terminal terraform apply -auto-approve -
Notez les outputs affichés à la fin. Vous en aurez besoin dans les étapes suivantes :
backend_bucket_name = "terraform-aws-state-demo-123456789012"lock_table_name = "terraform-aws-state-demo-locks"aws_region = "us-east-1"
Étape 2 — Configurer le producer sur le backend S3
Section intitulée « Étape 2 — Configurer le producer sur le backend S3 »Passez dans le répertoire producer/. Cette configuration va écrire son state directement dans S3 et publier quelques outputs que le consumer pourra relire.
cd ~/terraform-aws-state/producerCréez versions.tf :
terraform { required_version = ">= 1.11.0"
backend "s3" {}
required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
provider "aws" { region = var.aws_region}Ce bloc backend "s3" {} reste volontairement vide dans le code. Vous fournirez les valeurs réelles au moment du terraform init. Cette approche évite de figer des noms de bucket dans le dépôt et vous permet de réutiliser le même exemple dans plusieurs comptes AWS.
Créez variables.tf :
variable "aws_region" { description = "AWS region" type = string default = "us-east-1"}
variable "application_name" { description = "Logical application name published as an output" type = string default = "payments-api"}
variable "environment" { description = "Environment published as an output" type = string default = "lab"}Créez main.tf :
data "aws_caller_identity" "current" {}
locals { naming_prefix = "${var.application_name}-${var.environment}-${data.aws_caller_identity.current.account_id}"}Créez outputs.tf :
output "application_name" { description = "Application name shared with downstream stacks" value = var.application_name}
output "environment" { description = "Environment shared with downstream stacks" value = var.environment}
output "naming_prefix" { description = "Computed prefix built by the producer" value = local.naming_prefix}
output "account_id" { description = "AWS account ID seen by the producer" value = data.aws_caller_identity.current.account_id}Créez terraform.tfvars :
aws_region = "us-east-1"application_name = "payments-api"environment = "lab"Initialiser le backend S3
Section intitulée « Initialiser le backend S3 »Remplacez les valeurs ci-dessous par les outputs du bootstrap :
terraform init \ -backend-config="bucket=terraform-aws-state-demo-123456789012" \ -backend-config="key=producer/terraform.tfstate" \ -backend-config="region=us-east-1" \ -backend-config="dynamodb_table=terraform-aws-state-demo-locks" \ -backend-config="encrypt=true"Puis validez et appliquez :
terraform validateterraform apply -auto-approveComment savoir que le state est bien distant
Section intitulée « Comment savoir que le state est bien distant »Si tout est correct, Terraform doit mentionner le backend S3 dès l’initialisation. Ensuite, la commande suivante doit lister un fichier de state dans votre bucket :
aws s3 ls s3://terraform-aws-state-demo-123456789012/producer/Vous devez y voir terraform.tfstate. C’est la preuve que le state n’est plus seulement local.
Étape 3 — Lire les outputs avec terraform_remote_state
Section intitulée « Étape 3 — Lire les outputs avec terraform_remote_state »Le répertoire consumer/ simule une seconde équipe ou une seconde stack Terraform. Son objectif n’est pas de recréer les mêmes données, mais de consommer les outputs déjà publiés par le producer.
cd ~/terraform-aws-state/consumerCréez versions.tf :
terraform { required_version = ">= 1.11.0"
required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
provider "aws" { region = var.aws_region}Créez variables.tf :
variable "aws_region" { description = "AWS region" type = string default = "us-east-1"}
variable "backend_bucket_name" { description = "S3 bucket where the producer state is stored" type = string}
variable "producer_state_key" { description = "Object key of the producer state in S3" type = string default = "producer/terraform.tfstate"}Créez main.tf :
data "terraform_remote_state" "producer" { backend = "s3"
config = { bucket = var.backend_bucket_name key = var.producer_state_key region = var.aws_region }}Cette data source ne crée aucune ressource. Elle agit comme un lecteur de state : Terraform ouvre le fichier stocké dans S3 et expose ses outputs sous la forme data.terraform_remote_state.producer.outputs.<nom>.
Créez outputs.tf :
output "producer_application_name" { description = "Application name published by the producer" value = data.terraform_remote_state.producer.outputs.application_name}
output "producer_environment" { description = "Environment published by the producer" value = data.terraform_remote_state.producer.outputs.environment}
output "producer_naming_prefix" { description = "Naming prefix computed by the producer" value = data.terraform_remote_state.producer.outputs.naming_prefix}
output "producer_account_id" { description = "AWS account ID seen by the producer" value = data.terraform_remote_state.producer.outputs.account_id}Créez terraform.tfvars :
aws_region = "us-east-1"backend_bucket_name = "terraform-aws-state-demo-123456789012"producer_state_key = "producer/terraform.tfstate"Vérification du consumer
Section intitulée « Vérification du consumer »terraform initterraform validateterraform apply -auto-approveSortie attendue :
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:producer_account_id = "123456789012"producer_application_name = "payments-api"producer_environment = "lab"producer_naming_prefix = "payments-api-lab-123456789012"Le point important est le suivant : le consumer ne sait pas comment le producer calcule naming_prefix. Il consomme seulement une valeur déjà publiée. C’est précisément ce qui rend terraform_remote_state utile pour relier deux stacks sans dupliquer leur logique interne.
Anatomie
Section intitulée « Anatomie »Backend S3 vs terraform_remote_state
Section intitulée « Backend S3 vs terraform_remote_state »| Élément | Rôle | Exemple |
|---|---|---|
| Backend S3 | Emplacement où Terraform stocke son propre state | backend "s3" {} |
terraform_remote_state | Lecture d’un state déjà existant | data "terraform_remote_state" "producer" |
La confusion la plus fréquente vient de là : un backend sert à écrire et maintenir votre state courant ; terraform_remote_state sert à lire les outputs d’un autre state.
Pourquoi le verrouillage compte
Section intitulée « Pourquoi le verrouillage compte »Sans verrouillage, deux personnes peuvent lancer terraform apply au même moment et écrire dans le même state. Le résultat ressemble à deux personnes qui modifient le même document partagé sans coordination. La table DynamoDB évite ce scénario en autorisant un seul écrivain à la fois.
Bonnes pratiques
Section intitulée « Bonnes pratiques »1. Dédier un backend par projet ou domaine
Section intitulée « 1. Dédier un backend par projet ou domaine »network/terraform.tfstatesecurity/terraform.tfstateapplications/payments-api/terraform.tfstateNe mettez pas tous les projets dans la même clé S3. Un chemin clair vous aide à savoir qui possède quoi.
2. Chiffrer et versionner le bucket
Section intitulée « 2. Chiffrer et versionner le bucket »Le chiffrement protège les données stockées. Le versioning permet de revenir en arrière si un state a été écrasé ou corrompu par erreur.
3. Publier seulement des outputs utiles
Section intitulée « 3. Publier seulement des outputs utiles »Un output doit exposer une donnée utile à une autre stack : un nom, un identifiant, une URL, un préfixe de nommage. Ne publiez pas tout “au cas où”.
4. Ne partagez pas les secrets via terraform_remote_state
Section intitulée « 4. Ne partagez pas les secrets via terraform_remote_state »Le state peut contenir des informations sensibles. Si une valeur doit rester secrète, utilisez plutôt un service dédié comme AWS Secrets Manager ou SSM Parameter Store.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
Error: Failed to get existing workspaces | Bucket, clé ou région incorrects | Vérifier les -backend-config du producer |
Error acquiring the state lock | Un autre apply détient déjà le verrou | Attendre la fin de l’opération en cours |
No stored state was found for the given workspace | Le producer n’a pas encore écrit son state dans S3 | Exécuter terraform apply dans producer/ |
Unsupported attribute sur terraform_remote_state | L’output demandé n’existe pas côté producer | Vérifier le nom exact dans outputs.tf du producer |
BucketAlreadyExists | Le nom de bucket choisi n’est pas unique globalement | Changer project_slug puis recréer le bootstrap |
Nettoyage
Section intitulée « Nettoyage »Commencez toujours par les consommateurs, puis remontez vers le backend :
# 1. Détruire le consumercd ~/terraform-aws-state/consumerterraform destroy -auto-approverm -rf .terraform .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup
# 2. Détruire le producercd ~/terraform-aws-state/producerterraform destroy -auto-approverm -rf .terraform .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup
# 3. Détruire le bootstrap en derniercd ~/terraform-aws-state/bootstrapterraform destroy -auto-approverm -rf .terraform .terraform.lock.hcl terraform.tfstate terraform.tfstate.backupLe backend doit disparaître en dernier, sinon les autres configurations ne pourront plus lire ou mettre à jour leur state.
À retenir
Section intitulée « À retenir »- Le backend S3 stocke votre state courant dans AWS ; ce n’est pas la même chose que
terraform_remote_state. - Le bootstrap existe parce qu’un backend doit être déjà créé avant d’être utilisé.
- DynamoDB ajoute un verrou pour éviter deux écritures simultanées sur le même state.
terraform_remote_statesert à relire des outputs publiés par une autre stack, pas à partager toute sa logique interne.- Ne réutilisez jamais un backend de production pour un exercice ou un lab.
- Le nettoyage se fait dans l’ordre inverse : consumer, producer, puis backend.