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

Structure standard d'un module Terraform : les 4 fichiers essentiels

11 min de lecture

logo terraform

Un module Terraform fonctionne avec un seul fichier — mais personne ne fait ça. La convention universelle consiste à découper le module en 4 fichiers distincts, chacun ayant un rôle précis. Ce découpage ne change rien au comportement de Terraform (qui charge tous les .tf d’un dossier), mais il change tout pour les humains qui lisent et maintiennent le code.

Ce guide détaille chaque fichier, son rôle, et montre concrètement la différence entre un module monolithique et un module correctement structuré.

Prérequis : avoir lu Créer un module Terraform et compris le bloc module.

  • Nommer les 4 fichiers standards d’un module et leur rôle
  • Comparer un module monolithique vs un module découpé
  • Reconnaître la structure d’un module au premier coup d’oeil
  • Appliquer les conventions de nommage utilisées dans l’écosystème Terraform

Pourquoi découper un module en plusieurs fichiers

Section intitulée « Pourquoi découper un module en plusieurs fichiers »

Terraform traite chaque dossier comme un module. Il lit tous les fichiers .tf du dossier et les fusionne. Que vous ayez 1 fichier de 70 lignes ou 4 fichiers de 20 lignes chacun, le résultat est strictement identique.

Alors pourquoi découper ? Pour la lisibilité. Quand un collègue ouvre votre module, il doit pouvoir répondre en 5 secondes à ces questions :

QuestionFichier à ouvrir
Quels providers le module utilise-t-il ?versions.tf
Quels paramètres dois-je fournir ?variables.tf
Que fait concrètement le module ?main.tf
Quelles valeurs le module expose-t-il ?outputs.tf

Avec un seul fichier, il faut parcourir 70 lignes pour trouver ces réponses. Avec 4 fichiers, le nom du fichier est la réponse.

terraform {
required_version = ">= 1.11.0"
# ↑ version minimale de Terraform pour utiliser ce module
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
# ↑ contrainte de version du provider
}
}
}

Ce fichier déclare deux choses :

  • La version minimale de Terraform requise
  • Les providers dont le module dépend, avec leurs contraintes de version

Le module ne configure pas le provider (pas de bloc provider "libvirt" { ... }). C’est la configuration racine qui le fait. Le module dit simplement « j’ai besoin de ce provider, dans cette plage de versions ».

variable "nom" {
type = string
description = "Nom du réseau"
# ↑ pas de default → variable obligatoire
}
variable "adresse" {
type = string
description = "Adresse du réseau (ex: 10.10.80.0)"
}
variable "masque" {
type = string
description = "Masque de sous-réseau"
default = "255.255.255.0"
# ↑ default présent → variable optionnelle
}
variable "dhcp_debut" {
type = string
description = "Première adresse DHCP"
}
variable "dhcp_fin" {
type = string
description = "Dernière adresse DHCP"
}

Ce fichier rassemble toutes les variables du module. Pour chaque variable :

  • type : le type de donnée attendu (string, number, bool, list, map, object)
  • description : ce à quoi sert la variable — indispensable pour la documentation
  • default (optionnel) : si présent, la variable est optionnelle ; sinon, l’appelant doit fournir une valeur
resource "libvirt_network" "this" {
name = var.nom
autostart = true
forward = {
mode = "nat"
}
dns = {
enabled = true
}
ips = [{
address = var.adresse
netmask = var.masque
dhcp = {
ranges = [{
start = var.dhcp_debut
end = var.dhcp_fin
}]
}
}]
}

Ce fichier contient les ressources que le module crée. C’est la logique métier. Les valeurs dynamiques viennent des variables (var.nom, var.adresse, etc.).

Pour un module simple, un seul main.tf suffit. Pour un module complexe (réseau + volume + VM), on peut découper en fichiers supplémentaires par domaine : network.tf, storage.tf, compute.tf.

output "id" {
value = libvirt_network.this.id
description = "UUID du réseau créé"
}
output "nom" {
value = libvirt_network.this.name
description = "Nom du réseau créé"
}

Les outputs sont le contrat de sortie du module. Sans eux, la configuration racine ne peut pas accéder aux attributs des ressources créées par le module. C’est la seule façon de « passer des informations » d’un module vers son appelant.

Pour rendre la différence concrète, voici le même module dans les deux approches.

modules/
└── reseau/
└── main.tf ← 69 lignes (tout dedans)

En ouvrant main.tf, on trouve dans l’ordre : le bloc terraform, 5 blocs variable, 1 bloc resource, 2 blocs output. Pour trouver les variables, il faut scroller. Pour trouver les outputs, il faut aller à la fin.

modules/
└── reseau/
├── versions.tf ← 10 lignes
├── variables.tf ← 25 lignes
├── main.tf ← 23 lignes
└── outputs.tf ← 9 lignes

Chaque fichier a une seule responsabilité. Le développeur qui veut connaître les paramètres ouvre variables.tf — il n’a pas besoin de lire le reste.

Les deux approches passent terraform validate et produisent le même plan. Terraform n’impose aucune convention de nommage de fichiers. Le découpage est une convention humaine, pas une contrainte technique.

Fenêtre de terminal
terraform validate
# → Success! The configuration is valid.

Au-delà des 4 fichiers standards, certains modules incluent :

FichierRôleQuand l’ajouter
README.mdDocumentation du module (paramètres, exemples)Toujours pour un module partagé
locals.tfVariables locales calculéesQuand le module a des expressions complexes
data.tfSources de données (data blocks)Quand le module lit des ressources existantes
network.tf, compute.tfDécoupage de main.tf par domaineModule complexe (> 5 ressources)

Convention de nommage des ressources dans un module

Section intitulée « Convention de nommage des ressources dans un module »

Dans un module, la ressource principale porte souvent le nom this :

resource "libvirt_network" "this" { ... }

Pourquoi this ? Parce que le module est déjà nommé (module "reseau_lab"). L’identifiant complet dans le state sera module.reseau_lab.libvirt_network.this — le contexte est suffisant. Appeler la ressource reseau serait redondant (module.reseau_lab.libvirt_network.reseau).

Si le module crée plusieurs ressources, utilisez des noms descriptifs pour les distinguer :

resource "libvirt_volume" "disque_systeme" { ... }
resource "libvirt_volume" "disque_donnees" { ... }
SymptômeCause probableSolution
Terraform ignore un fichier .tfFichier dans un sous-dossier non référencéLes .tf doivent être à la racine du dossier module, pas dans un sous-dossier
Variable non trouvée dans main.tfVariable déclarée dans un autre fichierNormal — Terraform charge tous les .tf du dossier. Vérifier l’orthographe
Error: provider configuration not presentBloc provider manquantLe provider se configure dans la racine, pas dans le module
  1. Un module Terraform standard contient 4 fichiers : versions.tf, variables.tf, main.tf, outputs.tf
  2. Terraform charge tous les .tf d’un dossier — le découpage est une convention, pas une obligation
  3. Le découpage permet de répondre en 5 secondes à « quels paramètres ? », « que fait-il ? », « que renvoie-t-il ? »
  4. Ne jamais mettre de bloc provider dans un module réutilisable
  5. La ressource principale d’un module se nomme souvent this
  6. Pour un module complexe, découper main.tf en fichiers par domaine (network.tf, compute.tf)

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