Gérer l'état Terraform
Le fichier d'état, son stockage partagé et verrouillé (S3 + DynamoDB), et l'isolation des états par environnement.
Au chapitre précédent, à chaque terraform plan ou terraform apply, Terraform retrouvait sans peine les ressources qu'il avait créées et les mettait à jour. Mais comment savait-il quelles ressources lui appartenaient ? Votre compte AWS peut contenir toutes sortes d'infrastructures, déployées par des moyens variés — certaines à la main, d'autres via Terraform, d'autres encore via la CLI. La réponse tient en un mot : l'état (state). Ce chapitre explore comment Terraform suit l'état de votre infrastructure et l'impact que cela a sur l'organisation des fichiers, l'isolation et le verrouillage (locking) d'un projet.
Qu'est-ce que l'état Terraform ?
À chaque exécution, Terraform consigne les informations sur l'infrastructure qu'il a créée dans un fichier d'état (state file). Par défaut, quand vous lancez Terraform dans le dossier /foo/bar, il crée le fichier /foo/bar/terraform.tfstate. Ce fichier contient un format JSON propriétaire qui enregistre une correspondance (mapping) entre les ressources déclarées dans votre configuration et leur représentation dans le monde réel. Supposons que votre configuration contienne ceci :
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
} Après un terraform apply, voici un extrait du fichier terraform.tfstate (tronqué pour la lisibilité) :
{
"version": 4,
"terraform_version": "0.12.0",
"serial": 1,
"lineage": "1f2087f9-4b3c-1b66-65db-8b78faafc6fb",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "example",
"provider": "provider.aws",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-0c55b159cbfafe1f0",
"availability_zone": "us-east-2c",
"id": "i-00d689a0acc43af0f",
"instance_state": "running",
"instance_type": "t2.micro",
"(...)": "(truncated)"
}
}
]
}
]
} Grâce à ce format, Terraform sait qu'une ressource de type aws_instance nommée example correspond à une instance EC2 portant l'identifiant i-00d689a0acc43af0f dans votre compte AWS. À chaque exécution, il interroge AWS pour récupérer l'état le plus récent de cette instance et le compare à votre configuration afin de déterminer les changements à appliquer. La sortie de plan n'est rien d'autre qu'un diff entre le code présent sur votre machine et l'infrastructure réellement déployée, retrouvée par les identifiants (IDs) stockés dans le fichier d'état.
Attention
Le format du fichier d'état est une API privée, qui change à chaque version et n'est destinée qu'à l'usage interne de Terraform. Vous ne devez jamais éditer le fichier d'état à la main, ni écrire de code qui le lit directement. Si vous devez vraiment le manipuler — ce qui devrait rester rare —, passez par les commandes terraform import ou terraform state.
Pour un projet personnel, conserver l'état dans un unique fichier terraform.tfstate local sur votre machine convient parfaitement. Mais dès lors que vous voulez utiliser Terraform en équipe sur un vrai produit, trois problèmes surgissent : le stockage partagé des fichiers d'état, leur verrouillage, et leur isolation. Le reste du chapitre les traite l'un après l'autre.
Le stockage partagé des fichiers d'état
La technique la plus répandue pour qu'une équipe partage des fichiers est de les placer en gestion de version (Git). Vous devez impérativement y mettre votre code Terraform — mais y stocker l'état est une mauvaise idée, pour trois raisons.
D'abord, l'erreur humaine. Il est trop facile d'oublier de récupérer (pull) les derniers changements avant de lancer Terraform, ou de pousser (push) ses propres changements après. Ce n'est qu'une question de temps avant que quelqu'un n'exécute Terraform avec un état périmé et n'annule ou ne duplique par accident un déploiement antérieur. Ensuite, le verrouillage : la plupart des systèmes de gestion de version n'offrent aucun mécanisme empêchant deux personnes de lancer terraform apply sur le même état au même instant. Enfin, les secrets : toutes les données du fichier d'état sont stockées en clair. Or certaines ressources y inscrivent des données sensibles — la ressource aws_db_instance, par exemple, y consigne le nom d'utilisateur et le mot de passe de la base, en texte brut. Stocker des secrets en clair où que ce soit, y compris en gestion de version, est une mauvaise pratique.
Piège courant
Ne commitez jamais votre fichier terraform.tfstate en gestion de version, et ne l'éditez jamais à la main. Le code Terraform va en version control ; l'état va dans un backend distant, chiffré et verrouillé.
La bonne approche consiste à exploiter le support natif des backends distants (remote backends) de Terraform. Un backend détermine la façon dont Terraform charge et stocke l'état. Le backend par défaut, celui que vous utilisiez jusqu'ici, est le backend local, qui range l'état sur votre disque. Les backends distants permettent au contraire de stocker l'état dans un magasin partagé : Amazon S3, Azure Storage, Google Cloud Storage, ou encore Terraform Cloud, Pro et Enterprise de HashiCorp. Ils résolvent d'un coup les trois problèmes : Terraform charge automatiquement l'état depuis le backend avant chaque plan ou apply et l'y republie après chaque apply (fin de l'erreur humaine) ; la plupart supportent nativement le verrouillage ; et la plupart offrent le chiffrement en transit et au repos ainsi que des contrôles d'accès fins (politiques IAM sur un bucket S3, par exemple).
Créer le bucket S3 et la table DynamoDB
Avec AWS, Amazon S3 (Simple Storage Service) est généralement le meilleur choix de backend distant : c'est un service géré (rien à déployer ni à maintenir), conçu pour une durabilité de 99,999999999 % et une disponibilité de 99,99 %, qui supporte le chiffrement (AES-256 au repos, SSL en transit), le verrouillage via DynamoDB, et le versionnage — chaque révision de l'état est conservée et restaurable. Le tout pour un coût qui tient le plus souvent dans l'offre gratuite (free tier).
Pour activer ce stockage distant, créez d'abord un bucket S3 dans un nouveau dossier, distinct de celui du chapitre précédent :
provider "aws" {
region = "us-east-2"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "terraform-up-and-running-state"
# Empêcher la suppression accidentelle de ce bucket S3
lifecycle {
prevent_destroy = true
}
# Activer le versionnage pour conserver l'historique complet
# des révisions de nos fichiers d'état
versioning {
enabled = true
}
# Activer le chiffrement côté serveur par défaut
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
} Quatre réglages méritent un mot. L'argument bucket est le nom du bucket — il doit être globalement unique parmi tous les clients AWS, vous devrez donc choisir le vôtre. Le bloc lifecycle avec prevent_destroy = true fait échouer toute tentative de suppression (par terraform destroy, par exemple) : une excellente protection pour une ressource aussi critique que celle qui héberge tout votre état. Le bloc versioning crée une nouvelle version à chaque écriture, permettant de revenir en arrière. Le bloc server_side_encryption_configuration garantit que l'état, et les secrets qu'il pourrait contenir, sont toujours chiffrés sur disque.
Il faut ensuite une table DynamoDB pour le verrouillage. DynamoDB est le magasin clé-valeur distribué d'Amazon ; il supporte les lectures fortement cohérentes (strongly consistent reads) et les écritures conditionnelles (conditional writes) — tous les ingrédients d'un système de verrou distribué. Pour l'utiliser avec Terraform, vous devez créer une table dont la clé primaire s'appelle exactement LockID :
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-up-and-running-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
} Lancez terraform init pour télécharger le provider, puis terraform apply pour déployer. Vous obtenez alors un bucket et une table — mais votre état reste local.
Configurer le backend
Pour basculer l'état vers S3, ajoutez un bloc backend à l'intérieur du bloc terraform. C'est de la configuration pour Terraform lui-même :
terraform {
backend "s3" {
# Remplacez par le nom de votre bucket !
bucket = "terraform-up-and-running-state"
key = "global/s3/terraform.tfstate"
region = "us-east-2"
# Remplacez par le nom de votre table DynamoDB !
dynamodb_table = "terraform-up-and-running-locks"
encrypt = true
}
} Le bucket désigne le bucket S3 ; le key est le chemin du fichier d'état à l'intérieur du bucket (on verra plus loin pourquoi global/s3/terraform.tfstate) ; la region est celle du bucket ; dynamodb_table pointe la table de verrouillage ; et encrypt = true ajoute une seconde couche de chiffrement sur disque. Relancez ensuite terraform init — la même commande qui télécharge les providers configure aussi le backend, et elle est idempotente, donc sûre à répéter :
$ terraform init
Initializing the backend...
Acquiring state lock. This may take a few moments...
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 "s3" backend. No existing state was found in the
newly configured "s3" backend. Do you want to copy this state to the new
"s3" backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: Terraform détecte que vous possédez déjà un état local et propose de le copier vers le nouveau backend. Répondez yes. Désormais, il tirera automatiquement l'état le plus récent du bucket avant chaque commande et l'y republiera après. Vous pouvez le vérifier en ajoutant des sorties (outputs) :
output "s3_bucket_arn" {
value = aws_s3_bucket.terraform_state.arn
description = "The ARN of the S3 bucket"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_locks.name
description = "The name of the DynamoDB table"
} Au prochain apply, observez comme Terraform acquiert un verrou avant d'agir et le relâche après :
$ terraform apply
Acquiring state lock. This may take a few moments...
aws_dynamodb_table.terraform_locks: Refreshing state...
aws_s3_bucket.terraform_state: Refreshing state...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Releasing state lock. This may take a few moments...
Outputs:
dynamodb_table_name = terraform-up-and-running-locks
s3_bucket_arn = arn:aws:s3:::terraform-up-and-running-state Astuce
Si quelqu'un détient déjà le verrou, votre apply attendra. Passez -lock-timeout=<TIME> pour patienter jusqu'à un certain délai plutôt que d'échouer aussitôt : terraform apply -lock-timeout=10m attend dix minutes que le verrou se libère.
Les limites des backends
Les backends comportent quelques pièges. Le premier est la situation de l'œuf et la poule (chicken-and-egg) : vous utilisez Terraform pour créer le bucket S3… où Terraform doit stocker son état. La parade est un processus en deux étapes : (1) écrire le code du bucket et de la table DynamoDB, et le déployer avec un backend local ; (2) revenir dans le code, ajouter la configuration du backend distant pointant vers le bucket fraîchement créé, et relancer terraform init pour copier l'état local vers S3. Pour tout supprimer, le même processus se déroule à l'envers : retirer la configuration du backend et réinitialiser pour rapatrier l'état en local, puis terraform destroy. C'est un peu fastidieux, mais la bonne nouvelle est qu'un unique bucket et une unique table se partagent entre tout votre code Terraform : vous ne ferez ce bootstrap qu'une fois (ou une fois par compte AWS).
La seconde limite est plus douloureuse : le bloc backend n'accepte ni variables ni références. Le code suivant ne fonctionnera pas.
# Ceci NE FONCTIONNERA PAS. Les variables sont interdites
# dans une configuration de backend.
terraform {
backend "s3" {
bucket = var.bucket
region = var.region
dynamodb_table = var.dynamodb_table
key = "example/terraform.tfstate"
encrypt = true
}
} Conséquence : vous devez copier-coller manuellement le nom du bucket, la région, le nom de la table dans chacun de vos modules — et veiller, surtout, à donner une valeur key unique à chaque module pour ne pas écraser l'état d'un autre. Beaucoup de copier-coller manuels, donc beaucoup d'erreurs possibles.
La seule solution native est la configuration partielle (partial configuration) : on omet certains paramètres du bloc backend dans le code et on les fournit via des arguments -backend-config au moment du terraform init. On extrait par exemple les arguments répétés dans un fichier backend.hcl :
# backend.hcl
bucket = "terraform-up-and-running-state"
region = "us-east-2"
dynamodb_table = "terraform-up-and-running-locks"
encrypt = true Seul le key reste dans le code, puisqu'il doit différer pour chaque module :
# Configuration partielle. Les autres réglages (bucket, region...)
# seront passés depuis un fichier via -backend-config à 'terraform init'
terraform {
backend "s3" {
key = "example/terraform.tfstate"
}
} On assemble le tout au moment de l'initialisation :
$ terraform init -backend-config=backend.hcl Terraform fusionne la configuration partielle du fichier avec celle du code pour produire la configuration complète. Une autre option est Terragrunt, un outil open source qui comble certaines lacunes de Terraform : il garde la configuration du backend DRY (Don't Repeat Yourself) en définissant les réglages de base une seule fois et en positionnant automatiquement key sur le chemin relatif du module.
Isoler les fichiers d'état
Avec un backend distant et le verrouillage, la collaboration n'est plus un problème. Reste l'isolation. La tentation initiale est de tout définir dans un seul ensemble de fichiers Terraform, dans un seul dossier — mais alors tout l'état réside dans un unique fichier, et une erreur n'importe où peut tout casser. En déployant une nouvelle version en staging, vous pourriez casser la production ; pire, une corruption de l'état (faute de verrouillage, ou par un rare bug) mettrait à terre toute votre infrastructure, dans tous les environnements.
L'intérêt même d'avoir des environnements séparés est qu'ils soient isolés. De même qu'un navire possède des cloisons étanches empêchant une voie d'eau d'inonder toute la coque, votre conception Terraform doit comporter ses propres « cloisons » (bulkheads), pour qu'un incident dans un environnement reste confiné à celui-ci.
AVANT — un seul état pour tout APRÈS — des « cloisons »
+-----------------------------+ +---------+ +---------+ +---------+
| stage + prod + global | | stage | | prod | | global |
| (un unique tfstate) | | tfstate | | tfstate | | tfstate |
+-----------------------------+ +---------+ +---------+ +---------+
une erreur n'importe où un incident reste confiné
-> tout casse -> à un seul environnement Deux façons d'isoler les états s'offrent à vous : par espaces de travail (workspaces), utile pour des tests rapides et isolés sur une même configuration ; et par disposition de fichiers (file layout), pour les cas de production exigeant une séparation forte entre environnements.
Isolation par workspaces
Les workspaces Terraform stockent l'état dans plusieurs espaces nommés et distincts. Terraform démarre sur un workspace appelé default ; sans action explicite, vous y restez. Les commandes terraform workspace créent ou changent d'espace. Expérimentons sur une instance EC2, avec un backend dont le key vaut workspaces-example/terraform.tfstate. Après un premier apply dans default, créons un nouvel espace :
$ terraform workspace new example1
Created and switched to workspace "example1"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration. Désormais, terraform plan veut créer une instance EC2 entièrement neuve : les états de chaque workspace sont isolés, et comme vous êtes dans example1, Terraform ne voit pas l'instance du workspace default. Sous le capot, basculer de workspace revient à changer le chemin où l'état est rangé : Terraform crée dans le bucket un dossier env:, puis un sous-dossier par espace. Vous trouverez ainsi example1/workspaces-example/terraform.tfstate et example2/workspaces-example/terraform.tfstate.
Vous pouvez même faire varier le comportement d'un module selon l'espace courant, lu via l'expression terraform.workspace :
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = terraform.workspace == "default" ? "t2.medium" : "t2.micro"
} Ce code emploie la syntaxe ternaire pour donner un type t2.medium dans default et t2.micro ailleurs — pratique pour économiser en expérimentation. Les workspaces sont donc parfaits pour faire monter et descendre rapidement des copies du même code. Mais ils souffrent de défauts rédhibitoires pour isoler de vrais environnements. Les états de tous les workspaces vivent dans le même backend (même bucket), donc avec la même authentification et les mêmes contrôles d'accès — impossible de cloisonner staging et production. Les workspaces sont en outre invisibles dans le code et le terminal tant qu'on ne lance pas une commande workspace : un module déployé dans un espace ressemble en tout point à un module déployé dans dix. Conjuguées, ces deux faiblesses rendent les workspaces sujets aux erreurs : on oublie facilement dans quel espace on se trouve, et l'on risque un terraform destroy dans « production » au lieu de « staging », sans aucune barrière d'authentification pour s'en prémunir.
À retenir
Les workspaces conviennent aux expérimentations sur une infrastructure déjà déployée (tester un refactoring sans toucher à l'existant). Pour une vraie isolation entre environnements, préférez la disposition de fichiers. Pensez à nettoyer vos instances de test : terraform workspace select <name> puis terraform destroy dans chaque espace.
Isolation par disposition de fichiers
Pour une isolation complète, deux principes : placer la configuration de chaque environnement dans un dossier séparé (un dossier stage, un dossier prod…), et configurer un backend différent par environnement, avec des mécanismes d'authentification et des contrôles d'accès distincts (idéalement, chaque environnement dans un compte AWS séparé, avec son propre bucket). Les dossiers séparés rendent évident vers quel environnement on déploie, et les états séparés rendent bien plus improbable qu'une bourde dans l'un affecte l'autre.
L'auteur recommande de pousser l'isolation au-delà des environnements, jusqu'au niveau des composants (components) — un ensemble cohérent de ressources que l'on déploie ensemble. La topologie réseau (votre VPC et ses sous-réseaux, règles de routage, VPN, ACL) ne change qu'une fois tous les quelques mois ; un serveur web, plusieurs fois par jour. Gérer les deux dans la même configuration mettrait toute votre topologie réseau en danger de rupture plusieurs fois par jour, pour une simple faute de frappe. D'où des dossiers — et donc des états — séparés par environnement et par composant. Voici la disposition typique :
+-- stage/ # préproduction (tests)
| +-- vpc/ # topologie réseau de l'environnement
| +-- services/ # apps / microservices
| | +-- webserver-cluster/
| +-- data-stores/ # bases de données
| +-- mysql/
+-- prod/ # production (apps exposées aux utilisateurs)
| +-- vpc/
| +-- services/
| +-- data-stores/
+-- mgmt/ # outillage DevOps (bastion, Jenkins...)
+-- global/ # ressources transverses (S3, IAM)
+-- s3/ Au sein de chaque composant, on retrouve les fichiers de configuration, organisés selon une convention de nommage : variables.tf (variables d'entrée), outputs.tf (variables de sortie) et main.tf (les ressources). Terraform ne charge que les fichiers .tf du dossier courant et se moque des noms — mais vos collègues, eux, y tiennent : une convention prévisible rend le code navigable. Concrètement, le bucket S3 de ce chapitre va dans global/s3, et le cluster de serveurs web du chapitre 2 dans stage/services/webserver-cluster, avec un key de backend identique au chemin du dossier — stage/services/webserver-cluster/terraform.tfstate. On obtient ainsi une correspondance 1:1 entre l'arborescence du code en version control et celle des états dans S3.
Cette disposition a un revers : ce qui empêche de tout détruire en une commande empêche aussi de tout créer en une commande. Avec des composants dans des dossiers séparés, il faut lancer terraform apply dans chacun (Terragrunt peut automatiser cela avec sa commande apply-all). Autre difficulté : elle complique les dépendances entre ressources. Si le code de l'app et celui de la base vivaient dans les mêmes fichiers, l'app accéderait directement aux attributs de la base via une référence (par exemple l'adresse via aws_db_instance.foo.address). Dans des dossiers distincts, c'est impossible — d'où la data source terraform_remote_state.
La data source terraform_remote_state
La data source terraform_remote_state récupère, de manière strictement en lecture seule, le fichier d'état produit par un autre ensemble de configurations Terraform. Imaginez que votre cluster web doive dialoguer avec une base de données MySQL, déployée sur Amazon RDS (Relational Database Service). Vous ne voulez pas définir cette base dans les mêmes fichiers que le cluster web, déployé bien plus souvent, au risque de casser la base à chaque mise à jour. Créez donc un dossier stage/data-stores/mysql avec sa propre ressource :
provider "aws" {
region = "us-east-2"
}
resource "aws_db_instance" "example" {
identifier_prefix = "terraform-up-and-running"
engine = "mysql"
allocated_storage = 10
instance_class = "db.t2.micro"
name = "example_database"
username = "admin"
# Comment fixer le mot de passe ?
password = var.db_password
} Le mot de passe maître est un secret : ne le mettez surtout pas en clair dans le code. Deux approches valent mieux. La première est de le lire depuis un magasin de secrets via une data source — par exemple AWS Secrets Manager et aws_secretsmanager_secret_version, ou encore le Parameter Store, AWS KMS, Google Cloud KMS, Azure Key Vault, HashiCorp Vault. La seconde est de le gérer hors de Terraform (gestionnaire de mots de passe, trousseau) et de l'injecter via une variable d'environnement. Déclarez la variable sans valeur par défaut — c'est intentionnel :
variable "db_password" {
description = "The password for the database"
type = string
} Pour chaque variable foo, Terraform lit la variable d'environnement TF_VAR_foo. Notez l'espace avant la commande export, qui évite d'enregistrer le secret dans l'historique du shell, ou mieux, lisez-le depuis un magasin en ligne de commande comme pass :
$ export TF_VAR_db_password=$(pass database-password)
$ terraform apply Piège courant
Quelle que soit la manière dont vous lisez le secret, dès que vous le passez en argument à une ressource comme aws_db_instance, il est stocké en clair dans le fichier d'état. C'est une faiblesse connue de Terraform, sans solution efficace. Soyez donc paranoïaque : chiffrez toujours vos états et verrouillez l'accès à votre bucket S3 par des permissions IAM.
Configurez la base pour stocker son état dans stage/data-stores/mysql/terraform.tfstate, puis exposez l'adresse et le port en sorties :
output "address" {
value = aws_db_instance.example.address
description = "Connect to the database at this endpoint"
}
output "port" {
value = aws_db_instance.example.port
description = "The port the database is listening on"
} Ces sorties sont elles aussi stockées dans l'état de la base. Le code du cluster web peut alors les lire en ajoutant, dans stage/services/webserver-cluster/main.tf, la data source pointant vers l'état de la base :
data "terraform_remote_state" "db" {
backend = "s3"
config = {
bucket = "(YOUR_BUCKET_NAME)"
key = "stage/data-stores/mysql/terraform.tfstate"
region = "us-east-2"
}
} stage/data-stores/mysql/ stage/services/webserver-cluster/
│ ▲
ÉCRIT son état LIT l'état (lecture seule)
(outputs : address, port) via terraform_remote_state
│ │
└──────────► S3 (même bucket) ──────┘ Comme toute data source, les données renvoyées par terraform_remote_state sont en lecture seule : rien, dans le code du cluster web, ne peut modifier cet état — vous tirez les données de la base sans aucun risque pour la base elle-même. On lit les sorties via une référence de la forme data.terraform_remote_state.<NAME>.outputs.<ATTRIBUTE>. Voici, par exemple, comment exposer l'adresse et le port dans le User Data des instances du cluster :
user_data = <<EOF
#!/bin/bash
echo "Hello, World" >> index.html
echo "${data.terraform_remote_state.db.outputs.address}" >> index.html
echo "${data.terraform_remote_state.db.outputs.port}" >> index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF À mesure qu'il s'allonge, ce script user_data défini en ligne devient pénible : imbriquer un langage (Bash) dans un autre (Terraform) complique la maintenance des deux. Mieux vaut externaliser le script grâce à la fonction intégrée file et à la data source template_file, qui rend un gabarit (template) en y injectant une carte de variables (vars) :
data "template_file" "user_data" {
template = file("user-data.sh")
vars = {
server_port = var.server_port
db_address = data.terraform_remote_state.db.outputs.address
db_port = data.terraform_remote_state.db.outputs.port
}
} Le script user-data.sh accède alors aux variables par la syntaxe d'interpolation standard, sans préfixe (server_port et non var.server_port), puisqu'elles proviennent de la carte vars. Il suffit ensuite de pointer le user_data de la aws_launch_configuration vers l'attribut rendered de la data source. Le code gagne nettement en clarté — et, bénéfice supplémentaire, le script extrait dans son propre fichier devient testable : un test peut renseigner les variables interpolées via des variables d'environnement, la syntaxe Bash de lecture étant la même que l'interpolation Terraform.
Note
L'effort consacré à l'isolation, au verrouillage et à l'état tient au fait que l'infrastructure as code (IaC) a d'autres compromis que le code applicatif ordinaire. Dans une app classique, un bug ne casse qu'une petite partie d'une seule application ; dans le code qui pilote votre infrastructure, un bug peut casser toutes vos apps, vos magasins de données et votre topologie réseau d'un coup. D'où la nécessité de bien plus de « mécanismes de sûreté » qu'en programmation ordinaire.
Le grief habituel contre cette disposition de fichiers est qu'elle engendre de la duplication : pour faire tourner le cluster web en staging et en production, faut-il copier-coller le code entre stage/services/webserver-cluster et prod/services/webserver-cluster ? Non. La réponse tient dans les modules Terraform, sujet du prochain chapitre.
À retenir
- L'état (state) est la correspondance entre votre code et les ressources réelles : un fichier JSON (
terraform.tfstate) qui permet àplande calculer lediffentre code et infrastructure déployée. - Ne l'éditez jamais à la main (c'est une API privée — passez par
terraform importouterraform state) et ne le commitez jamais : erreur humaine, absence de verrouillage et secrets stockés en clair. - Pour le travail en équipe, utilisez un backend distant : S3 pour stocker l'état (chiffré, versionné, durable) et DynamoDB pour le verrouillage (locking), qui prévient les conditions de course.
- Le backend impose deux contraintes : un amorçage (bootstrap) en deux étapes (l'œuf et la poule), et l'interdiction des variables dans le bloc
backend— contournée par la configuration partielle (-backend-config) ou par Terragrunt. - Isolez les états avec des « cloisons » : les workspaces servent aux expérimentations rapides, mais partagent backend et authentification ; pour une vraie séparation entre environnements, préférez la disposition de fichiers (un dossier et un état par environnement et par composant).
- La data source
terraform_remote_statepartage les sorties (outputs) d'un composant à un autre en lecture seule (la base écrit sonaddress/port, le cluster web les lit) — sans aucun risque de modifier l'état distant. - Externalisez les scripts longs (
file+template_file) plutôt que de les écrire en ligne : code plus lisible, et script testable indépendamment.