ℹ️ À savoir · En bref
Un module est un dossier de fichiers .tf réutilisable : on lui passe des variables, il expose des outputs. Le dossier où l'on lance terraform plan est le module racine.
Bien découpé et versionné, un module permet de déployer la même brique (réseau, connexion Citrix, catalogue) en dev, recette et prod sans copier-coller, avec des states séparés.
Objectifs de la fiche
- Écrire un module propre : interface claire, validation, outputs utiles.
- Versionner et appeler un module (local, Git, registre), y compris en
for_each. - Refactoriser vers des modules sans recréer les ressources.
- Choisir une organisation multi-environnements et ses backends.
Composer des modules

structure d'un module
modules/citrix_catalog/
├── main.tf # ressources
├── variables.tf # interface d'entrée (types, validation, description)
├── outputs.tf # ce que le module expose
├── versions.tf # required_providers (sans bloc provider)
└── README.md
interface du module
# modules/citrix_catalog/variables.tf
variable "name" { type = string }
variable "machine_count" {
type = number
default = 2
validation {
condition = var.machine_count >= 1 && var.machine_count <= 50
error_message = "machine_count doit être entre 1 et 50."
}
}
variable "allowed_groups" {
type = list(string)
description = "Groupes AD autorisés sur le bureau"
}
# modules/citrix_catalog/outputs.tf
output "catalog_id" { value = citrix_machine_catalog.this.id }
output "delivery_group_id" { value = citrix_delivery_group.this.id }
💡 Astuce
Un module ne déclare pas de bloc provider : il hérite de ceux du module racine (ou les reçoit via providers = { … }). Il déclare seulement ses required_providers.
Nommer la ressource principale this quand le module n'en crée qu'une : citrix_machine_catalog.this.
Appeler et versionner
envs/prd/main.tf
module "network" {
source = "git::https://git.contoso.com/iac/modules.git//network?ref=v1.3.0"
env = var.env
cidr = "10.20.0.0/16"
}
module "catalogs" {
source = "../../modules/citrix_catalog" # local pendant le développement
for_each = var.catalogs # un catalogue par entrée de la map
name = each.key
machine_count = each.value.count
allowed_groups = each.value.groups
pool_id = module.network.pool_id # output d'un autre module
}
# var.catalogs = { "CAT-W2025-MS" = { count = 4, groups = ["CORP\\GG-Bureau"] } }
| Source | Syntaxe | Version |
|---|---|---|
| Local | ./modules/x ou ../../modules/x | Celle du dépôt courant |
| Git | git::https://…/repo.git//chemin?ref=v1.3.0 | Tag ou commit dans ref |
| Registre public / privé | Azure/avm-res-network-virtualnetwork/azurerm | Argument version = "~> 0.4" |
| HCP Terraform | app.terraform.io/<org>/<nom>/<provider> | Argument version |
⚠️ Piège
Après avoir changé la source ou la version d'un module, relance terraform init -upgrade. Et ne pointe jamais la prod sur une branche (ref=main) : un commit de collègue changerait ton prochain plan.
Refactoriser sans tout recréer
Déplacer des ressources existantes dans un module change leur adresse dans le state. Sans précaution, Terraform veut détruire puis recréer. Le bloc moved indique simplement le nouveau chemin :
refactoring
moved {
from = citrix_machine_catalog.w2025_ms
to = module.catalogs["CAT-W2025-MS"].citrix_machine_catalog.this
}
Le plan doit alors afficher « has moved to » et 0 to destroy. Une fois appliqué partout, le bloc moved peut rester comme trace historique.
Plusieurs environnements

| Approche | Principe | Avantages / limites |
|---|---|---|
| Dossiers par environnement | envs/dev, envs/prd : chacun son backend, ses .tfvars, ses versions de modules | Isolation forte, prod peut rester sur une version plus ancienne ; un peu de duplication |
| Workspaces CLI | Même code, plusieurs states : terraform workspace new rec, terraform.workspace dans le code | Simple pour des copies identiques ; même backend et mêmes droits pour tous, risque d'appliquer sur le mauvais workspace |
| Backend partiel | terraform init -backend-config=prd.backend.hcl | Un seul code, backend choisi à l'init ; demande de la rigueur en CI |
| Terragrunt / HCP Terraform | Outils dédiés à l'orchestration multi-environnements | Puissants ; une couche de plus à apprendre |
workspaces CLI
terraform workspace list # * default
terraform workspace new rec # crée et sélectionne
terraform workspace select prd
terraform plan -var-file=prd.tfvars
# dans le code
locals {
machine_count = terraform.workspace == "prd" ? 8 : 2
}
⚠️ Piège
Les workspaces CLI partagent le même backend et les mêmes identifiants : ce n'est pas une barrière de sécurité entre dev et prod. Pour la prod, préfère un dossier et un backend dédiés, avec des droits séparés. À ne pas confondre avec les *workspaces* de HCP Terraform, qui sont des espaces de travail complets.
Pièges classiques
| Symptôme | Cause | Parade |
|---|---|---|
| « Module not installed » | terraform init non relancé après ajout de module | terraform init (ou -upgrade) |
| Le plan détruit tout après refactoring | Adresses changées sans bloc moved | Ajouter moved (ou terraform state mv) |
| Module difficile à réutiliser | Valeurs codées en dur, trop de responsabilités | Une brique = un module ; tout ce qui varie en variable |
| Changement en prod non voulu | Prod pointée sur ref=main | Tags sémantiques, montée de version explicite par environnement |
| Apply sur le mauvais environnement | Workspace courant oublié | Dossiers séparés, ou afficher terraform workspace show dans la CI |
🔷 En contexte Citrix
Modules naturels pour Citrix : network (VNet, sous-réseaux), citrix_hosting (connexion + pool), citrix_catalog (catalogue + groupe de mise à disposition, voir TF-04).
En for_each sur une map de catalogues, ajouter un catalogue de recette ou de production devient une ligne dans un .tfvars.
La recette peut tester la version v1.4.0 du module pendant que la production reste en v1.3.0.
À retenir
- Module = dossier réutilisable : variables en entrée, outputs en sortie, pas de bloc provider.
- Versionner les modules (tags Git, registre) et figer la version par environnement.
for_eachsur un module pour déployer N fois la même brique.movedpour refactoriser sans recréer.- Prod : dossier et backend dédiés ; workspaces CLI pour des copies jetables.
Auto-évaluation
Q1. Pourquoi un module enfant ne doit-il pas contenir de bloc provider ?
→ Il hérite des providers du module racine ; déclarer un bloc provider dans le module empêche notamment de l'utiliser avec for_each/count et de lui passer un autre provider.
Q2. Tu ranges un catalogue existant dans un module. Comment éviter sa recréation ?
→ Ajouter un bloc moved de l'ancienne adresse vers la nouvelle, puis vérifier « 0 to destroy » dans le plan.
Q3. Pourquoi les workspaces CLI ne suffisent-ils pas à isoler la production ?
→ Ils partagent backend et identifiants : n'importe qui pouvant appliquer en dev peut sélectionner prd par erreur.
Q4. Comment épingler un module Git sur une version ?
→ Avec ?ref=v1.3.0 (tag) dans la source, puis terraform init -upgrade lors d'une montée de version.