ℹ️ À 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

Le module racine passe des variables aux modules enfants et relie leurs outputs
Le module racine passe des variables aux modules enfants et relie leurs outputs

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"] } }
SourceSyntaxeVersion
Local./modules/x ou ../../modules/xCelle du dépôt courant
Gitgit::https://…/repo.git//chemin?ref=v1.3.0Tag ou commit dans ref
Registre public / privéAzure/avm-res-network-virtualnetwork/azurermArgument version = "~> 0.4"
HCP Terraformapp.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

Recommandé : un dossier et un backend par environnement, des modules partagés et versionnés
Recommandé : un dossier et un backend par environnement, des modules partagés et versionnés
ApprochePrincipeAvantages / limites
Dossiers par environnementenvs/dev, envs/prd : chacun son backend, ses .tfvars, ses versions de modulesIsolation forte, prod peut rester sur une version plus ancienne ; un peu de duplication
Workspaces CLIMême code, plusieurs states : terraform workspace new rec, terraform.workspace dans le codeSimple pour des copies identiques ; même backend et mêmes droits pour tous, risque d'appliquer sur le mauvais workspace
Backend partielterraform init -backend-config=prd.backend.hclUn seul code, backend choisi à l'init ; demande de la rigueur en CI
Terragrunt / HCP TerraformOutils dédiés à l'orchestration multi-environnementsPuissants ; 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ômeCauseParade
« Module not installed »terraform init non relancé après ajout de moduleterraform init (ou -upgrade)
Le plan détruit tout après refactoringAdresses changées sans bloc movedAjouter moved (ou terraform state mv)
Module difficile à réutiliserValeurs codées en dur, trop de responsabilitésUne brique = un module ; tout ce qui varie en variable
Changement en prod non vouluProd pointée sur ref=mainTags sémantiques, montée de version explicite par environnement
Apply sur le mauvais environnementWorkspace 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_each sur un module pour déployer N fois la même brique.
  • moved pour 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.

Pour aller plus loin