Aller au contenu
Infrastructure as Code medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

Anti-patterns des modules Terraform : les erreurs à éviter

11 min de lecture

logo terraform

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.

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.

modules/infrastructure/ — anti-pattern
# 15 variables, 8 ressources de types différents, 300+ lignes
resource "libvirt_network" "this" { ... }
resource "libvirt_volume" "base" { ... }
resource "libvirt_volume" "disk" { ... }
resource "libvirt_domain" "vm" { ... }
# + firewall, DNS, monitoring...
  • 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

Découpez en modules à responsabilité unique et chaînez-les via les outputs :

Configuration racine — correct
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
}
modules/reseau/versions.tf — anti-pattern
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
# ↑ ERREUR : le provider est figé dans le module
}

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.

Le module déclare les contraintes, la configuration racine configure le provider :

modules/reseau/versions.tf — correct
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
# Pas de bloc provider ici
Configuration racine — correct
provider "libvirt" {
uri = "qemu:///system"
# ↑ configuré par l'appelant, pas par le module
}

Le module crée des ressources mais n’expose aucun output :

modules/reseau/outputs.tf — anti-pattern
# Fichier vide ou absent

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.

Exposez systématiquement les identifiants, noms et attributs calculés :

modules/reseau/outputs.tf — correct
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.

Un module qui ne fait que passer les arguments à une seule ressource sans ajouter de valeur :

modules/reseau/main.tf — anti-pattern
resource "libvirt_network" "this" {
name = var.nom
# ... recopie exacte de toutes les variables en attributs
# Aucune logique, aucun default, aucune validation
}
modules/reseau/variables.tf — anti-pattern
variable "nom" { type = string }
# ... une variable pour chaque attribut de la ressource, sans defaults ni validations

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.

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)
variables.tf — anti-pattern
variable "config" {
# Pas de type → type = any
default = {}
}
  • Pas de validation de type à terraform validate
  • L’appelant ne sait pas quelles clés sont attendues
  • Les erreurs apparaissent tard, au moment du plan ou de l’apply
variables.tf — correct
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.

modules/reseau/main.tf — anti-pattern
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
}
}

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.

Remplacez les valeurs hardcodées par des variables (avec des defaults pour les valeurs les plus courantes) :

modules/reseau/main.tf — correct
resource "libvirt_network" "this" {
name = var.nom
autostart = true
forward = {
mode = var.mode_forward
# ↑ paramétrable, avec default "nat" dans variables.tf
}
}

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.

  • 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.xxx dans 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

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.

Anti-patternSymptômeCorrection
God object10+ variables, 5+ types de ressourcesDécouper en modules à responsabilité unique
Provider hardcodéprovider {} dans le moduleSupprimer le bloc, configurer dans la racine
Outputs manquantsImpossible de chaîner les modulesExposer IDs, noms, attributs calculés
Wrapper inutileModule = 1 ressource sans valeur ajoutéeUtiliser la ressource directement
Variables sans typetype absent ou anyType explicite + description
Valeurs hardcodéesNoms/modes figés dans main.tfVariables avec defaults sensibles
Noms non descriptifsmodule1, infra, indexNoms qui décrivent la responsabilité
  1. Le module God est l’anti-pattern le plus courant — découpez en modules à responsabilité unique
  2. Le bloc provider dans un module empêche la réutilisation dans différents contextes
  3. Les outputs manquants forcent des contournements fragiles
  4. Un module wrapper n’a de sens que s’il ajoute des defaults, validations ou composition
  5. Les types explicites sont la documentation de l’interface publique
  6. Les valeurs hardcodées éliminent tout intérêt de la réutilisation

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn