
Un anti-pattern est une solution qui semble raisonnable mais crée des problèmes à long terme. Dans les modules Terraform, les anti-patterns rendent le code fragile, difficile à tester et impossible à réutiliser. Ce guide liste les erreurs les plus fréquentes, explique pourquoi elles posent problème, et montre comment les corriger.
Prérequis : avoir lu Bonnes pratiques modules.
1. Le module “God object”
Section intitulée « 1. Le module “God object” »Le problème
Section intitulée « Le problème »Un module qui crée tout : réseau, volumes, VMs, DNS, firewall, monitoring. Ce module a souvent des dizaines de variables et des centaines de lignes.
# 15 variables, 8 ressources de types différents, 300+ lignesresource "libvirt_network" "this" { ... }resource "libvirt_volume" "base" { ... }resource "libvirt_volume" "disk" { ... }resource "libvirt_domain" "vm" { ... }# + firewall, DNS, monitoring...Pourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »- Impossible à tester en isolation — il faut toute l’infrastructure pour tester un changement réseau
- Impossible à réutiliser — si un autre projet a besoin uniquement du réseau, il doit prendre tout le module
- Effets de bord — modifier une variable réseau peut casser une VM
La solution
Section intitulée « La solution »Découpez en modules à responsabilité unique et chaînez-les via les outputs :
module "reseau" { source = "./modules/reseau" nom = "lab" # ...}
module "vm" { source = "./modules/vm" network_name = module.reseau.nom # ↑ chaînage : l'output du réseau devient l'input de la VM}2. Le provider hardcodé dans le module
Section intitulée « 2. Le provider hardcodé dans le module »Le problème
Section intitulée « Le problème »terraform { required_providers { libvirt = { source = "dmacvicar/libvirt" version = "~> 0.8" } }}
provider "libvirt" { uri = "qemu:///system" # ↑ ERREUR : le provider est figé dans le module}Pourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »Le module ne peut pas être utilisé avec un provider distant (qemu+ssh://) ni dans un environnement CI où l’URI est différente. Le module impose une configuration qui devrait être décidée par l’appelant.
La solution
Section intitulée « La solution »Le module déclare les contraintes, la configuration racine configure le provider :
terraform { required_providers { libvirt = { source = "dmacvicar/libvirt" version = "~> 0.8" } }}# Pas de bloc provider iciprovider "libvirt" { uri = "qemu:///system" # ↑ configuré par l'appelant, pas par le module}3. Les outputs manquants
Section intitulée « 3. Les outputs manquants »Le problème
Section intitulée « Le problème »Le module crée des ressources mais n’expose aucun output :
# Fichier vide ou absentPourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »Sans outputs, l’appelant ne peut pas :
- Chaîner les modules entre eux
- Afficher les identifiants après
terraform apply - Référencer les ressources créées dans d’autres modules
L’appelant est forcé de contourner le problème avec des data sources ou du parsing de state — deux solutions fragiles.
La solution
Section intitulée « La solution »Exposez systématiquement les identifiants, noms et attributs calculés :
output "id" { value = libvirt_network.this.id description = "UUID du réseau"}
output "nom" { value = libvirt_network.this.name description = "Nom du réseau"}La règle est : si un module consommateur pourrait en avoir besoin, exposez-le.
4. Le module “wrapper” inutile
Section intitulée « 4. Le module “wrapper” inutile »Le problème
Section intitulée « Le problème »Un module qui ne fait que passer les arguments à une seule ressource sans ajouter de valeur :
resource "libvirt_network" "this" { name = var.nom # ... recopie exacte de toutes les variables en attributs # Aucune logique, aucun default, aucune validation}variable "nom" { type = string }# ... une variable pour chaque attribut de la ressource, sans defaults ni validationsPourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »Ce module n’apporte rien par rapport à utiliser la ressource directement. Il ajoute une indirection sans valeur ajoutée — plus de fichiers à maintenir, plus de confusion.
Quand un module wrapper a du sens
Section intitulée « Quand un module wrapper a du sens »Un module est justifié quand il ajoute au moins une de ces valeurs :
- Defaults sensibles :
mode_forward = "nat"par défaut - Validations :
length(var.nom) >= 3 - Composition : assemble plusieurs ressources liées
- Abstraction : masque la complexité (ex: calcul de sous-réseaux)
5. Les variables “fourre-tout” sans type
Section intitulée « 5. Les variables “fourre-tout” sans type »Le problème
Section intitulée « Le problème »variable "config" { # Pas de type → type = any default = {}}Pourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »- Pas de validation de type à
terraform validate - L’appelant ne sait pas quelles clés sont attendues
- Les erreurs apparaissent tard, au moment du
planou de l’apply
La solution
Section intitulée « La solution »variable "cidr" { type = object({ adresse = string masque = string }) description = "Bloc CIDR du réseau (adresse + masque)"}Le type explicite documente l’interface et détecte les erreurs tôt.
6. Les valeurs hardcodées dans le module
Section intitulée « 6. Les valeurs hardcodées dans le module »Le problème
Section intitulée « Le problème »resource "libvirt_network" "this" { name = "mon-reseau" # ↑ hardcodé : chaque projet crée un réseau avec le même nom autostart = true
forward = { mode = "nat" # ↑ hardcodé : impossible de changer le mode }}Pourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »Le module n’est pas paramétrable. Chaque utilisation crée exactement la même chose — ce qui élimine tout intérêt de la réutilisation.
La solution
Section intitulée « La solution »Remplacez les valeurs hardcodées par des variables (avec des defaults pour les valeurs les plus courantes) :
resource "libvirt_network" "this" { name = var.nom autostart = true
forward = { mode = var.mode_forward # ↑ paramétrable, avec default "nat" dans variables.tf }}7. Les noms flous et dossiers génériques
Section intitulée « 7. Les noms flous et dossiers génériques »Le problème
Section intitulée « Le problème »Ranger un module dans un dossier générique (infra/, module1/, index/) ou utiliser des noms qui ne décrivent pas sa responsabilité :
modules/├── module1/├── module2/└── infra/Vu depuis la configuration racine, on obtient vite des appels difficiles à relire :
module "infra" { source = "./modules/infra"}
module "module2" { source = "./modules/module2"}À la lecture du plan ou d’un terraform state list, il devient difficile de savoir ce que ces modules créent réellement.
Pourquoi c’est un problème
Section intitulée « Pourquoi c’est un problème »- Le nom du dossier n’aide pas à comprendre la responsabilité du module
- Les revues de code deviennent plus lentes, car il faut ouvrir le module pour savoir ce qu’il fait
- Les appels
module.xxxdans la configuration racine deviennent vagues et peu maintenables - Deux équipes peuvent donner des sens différents à un même nom générique comme
infra
La solution
Section intitulée « La solution »Des noms clairs qui décrivent la responsabilité :
modules/├── reseau/├── volume/└── vm/Le bon test est simple : si vous lisez module.reseau, module.vm ou module.volume, vous comprenez déjà l’intention sans ouvrir le dossier.
Tableau récapitulatif
Section intitulée « Tableau récapitulatif »| Anti-pattern | Symptôme | Correction |
|---|---|---|
| God object | 10+ variables, 5+ types de ressources | Découper en modules à responsabilité unique |
| Provider hardcodé | provider {} dans le module | Supprimer le bloc, configurer dans la racine |
| Outputs manquants | Impossible de chaîner les modules | Exposer IDs, noms, attributs calculés |
| Wrapper inutile | Module = 1 ressource sans valeur ajoutée | Utiliser la ressource directement |
| Variables sans type | type absent ou any | Type explicite + description |
| Valeurs hardcodées | Noms/modes figés dans main.tf | Variables avec defaults sensibles |
| Noms non descriptifs | module1, infra, index | Noms qui décrivent la responsabilité |
À retenir
Section intitulée « À retenir »- Le module God est l’anti-pattern le plus courant — découpez en modules à responsabilité unique
- Le bloc
providerdans un module empêche la réutilisation dans différents contextes - Les outputs manquants forcent des contournements fragiles
- Un module wrapper n’a de sens que s’il ajoute des defaults, validations ou composition
- Les types explicites sont la documentation de l’interface publique
- Les valeurs hardcodées éliminent tout intérêt de la réutilisation