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

Créer un module Terraform : votre premier code réutilisable

14 min de lecture

logo terraform

Vous copiez-collez les mêmes blocs de ressources entre projets ? Un module Terraform résout ce problème : il regroupe des ressources dans un répertoire dédié et les expose comme un bloc réutilisable. Vous l’appelez avec un bloc module, vous passez des paramètres, et Terraform crée les ressources pour vous — sans dupliquer une seule ligne.

Ce guide vous montre comment créer votre premier module, l’appeler depuis une configuration racine, et vérifier que l’infrastructure se déploie correctement. À la fin, vous serez capable de factoriser n’importe quelle configuration répétitive en module.

Prérequis : Terraform ≥ 1.11 installé, KVM/libvirt opérationnel. Avoir suivi les guides sur la déclaration de ressources et les variables Terraform.

  • Comprendre ce qu’est un module et pourquoi il est utile
  • Créer un module réseau avec ses fichiers standards
  • Appeler ce module depuis une configuration racine avec le bloc module
  • Réutiliser le même module avec des paramètres différents
  • Diagnostiquer les erreurs courantes liées aux modules

La logique est simple : « un module, c’est un dossier contenant des fichiers .tf ».

En réalité, chaque projet Terraform est déjà un module — le module racine. Quand vous écrivez main.tf, variables.tf et outputs.tf dans un répertoire, vous avez un module. La seule différence avec un « sous-module » est que celui-ci est appelé par un autre module via le bloc module.

Sans module, un projet qui crée 3 réseaux identiques avec des paramètres différents doit tripler le code de la ressource. Chaque modification — un changement de masque, un ajout d’option DNS — doit être faite 3 fois.

Avec un module :

Sans moduleAvec module
25 lignes × 3 réseaux = 75 lignes25 lignes dans le module + 3 appels de 8 lignes = 49 lignes
Modifier = 3 endroits à mettre à jourModifier = 1 seul endroit
Risque d’incohérence entre copiesComportement garanti identique

Plus le nombre de réseaux (ou de ressources) augmente, plus l’avantage grandit.

Un module réseau minimal contient 4 fichiers dans un sous-dossier modules/reseau/ :

mon-projet/
├── main.tf # ← configuration racine (appelle le module)
├── outputs.tf
├── versions.tf
└── modules/
└── reseau/
├── main.tf # ← ressource(s) du module
├── variables.tf
├── outputs.tf
└── versions.tf
  1. Créer modules/reseau/versions.tf — les contraintes de version du module :

    terraform {
    required_version = ">= 1.11.0"
    required_providers {
    libvirt = {
    source = "dmacvicar/libvirt"
    version = "~> 0.8"
    }
    }
    }

    Le module déclare ses propres contraintes de provider. C’est la configuration racine qui instancie le provider (provider "libvirt" { ... }), mais le module indique de quel provider il dépend.

  2. Créer modules/reseau/variables.tf — les paramètres du module :

    variable "nom" {
    type = string
    description = "Nom du réseau libvirt"
    }
    variable "adresse" {
    type = string
    description = "Adresse IP du réseau (ex: 10.10.80.0)"
    }
    variable "masque" {
    type = string
    description = "Masque de sous-réseau (ex: 255.255.255.0)"
    default = "255.255.255.0"
    }
    variable "dhcp_debut" {
    type = string
    description = "Première adresse DHCP"
    }
    variable "dhcp_fin" {
    type = string
    description = "Dernière adresse DHCP"
    }

    Chaque variable a un type, une description et éventuellement une valeur par défaut. Les variables sans default sont obligatoires — l’appelant doit fournir une valeur.

  3. Créer modules/reseau/main.tf — la ressource que le module crée :

    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
    }]
    }
    }]
    }

    Le code utilise var.nom, var.adresse, etc. — les valeurs viendront de l’appelant.

  4. Créer modules/reseau/outputs.tf — les valeurs que le module expose :

    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éé"
    }

    Sans outputs, le module racine ne peut pas accéder aux attributs des ressources créées par le module. Les outputs sont le contrat de sortie du module.

La configuration racine se trouve dans le répertoire parent. Elle instancie le provider et appelle le module avec un bloc module :

versions.tf (racine) :

terraform {
required_version = ">= 1.11.0"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}

main.tf (racine) :

module "reseau_lab" {
source = "./modules/reseau"
# ↑ chemin relatif vers le dossier du module
nom = "lab-module"
adresse = "10.10.80.0"
masque = "255.255.255.0"
dhcp_debut = "10.10.80.10"
dhcp_fin = "10.10.80.254"
}

Le bloc module contient deux types d’informations :

  • source : le chemin vers le dossier du module (ici ./modules/reseau)
  • Les arguments : chaque variable déclarée dans le module (nom, adresse, etc.) reçoit une valeur

outputs.tf (racine) :

output "reseau_id" {
value = module.reseau_lab.id
# ↑ module.<NOM_DU_BLOC>.<NOM_OUTPUT>
}
output "reseau_nom" {
value = module.reseau_lab.nom
}

Pour lire un output de module, la syntaxe est module.<nom_du_bloc>.<nom_output>.

  1. Initialiser — Terraform détecte et installe le module local :

    Fenêtre de terminal
    terraform init
    Initializing modules...
    - reseau_lab in modules/reseau
    Terraform has been successfully initialized!

    La ligne reseau_lab in modules/reseau confirme que Terraform a trouvé le module.

  2. Vérifier le plan :

    Fenêtre de terminal
    terraform plan
    # module.reseau_lab.libvirt_network.this will be created
    + resource "libvirt_network" "this" {
    + autostart = true
    + forward = { mode = "nat" }
    + ips = [{ address = "10.10.80.0", netmask = "255.255.255.0", ... }]
    + name = "lab-module"
    }
    Plan: 1 to add, 0 to change, 0 to destroy.

    Le préfixe module.reseau_lab. apparaît devant chaque ressource — c’est comme ça que Terraform identifie les ressources créées par un module dans le state.

  3. Déployer :

    Fenêtre de terminal
    terraform apply -auto-approve
    module.reseau_lab.libvirt_network.this: Creating...
    module.reseau_lab.libvirt_network.this: Creation complete after 0s [id=6cf43294-...]
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    Outputs:
    reseau_id = "6cf43294-b6c9-40c6-8811-f517fcfbe2e1"
    reseau_nom = "lab-module"
  4. Vérifier que le réseau existe bien :

    Fenêtre de terminal
    virsh net-list --all | grep lab-module
    lab-module active yes yes

    Le réseau est actif, en autostart, avec DHCP activé.

L’intérêt principal d’un module est la réutilisabilité. Un deuxième appel au même module, avec des paramètres différents, crée un deuxième réseau sans toucher au premier :

module "reseau_dev" {
source = "./modules/reseau"
nom = "dev-network"
adresse = "10.10.81.0"
masque = "255.255.255.0"
dhcp_debut = "10.10.81.10"
dhcp_fin = "10.10.81.254"
}

Après le terraform init et terraform apply, les deux réseaux coexistent :

Fenêtre de terminal
virsh net-list --all | grep -E "lab-module|dev-network"
dev-network active yes yes
lab-module active yes yes

Le state montre les deux modules avec leurs préfixes distincts :

Fenêtre de terminal
terraform state list
module.reseau_dev.libvirt_network.this
module.reseau_lab.libvirt_network.this

Chaque instance du module a son propre espace dans le state. Les deux réseaux sont indépendants — modifier ou détruire l’un ne touche pas l’autre.

Récapitulons la syntaxe du bloc module :

module "nom_unique" {
source = "./chemin/vers/module"
# ↑ obligatoire : où trouver le code du module
# Arguments = valeurs des variables du module
variable_1 = "valeur"
variable_2 = 42
}
ÉlémentRôleObligatoire
"nom_unique"Identifiant du bloc module (libre, doit être unique)Oui
sourceChemin vers le dossier du moduleOui
ArgumentsValeurs passées aux variable du moduleSi la variable n’a pas de default

La valeur de source peut être :

  • Un chemin local : ./modules/reseau ou ../modules-partages/reseau
  • Une URL Git : git::https://gitlab.example.com/infra/modules.git//reseau
  • Un module du Terraform Registry : hashicorp/consul/aws

Ce guide couvre les chemins locaux. Les autres sources sont traitées dans les guides suivants.

SymptômeCause probableSolution
Error: Module not installedNouveau bloc module sans terraform initRelancer terraform init
Error: No value for required variableVariable sans default non fournieAjouter l’argument manquant dans le bloc module
Error: Unsupported argumentArgument qui ne correspond à aucune variableVérifier le nom dans variables.tf du module
Ressource non préfixée dans le stateLe code n’est pas dans un moduleVérifier que la ressource est dans le dossier du module, pas à la racine
  1. Un module est un dossier de fichiers .tf — chaque projet est déjà un module (le module racine)
  2. Un module standard contient 4 fichiers : versions.tf, variables.tf, main.tf, outputs.tf
  3. Le bloc module dans la configuration racine appelle le module et lui passe des valeurs
  4. L’argument source est obligatoire — il pointe vers le chemin du module
  5. Après chaque ajout de bloc module, il faut relancer terraform init
  6. Les ressources du module apparaissent dans le state avec le préfixe module.<nom>.
  7. Réutiliser un module = un nouveau bloc module avec des paramètres différents — zéro duplication de code

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