HCTA-004 — Terraform Basics
HCTA-004 – HashiCorp Certified Terraform Associate

Historique du document
| Date | Version | Auteur | Commentaires |
|---|---|---|---|
| 27/07/2025 | 0.1 | Hamid HAMILA | Version initiale |
Document de référence
| Nom du document | Version / Date |
|---|---|
| 1 – HCTA003 – TERRAFORM BASICS.pdf | 1.0 / 27-07-2025 |
Sommaire
- A. Introduction
- B. Providers
- C. Commandes de bases
- D. Backend
- E. Terraform State
- F. Verrouillage HCL (Lock)
- G. Terraform console
- H. Variables
- I. Datasources
- J. Outputs & Interpolation
- K. Provisioners & Providers utiles
- L. Heredoc & Escape sequence
- M. Répétition de bloc
- N. Blocs spéciaux
- O. Fonctions
- P. Types de données
- Q. Commandes utiles
A. Introduction
Terraform est un outil open-source développé par HashiCorp qui permet de gérer des infrastructures de manière déclarative à l'aide de fichiers de configuration. Il s'inscrit dans une démarche appelée Infrastructure as Code (IaC), qui consiste à définir et provisionner son infrastructure à l'aide de code, au lieu de réaliser des actions manuelles sur une interface graphique.
Grâce à Terraform, il est possible de créer, modifier et détruire des ressources telles que :
- des machines virtuelles (EC2, GCE, etc.),
- des réseaux (VPC, subnets, security groups),
- des bases de données,
- des services cloud (load balancers, DNS, stockage, etc.),
sur de nombreux fournisseurs comme AWS, Azure, GCP, Kubernetes, et bien d'autres.
Terraform utilise un langage simple appelé HCL (HashiCorp Configuration Language) pour décrire l'état désiré de l'infrastructure. Ensuite, il compare l'état actuel (réel) à l'état défini dans le code et applique seulement les différences via une commande appelée terraform apply.

A.I. Identifiant Terraform
Un identifiant Terraform, c'est comme un nom unique pour référencer une ressource dans le code ou dans l'état.
aws_instance= type de ressourcemon_instance= nom logique (donné par l'utilisateur)aws_instance.mon_instance= identifiant complet (utilisé dansterraform state,terraform console, etc.)
resource "aws_instance" "mon_instance" {
}
A.II. Multi-cloud & Hybrid Cloud
L'un des intérêts majeurs de Terraform, c'est sa capacité à orchestrer plusieurs clouds dans une même configuration : c'est le multi-cloud (AWS + Azure + GCP par exemple). Quand on combine du cloud public et de l'infrastructure on-premise (vSphere, bare-metal, Kubernetes self-hosted), on parle d'hybrid cloud.
Pourquoi c'est important :
- Éviter le vendor lock-in : ne pas dépendre d'un seul fournisseur.
- Workloads par affinité : certaines charges vont mieux sur tel cloud (ex : ML sur GCP, AD sur Azure, écosystème mature sur AWS).
- Résilience régionale ou multi-provider pour la continuité d'activité.
- Cloud-agnosticité : Terraform parle à n'importe quel provider via le même workflow (
init → plan → apply), même si les ressources elles-mêmes restent spécifiques au cloud cible.
Exemple multi-cloud minimal :
provider "aws" {
region = "eu-west-1"
}
provider "azurerm" {
features {}
}
provider "google" {
project = "my-gcp-project"
region = "europe-west1"
}
resource "aws_s3_bucket" "logs" { bucket = "logs-bucket" }
resource "azurerm_resource_group" "rg" { name = "rg-eu" ; location = "westeurope" }
resource "google_storage_bucket" "data" { name = "data-bucket" ; location = "EU" }
⚠️ Cloud-agnostique ≠ ressources portables. Terraform standardise le workflow, pas les ressources. Un
aws_instancene se déploie pas tel quel sur Azure — il faut écrire la ressource cible équivalente (azurerm_linux_virtual_machine).
B. Providers
Un provider est un plugin qui permet à Terraform de gérer les ressources d'un service spécifique (AWS, Azure, GCP, Kubernetes, GitHub, Docker, etc.). Chaque provider définit un ensemble de ressources et de datasources qu'il gère.
Rôle :
- Interface entre Terraform et le service Cloud ou plateforme : traduit les fichiers
.tfécrits en HCL en appels API. - Chaque provider doit être configuré dans un
providerblock : idéalement dans un fichierproviders.tf. - Permet de gérer les identifiants, la région, etc. : dans le bloc de configuration de chaque provider.
- Permet de définir la version du provider : pour la stabilité.
- Les providers sont téléchargés automatiquement par Terraform lors de
terraform init: il faudra relancer uninitsi la version est changée.
Bonnes pratiques :
- Toujours spécifier la version du provider pour éviter les surprises liées aux mises à jour.
- Ne jamais stocker les identifiants secrets en clair dans les fichiers
.tf— privilégier les variables d'environnement ou systèmes d'authentification.
# Provider block
terraform {
required_providers { # Je déclare les providers que je veux installer
aws = { # provider AWS en version 4.0
source = "hashicorp/aws"
version = "4.0"
}
}
}
provider "aws" { # Je configure mon provider AWS pour utiliser mon compte
region = "us-east-1"
profile = "default" # Profil AWS CLI pour authentification (clé SSH ou autre possible)
}
B.I. Alias
Par défaut, Terraform ne permet qu'une seule configuration d'un provider donné dans un projet. Cependant, dans des cas réels, il est courant d'avoir besoin de plusieurs configurations différentes pour un même provider, par exemple :
- Utiliser plusieurs régions cloud
- Gérer plusieurs comptes utilisateurs ou projets
- Séparer environnements de développement, test, production, etc.
Pour gérer ces scénarios, Terraform offre la possibilité de définir des configurations multiples d'un même provider à l'aide de la directive alias :
provider "aws" { # Je configure mon provider AWS pour utiliser mon compte
region = "us-east-1"
profile = "default" # Profil AWS CLI pour authentification
}
provider "aws" { # Je configure une seconde fois le provider aws, pour un second compte
alias = "2" # je déclare l'alias que je veux, le provider sera identifié par "aws.2"
region = "us-east-2"
profile = "account2" # Profil AWS CLI pour authentification
}
C. Commandes de bases
terraform init
- Initialise un nouveau répertoire de travail Terraform.
- Télécharge les providers requis (ceux déclarés dans le
.tf) et leurs dépendances. - Crée le dossier caché
.terraform/où sont stockés les plugins, modules, etc. - Prépare le backend si configuré.
terraform validate
- Vérifie la syntaxe et la cohérence interne de la configuration (références, types, blocs requis).
- Ne contacte aucun provider, ne lit pas le state — c'est purement statique. Rapide.
- Souvent enchaîné en CI :
terraform init -backend=false && terraform validate.
$ terraform validate
Success! The configuration is valid.
terraform fmt
- Reformate les fichiers
.tfselon les conventions officielles (indentation, alignement, espaces). terraform fmt -recursivepour traiter tous les sous-dossiers.terraform fmt -check(ne modifie rien, exit code ≠ 0 si reformatage nécessaire) — idéal en CI/CD pour bloquer un PR non formaté.
terraform plan
- Simule les modifications à apporter à l'infrastructure, en comparant la configuration actuelle (
.tf) avec l'état enregistré (.tfstate). - Permet de voir quelles ressources vont être créées, modifiées ou détruites avant d'appliquer.
- Étape critique pour la revue et validation.
- Option :
terraform plan -out=tfplan/ stocke le plan dans un fichiertfplan/terraform apply tfplan/ lance l'apply à partir du fichiertfplangénéré au préalable. - Option :
terraform plan -refresh-only→ ne propose que la synchronisation entre state et réalité cloud, sans modifier l'infrastructure (remplaceterraform refreshdans le workflow moderne).
terraform destroy
- Détruit toutes les ressources gérées par la configuration Terraform dans l'état actuel.
- À utiliser avec prudence : détruit tout ! Utile en cas de test et cleanup surtout.
terraform refresh
- Met à jour le fichier state
.tfstateavec la réalité réelle de l'infrastructure dans le cloud. - Ne modifie pas l'infrastructure, mais seulement l'état (en récupérant les attributs mis manuellement par exemple).
- Synchronise l'état local avec la réalité si changements faits hors Terraform.
- Si on relance
terraform apply/plan, il va supprimer les changements d'états non présents sur les.tf. - Plus trop utilisé : on peut relancer un
planet voir la différence entre state actuel et local.
terraform state 'commands'
- Commandes pour gérer manuellement le fichier
tfstatede Terraform. - Options :
terraform state list,terraform state rm, etc. (rmà utiliser avec précaution car elle délie la ressource de terraform). - Exemple :
terraform state rm aws_instance.example/ suppression d'une ressource du state sans la détruire dans le cloud.
D. Backend
Le backend définit où et comment l'état (le fichier .tfstate) est stocké.
Par défaut, l'état est stocké localement, mais en PRD ou en travail collaboratif ou CI/CD c'est mieux de le mettre ailleurs.
Rôle :
- Permet de stocker l'état à distance (ex : S3, Azure Blob Storage) : pour partage et sécurité.
- Gère le verrouillage de l'état : pour éviter que plusieurs personnes ne modifient l'infra en même temps (important en équipe).
- Permet de conserver : l'historique, de restaurer des états, etc.
Utiles à savoir :
- Une fois configuré, il faut re-initialiser avec
terraform initpour migrer l'état. - Le backend est indépendant des providers.
- Le verrouillage est critique pour éviter les conflits.
- Plusieurs backends sont disponibles selon le cloud.
terraform {
backend "s3" { # je stocke mon backend dans un bucket S3 sur AWS
bucket = "mon-bucket-terraform-state"
key = "env/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-lock-table"
encrypt = true
}
}
E. Terraform State
Le fichier terraform.tfstate contient la carte exacte de l'infrastructure déployée, avec l'ID des ressources, leurs propriétés, et les métadonnées. C'est ce fichier qui permet à Terraform de faire la correspondance entre la config .tf et la réalité Cloud/Plateforme.
Rôle :
- Permet le suivi des ressources créées.
- Base des opérations
plan,apply,destroy. - Gère les dépendances implicites entre ressources.
- Stocke les outputs.
Bonnes pratiques :
- Ne jamais modifier manuellement sauf cas avancés.
- Le state peut contenir des données sensibles, donc sécurise-le : chiffrement, accès restreint.
- Pour le travail en équipe, stockage distant recommandé (backend).
Exemple de structure d'un terraform.tfstate :
{
"version": 4,
"terraform_version": "1.2.3",
"serial": 22,
"lineage": "bdb40ce1-d84c-f28f-fd90-b7ede49a08b5",
"outputs": {
"BucketName": {
"value": "terraform-state-test-bucket-state-file",
"type": "string"
},
"IpAddress": {
"value": "18.119.156.172",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "test",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-02f3416038bdb17fb",
"arn": "arn:aws:ec2:us-east-2:387232581030:instance/i-05974ed7db094f01e",
"associate_public_ip_address": true,
"availability_zone": "us-east-2c"
}
}
]
}
]
}
Serial & Lineage dans le tfstate :
- Serial : numéro de version du fichier
.tfstate— il augmente à chaque modification (plan/apply). - Lineage : identifiant unique du state — change si on fait
terraform initdans un autre dossier (permet de détecter les changements de state ou migrations).
Note : à chaque apply, un terraform.tfstate.backup est créé en cas de corruption du state actuel (il faudra simplement renommer le fichier, il donnera le state juste avant le dernier apply).
F. Verrouillage HCL (Lock)
Terraform utilise un fichier .terraform.lock.hcl pour enregistrer les versions exactes des providers utilisés, assurant ainsi la reproductibilité entre les environnements.
Rôle :
- Empêche que des versions de providers différentes soient utilisées sans contrôle.
- Verrouille les hashes : des providers téléchargés.
- Met à jour avec
terraform init -upgrade: si on change de version.

Si deux terminaux lancent un plan ou apply en même temps, le second sera bloqué :
# Terminal 1
$ terraform apply
Acquiring state lock. This may take a few moments...
aws_dynamodb_table.lock: Refreshing state... [id=terraform-lock]
aws_security_group.test: Refreshing state... [id=sg-063a7102436aa8eeb]
aws_s3_bucket.b: Refreshing state... [id=terraform-state-test-bucket-state-file]
...
# aws_instance.test will be updated in-place
~ resource "aws_instance" "test" {
id = "i-0bab680f73429f437"
~ instance_type = "t3.micro" -> "t2.micro"
}
# Terminal 2 (lancé pendant que Terminal 1 tourne)
$ terraform apply
Acquiring state lock. This may take a few moments...
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 7c7aa071-8dcb-fa97-d401-7692409a400f
Path: terraform-state-test-bucket-state-file/terraform.tfstate
Operation: OperationTypeApply
Who: LAPTOP-4KJ3D15F\jerin@LAPTOP-4KJ3D15F
Version: 1.2.3
Created: 2022-07-21 09:27:51.7978362 +0000 UTC
Note : si le lock est bloqué et crée des conflits, il faudra récupérer l'ID du lock et faire : terraform force-unlock <LOCK_ID>.
G. Terraform console
On tape
terraform consolesur le terminal, cela ouvre un outil interactif pour évaluer les expressions Terraform en direct, utile pour tester la syntaxe, les fonctions, les variables.
Rôle :
- Tester des expressions complexes
- Comprendre la sortie de fonctions Terraform
- Debug
(base) → test git:(master) × terraform console
> type("10.0.0.0/16")
string
> type(28)
number
> type(true)
bool
> type(["default", "dedicated", "host"])
tuple([
string,
string,
string,
])
> type(tolist(["default", "dedicated", "host"]))
list(string)
> type({"count" = 2, "env" = "prod"})
object({
count: number,
env: string,
})
> type(tomap({"count" = 2, "env" = "prod"}))
map(string)
Autres exemples :
# Utilisation avec -state
terraform console -state=terraform.tfstate
> aws_instance.mon_instance.private_ip
Permet d'interroger une ressource directement dans le state existant.
H. Variables
Les variables rendent la configuration modulaire et dynamique, en permettant d'injecter des valeurs à l'exécution.
Définition d'une variable :
variable "region" { # définition d'une variable "region"
type = string # facultatif
default = "us-east-1" # sa valeur par défaut
description = "AWS region" # facultatif
}
On peut aussi la déclarer ailleurs :
- Directement sur le CLI :
$ terraform apply -var="region=eu-west-1"
Terraform used the selected providers to generate
the following execution plan.
+ aws_instance.example will be created
- Ou dans un fichier
.tfvars:terraform apply -var-file='test.tfvars'(Si on nomme un fichierterraform.tfvars, il sera pris par défaut sans déclarer le-var-file.)
# test.tfvars
region = "us-east-1"
- Ou dans l'environnement avec
TF_VAR_:
export TF_VAR_env=prod
export TF_VAR_instance_count=3
Précédence des variables (dans l'ordre) :
- Ligne de commande (
-varargument) - Fichier
.tfvarsou.tfvars.json - Variable d'environnement
TF_VAR_ - Valeur par défaut dans le code
Bonne pratique :
- On déclare les variables dans
variables.tf - Puis on met leurs valeurs dans un fichier
.tfvarspar env (dev,uat,ppd,prd) - Pour charger le fichier tfvars :
terraform 'command' -var-file='env'.tfvars
Bloc de validation variables :
Utiliser || pour OU, && pour ET.
variable "env" {
type = string
validation {
condition = var.env == "dev" || var.env == "prd"
error_message = "L'env doit être dev ou prd"
}
}
Sensitive = true :
Cache la valeur dans les outputs et logs Terraform (mais visible dans le tfstate !).
variable "db_password" {
type = string
sensitive = true
}
H.I. Custom Conditions (precondition, postcondition, check)
Terraform permet de poser des assertions dans la configuration pour valider des hypothèses, attraper des erreurs tôt, et documenter les contrats entre composants. Trois mécanismes :
1. Bloc validation (sur les variables) — déjà vu plus haut, valide la valeur d'entrée avant tout déploiement.
2. precondition / postcondition (sur resource / data / output)
precondition: doit être vraie avant que la ressource soit créée/lue. Sert à valider les hypothèses qu'on fait sur les données d'entrée.postcondition: doit être vraie après que la ressource soit créée/lue. Sert à valider que le résultat respecte ce qu'on attend.
data "aws_ami" "example" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
lifecycle {
# Bloque l'apply si l'AMI trouvée n'est pas une x86_64
postcondition {
condition = self.architecture == "x86_64"
error_message = "L'AMI sélectionnée n'est pas en architecture x86_64."
}
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.example.id
instance_type = var.instance_type
lifecycle {
# Refuse de déployer une t2.nano en production
precondition {
condition = !(var.env == "prd" && var.instance_type == "t2.nano")
error_message = "t2.nano est interdit en production."
}
}
}
3. Bloc check {} (top-level, exécuté en fin de plan / apply)
Le bloc check permet des assertions continues sur l'infrastructure, sans bloquer l'apply. Si l'assertion échoue, Terraform émet un warning (pas d'erreur fatale). Pratique pour du monitoring de cohérence.
check "health_endpoint" {
data "http" "homepage" {
url = "https://${aws_lb.web.dns_name}/health"
}
assert {
condition = data.http.homepage.status_code == 200
error_message = "Le endpoint /health ne répond pas 200."
}
}
| Bloc | Quand | Bloque ? | Cible |
|---|---|---|---|
validation |
avant tout | ✅ oui | variable |
precondition |
avant création/lecture ressource | ✅ oui | resource / data / output |
postcondition |
après création/lecture ressource | ✅ oui | resource / data / output |
check |
en fin de plan/apply | ❌ warning seulement | top-level |
H.II. Ephemeral values & write-only arguments
Terraform 1.10+ introduit des mécanismes pour manipuler des secrets sans jamais les persister dans le tfstate — un vrai pas en avant pour la sécurité.
1. Variables / outputs / ressources ephemeral
Une valeur marquée ephemeral = true n'est utilisée que pendant la durée de l'opération (plan/apply) et n'est jamais écrite dans le state ni dans le plan file.
variable "db_admin_password" {
type = string
ephemeral = true # jamais dans le tfstate ni dans le plan
sensitive = true
}
output "rds_endpoint" {
value = aws_db_instance.main.endpoint
ephemeral = true # consommable pendant l'apply, pas persisté
}
Ressources ephemeral : certaines ressources sont conçues pour produire des valeurs jetables (ex : ephemeral "random_password" "tmp" {}) — le résultat est utilisé puis disparaît.
2. Arguments write_only
Certains attributs de ressource peuvent être marqués write_only : Terraform envoie la valeur au provider à la création (ou via un trigger explicite), mais ne la lit jamais en retour. Donc elle n'apparaît jamais en clair dans le tfstate.
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string_wo = var.db_admin_password # _wo = write-only
secret_string_wo_version = 1 # incrémenter pour pousser une nouvelle valeur
}
Différences clés :
| Mécanisme | Stocké en state ? | Visible en log/plan ? | Cas d'usage |
|---|---|---|---|
sensitive = true |
✅ oui (en clair) | ❌ masqué | Limiter l'exposition visuelle |
ephemeral = true |
❌ jamais | ❌ masqué | Secret utilisé pendant l'op, jeté ensuite |
write_only |
❌ jamais (l'attribut) | ❌ pas relu | Pousser un secret au provider sans le persister |
💡 Règle : pour un mot de passe ou un token, préfère
ephemeral+write_onlyplutôt quesensitiveseul.sensitivene fait que masquer l'affichage, le secret reste en clair dans letfstate.
I. Datasources
Une datasource permet de lire des données externes (une info existante dans le cloud ou un service) et les utiliser dans ta configuration (c'est-à-dire qu'au lieu de déployer une config sur le Cloud par exemple, tu récupères la config d'un élément du Cloud pour pouvoir utiliser ses attributs).
Rôle :
- Référencer des ressources externes sans les créer.
- Récupérer les attributs de n'importe quelle ressource existante non créée par Terraform.

# Created by Terraform
resource "aws_instance" "example01" {
ami = data.aws_instance.test01.ami
instance_type = var.ami_type
}
# Not by Terraform (récupération d'une instance existante)
data "aws_instance" "test01" {
instance_id = "i-06d36033c3a28be69"
}
J. Outputs & Interpolation
Outputs
Permet de récupérer et exposer des valeurs importantes après la création d'une infrastructure (ex : IP publique, ID).
terraform output: voir les outputsterraform plan/apply: les outputs s'affichent à la finterraform output -json: affiche les outputs sous forme JSON (pour scripts, outils, CI/CD)
output "vpc_id" {
description = "ID of project VPC"
value = module.vpc.vpc_id
}
output "lb_url" {
description = "URL of load balancer"
value = "http://${module.elb_http.this_elb_dns_name}/"
}
output "web_server_count" {
description = "Number of web servers provisioned"
value = length(module.ec2_instances.instance_ids)
}
$ terraform output
lb_url = "http://lb-5YI-project-alpha-dev-2144336064.us-east-1.elb.amazonaws.com/"
vpc_id = "vpc-004c2d1ba7394b3d6"
web_server_count = 4
Interpolation
Syntaxe Terraform pour intégrer des variables et expressions dans des chaînes de caractères ou arguments.
${ }
Exemple :
env = dev
resource "xxx" "xxx" {
Name = "web-${var.env}" # ca donne web-dev
}
K. Provisioners & Providers utiles
Random Provider
Plugin qui génère des valeurs aléatoires (passwords, UUID, chaînes aléatoires).
Exemple :
resource "random_password" "db_password" {
length = 16
special = true
}
Le provider a aussi d'autres ressources comme random_uuid et autres…
⚠️ Important : il faut déclarer le provider dans le fichier
providersavec les autres (voir site de Terraform) !
Provisioner (Local-exec)
Permet d'exécuter une commande shell (ou autres) locale après création ou modification d'une ressource.
Rien n'est stocké dans le state de Terraform (un destroy ne va pas détruire la commande faite).
Il existe aussi des provisioners remote-exec et file :

Exemple :
Une fois le terraform apply terminé, la commande echo ... sera exécutée directement dans notre terminal. Cela n'a rien à voir avec Terraform, car aucune information liée à cette commande n'est enregistrée dans le fichier tfstate.
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo La ressource a été créée"
}
}
L. Heredoc & Escape sequence
Heredoc : écrire un gros bloc de texte multi-lignes (<<EOF xxx EOF).
Escape sequences :
\n= saut de ligne\"= guillemet\\= backslash normal
Utilisé pour les user/custom data :
user_data = <<EOF
#!/bin/bash
echo "Bienvenue sur le serveur" # Texte normal
echo \"Nom du serveur : test-vm\" # Affiche les guillemets
echo "Utilisateur actuel : \$USER" # \$ pour afficher la variable shell sans l'interpréter côté Terraform
echo "Ligne 1\nLigne 2" # \n pour un saut de ligne
echo "Chemin : C:\\Program Files\\App" # \\ pour un backslash
EOF
M. Répétition de bloc
count
Permet de créer plusieurs instances identiques d'une ressource en répétant un bloc.
resource "aws_instance" "app" {
count = 2
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
}
for_each & maps
Permet d'itérer sur des maps ou sets pour créer plusieurs ressources avec des clés différentes.
variable "instances" {
default = {
web1 = "t2.micro"
web2 = "t3.micro"
web3 = "t2.small"
}
}
resource "aws_instance" "ec2" {
for_each = var.instances # va boucler sur toutes les keys de la variable instances
ami = "ami-12345678"
instance_type = each.value # t2.micro, t3.micro, t2.small
tags = {
Name = each.key # web1, web2, web3
}
}
for expressions
Permet de créer des listes ou maps à partir d'autres collections avec transformation.
locals {
names = ["a", "b", "c"]
upper_names = [for n in local.names : upper(n)] # renvoie le tout en MAJ (upper permet de maj)
}
Splat expressions *
Récupère tous les id des instances créées via count ou for_each.
aws_instance.web[*].id
Boucle for…in avec liste
Parcourt une liste d'objets (var.instances) et retourne une nouvelle liste contenant un élément spécifique extrait de chaque objet (ici, p.id).
variable "instances" {
default = [
{
name = "web"
id = "i-abc123"
},
{
name = "db"
id = "i-def456"
}
]
}
output "instance_ids" {
value = [for p in var.instances : p.id]
}
# Résultat : ["i-abc123", "i-def456"]
output "instance_names_ids" {
value = [for p in var.instances : "${p.name}-${p.id}"]
}
# Résultat : ["web-i-abc123", "db-i-def456"]
Boucle for => ... pour créer une map
Cette expression parcourt une liste d'éléments et construit une map où chaque élément fournit une clé et une valeur. C'est utile pour transformer ou restructurer des données selon des besoins spécifiques, comme associer un identifiant à un nom ou regrouper des informations par catégorie.
variable "github" {
default = [
{
name = "repo1"
clone_url = "https://github.com/user/repo1.git"
},
{
name = "repo2"
clone_url = "https://github.com/user/repo2.git"
}
]
}
output "repos_map" {
value = {
for i in var.github : i.name => i.clone_url
}
}
Résultat :
{
"repo1" = "https://github.com/user/repo1.git"
"repo2" = "https://github.com/user/repo2.git"
}
N. Blocs spéciaux
Dynamic block
Permet de générer dynamiquement un ou plusieurs blocs imbriqués selon des variables.
variable "ingress_rules" {
default = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
resource "aws_security_group" "example" {
name = "example-sg"
description = "SG with dynamic ingress rules"
vpc_id = "vpc-xxxxxxxx"
# Crée dynamiquement un bloc `ingress` pour chaque règle définie dans `var.ingress_rules`
dynamic "ingress" {
for_each = var.ingress_rules # boucle sur chaque règle
content {
from_port = ingress.value.from_port # récupère chaque champ
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Nested blocks
Les nested blocks sont des blocs à l'intérieur d'autres blocs.
resource "aws_lb_listener" "example" {
load_balancer_arn = "arn:aws:..."
port = 80
protocol = "HTTP"
# Bloc imbriqué dans aws_lb_listener
default_action {
type = "forward"
}
}
depends_on
Force la dépendance explicite entre ressources, garantissant leur ordre de création.
resource "aws_security_group" "sg" {
name = "web-sg"
description = "SG for web instances"
vpc_id = "vpc-xxxxxxxx"
}
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
depends_on = [aws_security_group.sg] # la ressource aws_instance.web ne sera créée qu'après aws_security_group.sg
}
Implicit Dependency (dépendance implicite)
C'est quand Terraform comprend tout seul qu'une ressource dépend d'une autre sans que tu aies à écrire depends_on (explicite).
resource "aws_instance" "a" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = { Name = "instance-a" }
}
resource "aws_instance" "b" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = { Name = aws_instance.a.tags.Name } # dépendance implicite ici (référence à .a)
}
lifecycle ignore_changes
Empêcher Terraform de modifier ou supprimer certains attributs d'une ressource même s'ils ont changé dans le cloud ou ont été modifiés manuellement.
resource "aws_instance" "example" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Env = "prod"
}
lifecycle {
# Terraform ne touchera pas aux modifications manuelles sur ces champs
ignore_changes = [
tags["Env"], # Si tu modifies ce tag manuellement dans AWS, Terraform ne dira rien
]
}
}
Exemple create_before_destroy (très utile pour les ressources critiques sans downtime) :
Par défaut, Terraform détruit puis recrée une ressource quand un changement le nécessite (ex : modification de l'AMI d'une EC2). Sur une ressource critique servant du trafic, ça crée une interruption. Avec create_before_destroy, Terraform fait l'inverse : il crée d'abord la nouvelle ressource, attend qu'elle soit prête, puis détruit l'ancienne.
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
}
}
⚠️ Pour que ça fonctionne, la ressource doit pouvoir coexister en double pendant la transition. Si elle a un nom unique fixe (ex : un security group avec un
namefigé), il faut utilisername_prefixau lieu denamepour que la nouvelle puisse être créée avant que l'ancienne ne libère son nom.
Autres arguments lifecycle utiles :
prevent_destroy(empêche la suppression accidentelle d'une ressource critique)create_before_destroy(crée la nouvelle ressource avant de supprimer l'ancienne pour éviter les interruptions)ignore_changes(ignore certains changements faits en dehors de Terraform)replace_triggered_by(force la recréation d'une ressource si une autre change)ignore_changes = [all](ignore tous les changements non gérés par Terraform)ignore_changes = [specific_attribute](ignore un ou plusieurs attributs spécifiques — ex :tags)replace_triggered_by = [resource.attr](remplace la ressource si un attribut d'une autre ressource change)create_before_destroy = trueavecdepends_on(gère les dépendances pour éviter un ordre de destruction incorrect)prevent_destroy = truedans un module (protège les ressources sensibles, comme un VPC ou un bucket S3)ignore_changespourtags(évite des changements inutiles si des tags sont ajoutés manuellement)replace_triggered_by = [var.*](recrée une ressource si une variable module importante change)ignore_changes = [timeouts](évite la recréation liée à des modifications de délai de provisioning)
O. Fonctions
try()
Essaie plusieurs expressions dans l'ordre, renvoie la première sans erreur.
output "my_val" {
value = try(var1, var2, var3, [...], default) # Il va tester toutes les variables et passer à la suivante jusqu'à celle par défaut
}
templatefile()
Charge un fichier template et injecte dynamiquement des variables dans (utile pour script et autre).
resource "azurerm_linux_virtual_machine" "vm1" {
name = "example-vm"
custom_data = templatefile("cloud-init.tpl", # va se baser sur le fichier cloud-init.tpl
{
hostname = "vm-test" # dans cloud-init il y a $hostname, il prendra la valeur
username = "azureuser" # dans cloud-init il y a $username, il prendra la valeur
}
)
}
length()
Retourne la taille d'une liste, chaîne ou map.
output "count" {
value = length(["a", "b", "c"]) # va retourner 3, car la liste fait 3
}
contains()
Vérifie si une liste contient un élément.
output "has_b" {
value = contains(var.list, "b") # si la list contient b, il retournera true sinon false
}
toset()
Convertit une liste en ensemble (sans doublons).
toset(["a", "a", "b"]) # retourne [a,b]
upper() et lower()
Majuscules / minuscules.
upper("hello") # retourne HELLO
lower("HELLO") # retourne hello
join()
Fusionner une liste en une seule chaîne.
join(", ", ["web", "db", "cache"]) # retourne "web, db, cache"
split()
Couper une chaîne en une liste / Tu as une chaîne et tu veux récupérer les éléments dedans.
split("-", "10-20-30") # ["10", "20", "30"] / le séparateur sélectionné est "-"
merge()
Fusionner plusieurs maps / Tu as deux blocs de données et tu veux les combiner. (Utile pour les tags surtout.)
variable "common_tags" {
type = map(string)
default = {
env = "production"
team = "devops"
}
}
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
tags = merge(
var.common_tags,
{
Name = "web-server"
component = "frontend"
}
)
}
Résultat :
{
env = "production"
team = "devops"
Name = "web-server"
component = "frontend"
}
Fonctions utiles avec exemples :
| Fonction | Description | Exemple d'utilisation | Retourne (exemple concret) | Remarques utiles |
|---|---|---|---|---|
toset() |
Convertit une liste ou tuple en set (unique, non ordonné). | toset(["a", "b", "a"]) |
["a", "b"] |
Supprime les doublons, idéal avec for_each. |
coalesce() |
Retourne le premier argument non null/non vide. | coalesce("", null, "x") |
"x" |
Pratique pour valeurs par défaut. |
merge() |
Fusionne plusieurs maps en une seule. | merge({a=1}, {a=2, b=3}) |
{a = 2, b = 3} |
Les clés identiques sont remplacées. |
split() |
Coupe une chaîne en liste selon un séparateur. | split(",", "a,b,c") |
["a", "b", "c"] |
Inverse de join(). |
flatten() |
Aplatit une liste de listes. | flatten([["a"], ["b", "c"]]) |
["a", "b", "c"] |
Une seule profondeur. |
length() |
Retourne la taille d'une liste, map ou chaîne. | length(["a","b"]) |
2 |
Aussi utile avec count. |
contains() |
Vérifie si une liste contient une valeur. | contains(["a","b"], "a") |
true |
Peut conditionner des ressources. |
lookup() |
Récupère une valeur dans une map, avec fallback. | lookup({a=1}, "b", 42) |
42 |
Plus sûr que l'accès direct. |
element() |
Récupère un élément d'une liste (cyclique). | element(["a","b"], 3) |
"a" |
3 % 2 = 1 → "a". |
join() |
Concatène une liste en chaîne avec séparateur. | join("-", ["a", "b"]) |
"a-b" |
Inverse de split(). |
zipmap() |
Crée une map à partir de deux listes. | zipmap(["a", "b"], [1, 2]) |
{a = 1, b = 2} |
Longueurs doivent matcher. |
for expressions |
Génère dynamiquement une liste ou map. | [for x in [1,2,3] : x * 2] |
[2, 4, 6] |
Incontournable en modules. |
setproduct() |
Produit cartésien de deux listes. | setproduct(["a"], [1,2]) |
[["a", 1], ["a", 2]] |
Pour combiner dimensions. |
regex() |
Extrait via regex une portion de texte. | regex("[a-z]+", "abc123") |
"abc" |
Peut planter si pas de match. |
cidrsubnet() |
Calcule un sous-réseau à partir d'un CIDR. | cidrsubnet("10.0.0.0/16", 8, 1) |
"10.0.1.0/24" |
Très utile en réseau VPC. |
compact() |
Supprime les chaînes vides d'une liste. | compact(["a", "", "b"]) |
["a", "b"] |
Nettoyage rapide. |
distinct() |
Supprime les doublons d'une liste (conserve l'ordre). | distinct(["a", "b", "a"]) |
["a", "b"] |
Contrairement à toset(), garde l'ordre. |
reverse() |
Inverse l'ordre d'une liste. | reverse(["a", "b", "c"]) |
["c", "b", "a"] |
Pratique en stratégie fallback. |
keys() |
Récupère toutes les clés d'une map. | keys({a=1, b=2}) |
["a", "b"] |
Combinable avec for_each. |
values() |
Récupère toutes les valeurs d'une map. | values({a=1, b=2}) |
[1, 2] |
À combiner avec keys() parfois. |
P. Types de données
Types primitifs :
string: chaîne de caractèresnumber: nombre entier ou décimalbool:trueoufalse
Collections :
list: liste ordonnée, ex :["a", "b"]set: liste non ordonnée sans doublons, ex :["a", "b", "a"]=>toset=>["a", "b"]map: dictionnaire clé-valeur
variable "example_map" {
type = map(string)
default = {
key1 = "value1"
key2 = "value2"
key3 = "value3"
}
}
Objet complexe :
Structures avec plusieurs attributs typés.
variable "server" {
type = object({
id = number
name = string
tags = list(string)
active = bool
})
}
Q. Commandes utiles
terraform import
Intégrer une ressource existante (sur le Cloud mais inexistante dans Terraform) dans le state Terraform en la déclarant dans les .tf.
Fonctionnement :
- Il faut avoir une définition de la ressource dans ton fichier
.tf: l'import ne génère pas la configuration, il faut que la ressource soit déclarée dans le code avant. - Tu indiques à Terraform sur le CLI l'ID exact de la ressource existante.
- Terraform ajoute cette ressource dans ton state sans la recréer.
- Après l'import, tu dois faire un
terraform planpour vérifier la cohérence.
resource "aws_instance" "ec2_example" {
ami = "vm a importer"
}
$ terraform import aws_instance.ec2_example i-097f1ec37854d01c2
aws_instance.ec2_example: Importing from ID "i-097f1ec37854d01c2"...
aws_instance.ec2_example: Import prepared!
Prepared aws_instance for import
aws_instance.ec2_example: Refreshing state... [id=i-097f1ec37854d01c2]
Import successful!
Bonus — Méthode moderne (depuis Terraform 1.5+) — avec blocs import :
Tu peux maintenant déclarer ce que tu veux importer directement dans ton code, et Terraform s'en occupe.
Syntaxe :
import {
to = aws_instance.example
id = "i-0abcd1234efgh5678"
}
terraform taint/untaint
taint: marquer une ressource comme "à recréer" lors du prochain apply.untaint: annuler l'effet du taint, c'est-à-dire dire à Terraform de ne plus recréer la ressource.
Fonctionnement :
- Si une ressource est corrompue, ou si tu veux forcer sa recréation, tu peux "taint" cette ressource.
- Terraform considérera alors qu'elle doit être détruite puis recréée à la prochaine application.
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform taint github_repository.newname
Resource instance github_repository.newname has been marked as tainted.
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform untaint github_repository.newname
Resource instance github_repository.newname has been successfully untainted.
terraform state list
list: liste toutes les ressources connues dans le state.show <resource>: affiche les attributs d'une ressource dans le state.rm <resource>: supprime une ressource du state sans la détruire dans le cloud. Utile pour retirer Terraform d'une ressource.mv <resource>: permet de renommer une ressource dans le state, ou déplacer entre modules.
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform state list
github_repository.example
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform state show github_repository.example
# github_repository.example:
resource "github_repository" "example" {
allow_auto_merge = false
allow_merge_commit = true
allow_rebase_merge = true
allow_squash_merge = true
allow_update_branch = false
archived = false
default_branch = "main"
delete_branch_on_merge = false
description = "My awesome codebase"
}
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform state rm github_repository.example
Removed github_repository.example
Successfully removed 1 resource instance(s).
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform state list
PS C:\Users\Hamid\Desktop\TERRAFORM>
# Avant : resource "github_repository" "example"
# resource "github_repository" "example" {
# name = "example"
# description = "My awesome codebase"
# visibility = "public"
# }
# Après renommage :
resource "github_repository" "newname" { # example changed to "newname"
name = "example"
description = "My awesome codebase"
visibility = "public"
}
PS C:\Users\Hamid\Desktop\TERRAFORM> terraform state mv github_repository.example github_repository.newname
Move "github_repository.example" to "github_repository.newname"
Successfully moved 1 object(s).
Bloc moved :
Équivalent du mv, indique à Terraform un renommage ou déplacement de ressource.
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
Bloc removed :
À l'inverse de moved, le bloc removed indique à Terraform qu'une ressource doit être retirée du state sans être détruite dans le cloud (équivalent déclaratif de terraform state rm). Très utile en CI/CD où on ne veut pas devoir lancer une commande CLI manuelle.
removed {
from = aws_instance.legacy_db
lifecycle {
destroy = false # false = retire seulement du state, true = détruit aussi dans le cloud
}
}
terraform state VS terraform show :
terraform state= inspecte ou modifie l'état (le fichier.tfstate)terraform state list= liste toutes les ressources suivies par Terraformterraform state show resource.id= affiche les détails d'une seule ressourceterraform show= affiche tout le fichier.tfstate, humainement lisible ; sans option, tout l'état actuelterraform show -json= version JSON, utile pour du scripting/automation
Q.I. Bonnes pratiques et sécurité
- Sécurité des variables sensibles : utilisation de
sensitive = truepour masquer les valeurs dans les sorties. - Gestion des secrets : éviter de stocker des secrets en clair dans le code. Utiliser des services comme AWS Secrets Manager ou HashiCorp Vault.
- Versioning : utilisation de Git pour versionner les configurations Terraform.
- Revue de code : mettre en place des revues de code pour assurer la qualité et la sécurité des configurations.
- Toujours sécuriser le fichier state, car il peut contenir des données sensibles : mots de passe, clés. (OpenTofu est l'équivalent de Terraform mais avec le
tfstatecrypté !) - Utiliser un backend distant : pour les équipes, avec verrouillage pour éviter les conflits.
- Ne jamais modifier manuellement le fichier state : sauf si tu sais exactement ce que tu fais.
- Versionner la configuration
.tfmais pas le state local : à moins que tu sois seul sur un projet simple. - Utiliser
terraform plan -refresh-onlyrégulièrement : pour détecter la dérive sans modifier l'infrastructure (leterraform refreshstandalone est considéré obsolète depuis Terraform 0.15). - En cas de conflit ou erreur, utiliser les commandes
state: pour manipuler proprement l'état.
Q.II. Logs verbeux & debug (TF_LOG)
Pour debugger un comportement bizarre (provider qui ne répond pas, plan qui boucle, etc.), Terraform expose des niveaux de log via la variable d'environnement TF_LOG.
Niveaux disponibles (du moins au plus verbeux) :
TRACE > DEBUG > INFO > WARN > ERROR
Exemples :
# Activer les logs DEBUG sur tous les appels
export TF_LOG=DEBUG
terraform apply
# Niveau maximum, écrire dans un fichier
export TF_LOG=TRACE
export TF_LOG_PATH=./terraform-trace.log
terraform plan
# Filtrer : logs côté core uniquement
export TF_LOG_CORE=DEBUG
# Filtrer : logs côté provider uniquement
export TF_LOG_PROVIDER=TRACE
# Désactiver
unset TF_LOG
💡
TF_LOG=TRACEgénère des gros volumes (clés API, payloads HTTP, etc.). Ne jamais commit/uploader ces logs en l'état, ils peuvent contenir des secrets.