ℹ️ À savoir · En bref
Terraform (HashiCorp) est un outil d'Infrastructure as Code déclaratif : on décrit en HCL l'infrastructure voulue (VM, réseaux, groupes Entra, catalogues Citrix…), Terraform compare avec l'existant puis appelle les API pour combler l'écart.
Il mémorise ce qu'il gère dans un fichier d'état (*state*). Sans state, pas de Terraform : c'est la pièce à protéger.
Licence BSL depuis la v1.6 (2023) ; OpenTofu est le fork open source compatible (commande tofu).
Objectifs de la fiche
- Maîtriser le cycle
init → plan → applyet savoir lire un plan. - Comprendre providers, ressources, data sources, variables et outputs.
- Savoir pourquoi et comment stocker le state à distance.
- Structurer du code réutilisable (modules,
for_each) et éviter les erreurs courantes.
Le vocabulaire en 1 tableau
| Bloc / terme | Rôle | Exemple |
|---|---|---|
provider | Plugin qui traduit le HCL en appels API d'une plateforme. | hashicorp/azurerm |
resource | Un objet géré (créé / modifié / détruit) par Terraform. | azurerm_resource_group |
data | Lecture d'un objet existant non géré par ce code. | data.azurerm_subnet |
variable | Paramètre d'entrée, typé et validable. | var.env |
locals | Valeurs calculées internes (évite la répétition). | local.tags |
output | Valeur exposée après apply (ou à un module parent). | ID d'un réseau |
module | Dossier de .tf réutilisable, appelé avec des variables. | module "vnet" |
state | Correspondance code ↔ objets réels (IDs, attributs). | terraform.tfstate |
backend | Où est stocké le state (+ verrouillage). | Blob Azure, S3, HCP |
Le workflow

| Étape | Ce qui se passe | Bon réflexe |
|---|---|---|
terraform init | Télécharge providers et modules, configure le backend, crée .terraform.lock.hcl. | À relancer après tout changement de provider/backend. |
terraform fmt / validate | Formatage canonique ; contrôle syntaxique et de cohérence. | Dans un hook pre-commit ou la CI. |
terraform plan | Rafraîchit l'état réel, calcule les différences, n'écrit rien. | -out=tfplan pour figer ce qui sera appliqué. |
terraform apply | Exécute le plan (graphe parallélisé), met à jour le state. | apply tfplan = exactement le plan relu. |
terraform destroy | Détruit tout ce que gère cette configuration. | Réservé au lab ; prevent_destroy en prod. |
Lire un plan sans se tromper
| Symbole | Action | Risque |
|---|---|---|
+ | create : nouvelle ressource | Faible |
~ | update in-place : modification sans recréation | Faible à moyen |
-/+ | replace : destruction puis recréation (« forces replacement ») | ÉLEVÉ : perte de données / d'IP / de VM |
- | destroy : suppression | ÉLEVÉ |
<= | read : lecture d'une data source pendant l'apply | Aucun |
⚠️ Piège
Le plan affiche en bas Plan: 2 to add, 1 to change, 1 to destroy. Lis toujours la ligne « to destroy » et cherche # forces replacement : un simple renommage de ressource peut déclencher la recréation d'une VM.
Architecture

Un exemple complet commenté
versions.tf / providers.tf
terraform {
required_version = ">= 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0" # accepte 4.x, refuse 5.0
}
}
backend "azurerm" { # state distant + verrou (lease)
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstatecontoso"
container_name = "tfstate"
key = "lab-citrix.tfstate"
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id # obligatoire en v4
}
main.tf
locals {
tags = { owner = "alice", managed_by = "terraform", env = var.env }
}
resource "azurerm_resource_group" "citrix" {
name = "rg-${var.env}-citrix"
location = var.location
tags = local.tags
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-${var.env}-citrix"
address_space = ["10.20.0.0/16"]
location = azurerm_resource_group.citrix.location # référence
resource_group_name = azurerm_resource_group.citrix.name # = dépendance implicite
}
resource "azurerm_subnet" "snet" {
for_each = { vda = "10.20.1.0/24", infra = "10.20.2.0/24" }
name = "snet-${each.key}"
resource_group_name = azurerm_resource_group.citrix.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [each.value]
}
variables.tf / outputs.tf
variable "env" {
type = string
description = "Environnement cible"
validation {
condition = contains(["dev", "prd"], var.env)
error_message = "env doit valoir dev ou prd."
}
}
variable "location" {
type = string
default = "francecentral"
}
variable "subscription_id" {
type = string
}
output "subnet_ids" {
value = { for k, s in azurerm_subnet.snet : k => s.id }
}
ℹ️ À savoir · Comment Terraform ordonne le travail
Dépendances implicites : dès qu'une ressource référence l'attribut d'une autre (azurerm_resource_group.citrix.name), Terraform sait qu'il doit créer la seconde d'abord. depends_on n'est utile que pour une dépendance invisible dans le code.
Les valeurs des variables viennent d'un fichier *.tfvars, de -var, ou de variables d'environnement TF_VAR_nom.
Le state : la pièce critique
- Il contient les IDs réels et tous les attributs, secrets compris, en clair → à protéger comme un coffre.
- Stocké à distance (Blob Azure, S3, HCP Terraform) pour être partagé, versionné et verrouillé : deux
applysimultanés sont refusés. - Jamais dans Git, jamais modifié à la main. On le manipule avec les commandes
terraform state …. - Drift : une modification faite dans le portail hors Terraform apparaît au prochain
plan, qui proposera de la défaire.
| Besoin | Solution |
|---|---|
| Reprendre sous Terraform une ressource créée à la main | Bloc import { to = … id = … } puis plan (ou terraform import) |
| Renommer une ressource sans la recréer | Bloc moved { from = … to = … } |
| Ne plus gérer une ressource sans la détruire | Bloc removed { from = … } ou terraform state rm |
| Lister / inspecter ce qui est géré | terraform state list, terraform state show <adresse> |
Méta-arguments à connaître
| Méta-argument | Usage | Remarque |
|---|---|---|
count | N copies identiques | Indexé 0..N : supprimer un élément décale les suivants |
for_each | Une instance par clé d'une map / d'un set | Préféré : clés stables, pas de recréation en cascade |
depends_on | Forcer un ordre invisible dans le code | À utiliser avec parcimonie |
lifecycle | prevent_destroy, ignore_changes, create_before_destroy | Protège la prod, tolère des champs gérés ailleurs |
provider | Choisir un alias de provider (autre souscription / région) | provider = azurerm.hub |
Modules
Un module est simplement un dossier de fichiers .tf. On l'appelle depuis la racine en lui passant des variables, et on récupère ses outputs :
appel d'un module
module "vnet_citrix" {
source = "./modules/vnet" # ou registre : "Azure/avm-res-network-virtualnetwork/azurerm"
env = var.env
cidr = "10.20.0.0/16"
}
# ailleurs, dans une ressource : utilisation d'un output du module
# subnet_id = module.vnet_citrix.subnet_ids["vda"]
Terraform ou Ansible ?
| Critère | Terraform | Ansible |
|---|---|---|
| Rôle principal | Provisionner l'infrastructure (cycle de vie des objets) | Configurer l'intérieur des machines |
| Approche | Déclaratif, graphe calculé | Déclaratif par module, exécution séquentielle |
| Mémoire | State : sait ce qu'il a créé, peut détruire | Pas d'état : vérifie à chaque passage |
| Langage | HCL | YAML + Jinja2 |
| Destruction propre | Native (destroy) | À écrire soi-même (state: absent) |
Ils sont complémentaires : Terraform crée VM, réseaux et objets Citrix, puis Ansible configure les OS (voir ANS-01).
Commandes essentielles
| Commande | À quoi ça sert |
|---|---|
terraform init -upgrade | Initialiser / mettre à jour les providers dans les contraintes |
terraform fmt -recursive | Formater tout le projet |
terraform validate | Vérifier la configuration |
terraform plan -out=tfplan | Calculer et enregistrer le plan |
terraform apply tfplan | Appliquer exactement le plan enregistré |
terraform plan -destroy | Prévisualiser une destruction |
terraform output -json | Lire les sorties (scripts, CI) |
terraform state list / show | Inspecter le state |
terraform console | Tester une expression HCL en interactif |
terraform providers lock -platform=windows_amd64 -platform=linux_amd64 | Figer les empreintes multi-OS dans le lock file |
Pièges classiques
| Symptôme | Cause | Parade |
|---|---|---|
| « Error acquiring the state lock » | Un autre apply tourne, ou un run a planté | Attendre ; sinon terraform force-unlock <ID> en connaissance de cause |
| Ressources recréées après suppression d'un élément de liste | count : les index se décalent | for_each sur une map |
| Le plan veut annuler un changement fait dans le portail | Drift | Reporter le changement dans le code, ou ignore_changes |
| Comportement différent sur un autre poste | Version de provider non figée | Contrainte ~> + commiter .terraform.lock.hcl |
| Secret exposé | State local ou commité, outputs non marqués | Backend chiffré + RBAC ; sensitive = true |
| « Cycle » dans le graphe | Deux ressources se référencent mutuellement | Séparer en ressource d'association / revoir le découpage |
🔷 En contexte Citrix
Le provider officiel citrix/citrix pilote Citrix DaaS et CVAD on-premises récents : zones, connexions d'hébergement (citrix_azure_hypervisor, vSphere…), catalogues MCS (citrix_machine_catalog) et groupes de mise à disposition (citrix_delivery_group).
Chaîne type : Terraform crée réseau + VM « master » → Ansible prépare l'image → Terraform crée le catalogue MCS à partir du snapshot et le delivery group.
Bénéfice : un environnement de recette reconstruit à l'identique, et chaque changement de prod passe par un plan relu.
À retenir
- On décrit l'état voulu ;
planmontre l'écart,applyle comble. - Le state est la mémoire de Terraform : distant, verrouillé, chiffré, jamais dans Git.
- Toujours lire « to destroy » et « forces replacement » avant d'appliquer.
for_eachplutôt quecount; versions de providers figées + lock file commité.- Terraform provisionne, Ansible configure.
Auto-évaluation
Q1. Que fait terraform plan exactement ?
→ Il rafraîchit l'état réel via les API, le compare au code et au state, puis affiche les actions nécessaires — sans rien modifier.
Q2. Pourquoi stocker le state dans un backend distant ?
→ Pour le partager entre personnes / CI, le verrouiller pendant un apply, le versionner et le protéger (il contient des secrets).
Q3. Tu as 3 VM créées avec count = 3 et tu supprimes la 1re de la liste. Que se passe-t-il ?
→ Les index se décalent : Terraform veut modifier/recréer les VM suivantes. Avec for_each sur des clés stables, seule la VM retirée serait détruite.
Q4. Comment renommer azurerm_subnet.a en azurerm_subnet.vda sans recréer le sous-réseau ?
→ Avec un bloc moved { from = azurerm_subnet.a to = azurerm_subnet.vda }.