Aller au contenu
Infrastructure as Code medium

Cheatsheet Professional : commandes Terraform avancées

20 min de lecture

logo terraform

Référence centrée sur la vitesse d'exécution en lab. L'examen Professional est 100% pratique : vous écrivez du HCL, vous exécutez des commandes et vous résolvez des problèmes réels. Cette page couvre les commandes avancées organisées par objectif d'examen.

L'examen formule ses consignes en termes de résultat attendu, jamais de nom de commande : ce tableau fait la traduction dans ce sens. Deux entrées demandent une attention particulière, moved {} et terraform state mv répondent au même besoin de renommage, mais le premier est déclaratif et versionné avec le code, le second modifie le state en direct. Savoir lequel choisir est souvent le véritable objet de la tâche, pas la syntaxe elle-même.

BesoinCommande
Importer une ressource existanteterraform import <addr> <id>
Renommer une ressource (déclaratif)Bloc moved {}
Forcer la recréationterraform apply -replace=<addr>
Lister les ressources du stateterraform state list
Inspecter une ressourceterraform state show <addr>
Déplacer dans le stateterraform state mv <old> <new>
Retirer du state sans détruireterraform state rm <addr>
Exécuter les teststerraform test
Sauvegarder un planterraform plan -out=tfplan
Appliquer un plan sauvegardéterraform apply tfplan
Réconcilier le state (changements hors Terraform)terraform plan -refresh-only
Mettre à jour les providersterraform init -upgrade
Debug avancéTF_LOG=TRACE terraform plan
Voir les providers requisterraform providers
Voir les versions Terraform + providers installésterraform version
Mettre à jour / préremplir le lock fileterraform providers lock
Lire le lock fileOuvrir .terraform.lock.hcl
Afficher un output sensibleterraform output -raw <name>

Cet objectif porte sur des ressources qui existent déjà et qu'il faut faire entrer, sortir ou déplacer dans le state sans les détruire. C'est la partie la plus risquée du lab : une manipulation de state ratée ne se rattrape pas par un simple apply. La ligne de conduite reste constante d'un exercice à l'autre, privilégier les blocs déclaratifs (import {}, moved {}) aux commandes terraform state, qui n'écrivent rien dans le code et ne laissent donc aucune trace pour le prochain lecteur.

L'import associe une ressource réelle à une adresse du code : il remplit le state mais n'écrit pas le HCL, qui doit exister au préalable. C'est précisément ce que corrige l'option -generate-config-out, qui produit le fichier de configuration correspondant.

Fenêtre de terminal
# Import classique (CLI)
terraform import aws_instance.web i-0abc123def456
# Import avec adresse de module
terraform import module.vpc.aws_vpc.main vpc-0abc123

Bloc import (déclaratif, Terraform 1.5+) :

import {
to = aws_instance.web
id = "i-0abc123def456"
}
Fenêtre de terminal
# Générer le code HCL automatiquement
terraform plan -generate-config-out=generated_resources.tf

moved {} déclare qu'une adresse a changé de nom, de conteneur ou de module. Le bloc reste dans le code une fois appliqué : il ne se supprime qu'une fois toutes les configurations appelantes migrées, sinon elles recréeraient la ressource.

# Renommer une ressource
moved {
from = aws_instance.web
to = aws_instance.application
}
# Déplacer dans un module
moved {
from = aws_instance.web
to = module.compute.aws_instance.main
}
# Renommer un module
moved {
from = module.old_name
to = module.new_name
}

Vérification :

Fenêtre de terminal
# Le plan doit montrer "moved" sans create/destroy
terraform plan
# → aws_instance.application has moved from aws_instance.web

Ces sous-commandes agissent sur le state sans passer par le code. state list et state show sont en lecture seule et servent au diagnostic ; mv, rm et push écrivent directement dans le fichier d'état. state rm reste le seul moyen d'oublier une ressource sans la détruire tant que l'examen porte sur Terraform 1.6.

Fenêtre de terminal
# Lister toutes les ressources
terraform state list
# Filtrer par adresse
terraform state list module.vpc
# Inspecter une ressource en détail
terraform state show aws_instance.web
# Déplacer (opération manuelle ponctuelle)
terraform state mv aws_instance.web aws_instance.application
terraform state mv aws_instance.web module.compute.aws_instance.main
# Retirer du state (la ressource reste dans AWS)
terraform state rm aws_instance.legacy
# Télécharger le state distant en JSON
terraform state pull
# Envoyer un state local vers le backend
terraform state push terraform.tfstate

-replace programme la destruction puis la recréation d'une ressource précise, sans toucher aux autres. Il a remplacé terraform taint : la décision n'est plus persistée dans le state, elle est prise au moment du plan et visible dedans avant validation.

Fenêtre de terminal
# Forcer la destruction + recréation d'une ressource
terraform apply -replace=aws_instance.web
# Avec un plan sauvegardé
terraform plan -replace=aws_instance.web -out=tfplan
terraform apply tfplan

Ces options modifient le périmètre ou le mode d'exécution. -target restreint le graphe et est explicitement déconseillé hors dépannage, car le plan obtenu ne reflète plus la configuration entière. -refresh-only sert à faire accepter une dérive constatée hors Terraform, en alignant le state sur le réel sans rien modifier dans le cloud.

Fenêtre de terminal
# Plan avec cible spécifique
terraform plan -target=module.vpc
# Plan de destruction
terraform plan -destroy
# Apply sans interaction (CI/CD)
terraform apply -input=false -auto-approve
# Refresh only (réconcilier le state avec l'infrastructure réelle)
terraform apply -refresh-only -auto-approve
# Parallélisme (par défaut : 10)
terraform apply -parallelism=20

Un provisioner exécute une commande au moment de la création ou de la destruction d'une ressource. Il n'apparaît pas dans le plan, ne peut donc pas être vérifié à l'avance, et n'est pas rejoué tant que la ressource ne change pas. Le bloc connection est obligatoire pour remote-exec, et un provisioner en échec marque la ressource comme tainted, ce qui déclenche sa recréation au prochain apply.

resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
# Exécution locale après création
provisioner "local-exec" {
command = "echo ${self.private_ip} >> hosts.txt"
}
# Exécution distante
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx"
]
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
# Exécution à la destruction
provisioner "local-exec" {
when = destroy
command = "echo 'Instance destroyed'"
}
}

Cet objectif regroupe tout ce qui rend une configuration paramétrable et vérifiable : conditions de garde, tests natifs, blocs générés et types composés. Le fil commun est le moment de la détection. Chaque mécanisme est conçu pour faire échouer une configuration invalide le plus tôt possible, au validate ou au plan, plutôt que de laisser le provider refuser la requête au milieu d'un apply déjà partiellement exécuté.

Ces blocs déplacent les vérifications dans le cycle de vie de la ressource : une precondition est évaluée avant la création et bloque le plan, une postcondition est évaluée après et fait échouer l'apply. La différence avec un bloc validation sur une variable tient au périmètre, une précondition peut référencer d'autres ressources et data sources, pas seulement la valeur reçue.

resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
# Vérification AVANT la création
precondition {
condition = can(regex("^ami-", var.ami_id))
error_message = "L'AMI doit commencer par 'ami-'."
}
# Vérification APRÈS la création
postcondition {
condition = self.public_ip != ""
error_message = "L'instance doit avoir une IP publique."
}
}
}

Un bloc check évalue une assertion après l'apply et produit un avertissement, jamais une erreur : le run se termine en succès et l'infrastructure reste en place. C'est ce qui le distingue d'une postcondition. Il peut embarquer son propre data source, dont l'échec n'interrompt pas le run non plus.

check "website_health" {
data "http" "app" {
url = "https://${aws_lb.main.dns_name}/health"
}
assert {
condition = data.http.app.status_code == 200
error_message = "Le site ne répond pas correctement."
}
}

Le framework natif exécute un vrai plan ou un vrai apply depuis un fichier .tftest.hcl, puis détruit ce qu'il a créé. Le choix du command est décisif : plan valide sans rien créer et va vite, apply crée réellement les ressources et permet d'asserter sur des attributs connus seulement après création. Les valeurs du bloc variables s'appliquent à tous les run du fichier.

tests/main.tftest.hcl
variables {
instance_type = "t3.micro"
environment = "test"
}
run "create_instance" {
command = apply
assert {
condition = aws_instance.web.instance_type == "t3.micro"
error_message = "Mauvais type d'instance."
}
assert {
condition = aws_instance.web.tags["Environment"] == "test"
error_message = "Tag Environment incorrect."
}
}
run "verify_plan_only" {
command = plan
assert {
condition = aws_instance.web.ami != ""
error_message = "AMI non définie."
}
}
Fenêtre de terminal
# Exécuter les tests
terraform test
# Tests verbeux
terraform test -verbose
# Filtre sur un fichier de test
terraform test -filter=tests/main.tftest.hcl

Un bloc dynamic génère des blocs imbriqués répétés à partir d'une collection, ce que for_each posé sur la ressource ne sait pas faire. À l'intérieur du content, l'itérateur porte le nom du bloc généré, ici ingress, avec ingress.value pour l'élément courant. Le mécanisme ne s'applique qu'aux blocs, jamais aux arguments simples.

variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{ port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
]
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}

Ces cinq fonctions couvrent les manipulations de structures imbriquées que l'examen demande. flatten associé à une double compréhension est le patron habituel pour ramener une arborescence à une liste plate exploitable par for_each. cidrsubnet découpe un préfixe en sous-réseaux calculés, ce qui évite d'écrire des plages en dur. try et can se ressemblent mais ne s'emploient pas au même endroit : can renvoie un booléen et va dans une condition de validation, try renvoie une valeur de repli.

# flatten : aplatir des listes imbriquées
locals {
all_subnets = flatten([
for env, config in var.environments : [
for subnet in config.subnets : {
env = env
cidr = subnet
}
]
])
}
# setproduct : produit cartésien
locals {
az_subnet_pairs = setproduct(var.azs, var.subnet_cidrs)
}
# cidrsubnet : calcul de sous-réseaux
locals {
subnets = [for i in range(3) : cidrsubnet("10.0.0.0/16", 8, i)]
# → ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
}
# templatefile : template avec variables
resource "aws_instance" "web" {
user_data = templatefile("${path.module}/user-data.tpl", {
hostname = var.hostname
packages = join(" ", var.packages)
})
}
# try et can : gestion d'erreurs
locals {
name = try(var.config.name, "default")
is_valid = can(regex("^[a-z]+$", var.input))
}

optional(type, défaut) rend un attribut d'objet facultatif et fournit sa valeur quand l'appelant l'omet. Sans ce second argument, l'attribut absent vaut null, ce qui n'est pas équivalent et se propage tel quel jusqu'au provider. Les objets s'imbriquent, chaque niveau ayant ses propres attributs optionnels.

variable "server_config" {
type = object({
name = string
instance_type = optional(string, "t3.micro")
tags = optional(map(string), {})
monitoring = optional(object({
enabled = bool
interval = optional(number, 60)
}), { enabled = false })
})
}

Cet objectif traite du partage entre plusieurs personnes et plusieurs configurations : où vit le state, comment il est verrouillé, et comment une configuration lit les résultats d'une autre. Le fil conducteur est le découpage : plutôt qu'un état unique qui contient tout, plusieurs états séparés qui communiquent par leurs outputs, avec des contraintes de version explicites pour que le tout reste reproductible.

Le bucket S3 stocke le fichier d'état, la table DynamoDB fournit le verrou qui empêche deux apply concurrents. Point souvent oublié : le bloc backend n'accepte ni variable ni interpolation, ses valeurs sont littérales ou passées à l'init avec -backend-config.

terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}

terraform_remote_state lit le fichier d'état d'une autre configuration et expose ses outputs. Deux conséquences pratiques : la configuration lectrice a besoin d'un accès en lecture au stockage du state, et elle ne voit que ce qui a été déclaré en output, jamais les attributs internes des ressources.

# Lire le state d'une autre configuration
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "eu-west-1"
}
}
# Utiliser les outputs
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.vpc.outputs.private_subnet_ids[0]
}

Cette source de données appartient au provider tfe, à déclarer dans required_providers au même titre que aws. Elle désigne un workspace par son organisation et son nom, sans exiger d'accès au stockage du state sous-jacent.

# Lire les outputs d'un workspace HCP Terraform
data "tfe_outputs" "network" {
config = {
organization = "my-org"
workspaces = {
name = "network-prod"
}
}
}
resource "aws_instance" "web" {
subnet_id = data.tfe_outputs.network.outputs.subnet_id
}

Deux contraintes de nature différente cohabitent dans le même bloc. required_version porte sur le binaire Terraform et fait échouer l'init si la version du CLI ne convient pas. version dans required_providers guide la sélection au téléchargement, puis se fige dans le lock file, qui devient ensuite la source de vérité.

terraform {
required_version = ">= 1.6.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

Syntaxe des contraintes :

OpérateurSignificationExemple
=Exactement= "5.0.0"
!=Pas cette version!= "5.1.0"
>, >=, <, <=Comparaison>= 5.0
~>Pessimistic (incrémente le dernier chiffre)~> 5.0 → >= 5.0, < 6.0

Séparer le plan de l'apply est le patron attendu en pipeline : le plan est sauvegardé en artefact avec -out, puis appliqué tel quel après approbation. Appliquer un fichier de plan garantit qu'aucun changement survenu entre-temps ne sera exécuté par surprise, et -input=false fait échouer le run au lieu d'attendre une saisie sur une variable manquante.

Fenêtre de terminal
# Pipeline type : plan séparé de l'apply
# Étape 1 : Plan
terraform init -input=false
terraform plan -input=false -out=tfplan
# Étape 2 : Apply (après approbation)
terraform apply -input=false tfplan

Cet objectif porte moins sur l'écriture d'un module que sur son intégration : comment il reçoit ses providers, où le CLI cherche ses tests, et comment du code existant y est déplacé. Le refactoring est la partie la plus testée, parce que c'est la seule qui peut détruire des ressources en cas d'oubli, et parce que la vérification attendue est parfaitement objective : un plan sans destroy.

Terraform lit tous les fichiers .tf d'un répertoire sans distinction, cette arborescence n'est donc pas imposée par le langage. Elle compte pour deux raisons : le Registry et les générateurs de documentation s'appuient dessus, et le répertoire tests/ est le chemin cherché par défaut par terraform test.

modules/
└── compute/
├── main.tf # Ressources
├── variables.tf # Inputs
├── outputs.tf # Outputs
├── versions.tf # Required providers
├── README.md # Documentation
└── tests/
└── main.tftest.hcl # Tests

Un module hérite automatiquement du provider par défaut de la configuration appelante, mais jamais d'un alias. Le bloc providers = {} sert précisément à lui en transmettre un : le nom à gauche est celui attendu à l'intérieur du module, celui de droite est l'alias défini dans la racine.

# Root module
provider "aws" {
alias = "us"
region = "us-east-1"
}
provider "aws" {
alias = "eu"
region = "eu-west-1"
}
module "us_infra" {
source = "./modules/compute"
providers = {
aws = aws.us
}
}
module "eu_infra" {
source = "./modules/compute"
providers = {
aws = aws.eu
}
}

Déplacer une ressource dans un module change son adresse, et Terraform interprète par défaut ce changement comme une destruction suivie d'une création. Les blocs moved décrivent la correspondance et suppriment ce comportement. La vérification est binaire, comme le rappelle l'étape 3 : si le plan annonce le moindre destroy, c'est qu'une adresse a été oubliée.

# Étape 1 : Déplacer le code dans le module
# Étape 2 : Ajouter le bloc moved
moved {
from = aws_instance.web
to = module.compute.aws_instance.main
}
moved {
from = aws_security_group.web
to = module.compute.aws_security_group.main
}
# Étape 3 : Vérifier
# terraform plan ne doit montrer AUCUN destroy

Cet objectif couvre la configuration des providers plutôt que les ressources qu'ils gèrent : plusieurs régions dans une même configuration, plusieurs méthodes d'authentification, et le diagnostic quand l'un ou l'autre est mal réglé. Ces erreurs se présentent souvent sous les traits d'une erreur de code, d'où la dernière section, qui les traite pour ce qu'elles sont.

Un provider sans alias est le provider par défaut, utilisé par toute ressource qui ne précise rien. Chaque configuration supplémentaire porte un alias, désigné ensuite par l'argument provider sur la ressource. Un module ne reçoit jamais un alias tout seul, il faut le lui passer explicitement.

provider "aws" {
region = "eu-west-1" # Provider par défaut
}
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
# Utilisation dans une ressource
resource "aws_s3_bucket" "backup" {
provider = aws.us_east
bucket = "backup-us-east"
}

Ces méthodes suivent un ordre de résolution : ce qui est écrit dans le bloc provider prime sur les variables d'environnement, elles-mêmes prises avant le fichier ~/.aws/credentials. assume_role ne se substitue pas aux deux autres, il s'y ajoute, puisqu'il faut des credentials de base pour demander le rôle.

# Méthode 1 : Environment variables (recommandé en CI/CD)
# export AWS_ACCESS_KEY_ID="..."
# export AWS_SECRET_ACCESS_KEY="..."
# export AWS_REGION="eu-west-1"
# Méthode 2 : Shared credentials file
provider "aws" {
shared_credentials_files = ["~/.aws/credentials"]
profile = "production"
}
# Méthode 3 : Assume role
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::123456789:role/terraform"
session_name = "terraform-session"
}
}

Ces quatre cas couvrent l'essentiel des blocages qui ne viennent pas du HCL. Le plus trompeur est le lock file : il fige les versions et les empreintes de paquets, et un init lancé depuis une autre plateforme échoue si l'empreinte correspondante n'y figure pas. Le supprimer puis relancer init le régénère. Le rate limiting se reconnaît à des échecs intermittents sur des ressources différentes d'un run à l'autre, et se traite par le parallélisme.

Fenêtre de terminal
# Version incompatible
terraform init -upgrade
terraform providers
# Credentials invalides
export TF_LOG=DEBUG
terraform plan 2>&1 | grep -i "auth\|credential\|access"
# Rate limiting
terraform apply -parallelism=5 # Réduire le parallélisme
# Provider lock corrompu
rm .terraform.lock.hcl
terraform init

Terraform n'affiche par défaut qu'un message de synthèse, souvent recopié tel quel depuis le provider et sans indication de cause. Les journaux détaillés contiennent la requête API émise et la réponse reçue, ce qui suffit à distinguer une erreur de configuration d'un refus côté cloud. Le réflexe se travaille avant l'examen, car la sortie est volumineuse et inexploitable sans filtrage.

TF_LOG fixe le niveau de verbosité, TF_LOG_PATH redirige vers un fichier et rend la console de nouveau lisible. TF_LOG_PROVIDER et TF_LOG_CORE permettent de ne tracer qu'un seul côté, indispensable pour isoler la cause quand un TRACE global produit des milliers de lignes.

Fenêtre de terminal
# Niveaux : TRACE > DEBUG > INFO > WARN > ERROR
export TF_LOG=TRACE
export TF_LOG_PATH=/tmp/terraform-debug.log
# Debug uniquement le provider
export TF_LOG_PROVIDER=TRACE
# Debug uniquement le core
export TF_LOG_CORE=DEBUG
# Désactiver
unset TF_LOG TF_LOG_PATH TF_LOG_PROVIDER TF_LOG_CORE

Cet ordre va du moins coûteux au plus coûteux et évite de lancer un TRACE pour une accolade manquante. Les trois premières étapes ne contactent pas le cloud et prennent quelques secondes ; seule la dernière, -refresh-only, interroge réellement l'API pour confirmer une dérive entre le state et le réel.

Fenêtre de terminal
# Le plan échoue ? Vérifiez dans cet ordre :
# 1. Syntaxe
terraform validate
# 2. State
terraform state list
terraform state show <resource>
# 3. Provider
terraform providers
cat .terraform.lock.hcl
# 4. Logs détaillés
TF_LOG=DEBUG terraform plan 2>&1 | tail -50
# 5. Refresh
terraform plan -refresh-only

Section intitulée « Naviguer dans la documentation (stratégie examen) »

La documentation est autorisée pendant le lab. Voici les pages les plus utiles :

BesoinOù chercher
Syntaxe d'une ressource AWSregistry.terraform.io/providers/hashicorp/aws → recherche
Fonctions HCLdeveloper.hashicorp.com/terraform/language/functions
Bloc lifecycledeveloper.hashicorp.com/terraform/language/meta-arguments/lifecycle
terraform testdeveloper.hashicorp.com/terraform/language/tests
Check blocksdeveloper.hashicorp.com/terraform/language/checks
Bloc moveddeveloper.hashicorp.com/terraform/language/modules/develop/refactoring
Backend S3developer.hashicorp.com/terraform/language/backend/s3
Provider configurationdeveloper.hashicorp.com/terraform/language/providers/configuration
  • L'examen lab utilise Terraform 1.6 + AWS, entraînez-vous avec cette version
  • La documentation est votre alliée, apprenez à y naviguer vite
  • moved {} est préféré à state mv, apprenez les deux
  • terraform test (.tftest.hcl) est un sujet clé de l'objectif 2
  • Les preconditions/postconditions dans lifecycle sont un pattern récurrent
  • Le provider aliasing multi-région est un sujet incontournable
  • Les provisioners existent mais sont déconseillés, sachez quand les utiliser
  • L'objectif 6 (HCP Terraform) est en QCM, pas besoin de commandes lab

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