
Un module local est un module stocké sur le disque, référencé par un chemin relatif. C’est la méthode la plus simple pour partager du code Terraform entre plusieurs projets sans Registry ni Git. Vous placez le module dans un dossier accessible, et chaque projet le consomme via source = "../chemin/vers/module".
Prérequis : avoir lu Créer un module Terraform et Variables et outputs d’un module.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Organiser un dossier de modules partagés entre projets
- Référencer un module local avec
sourceet un chemin relatif - Déployer deux projets indépendants à partir du même module
- Comprendre que chaque projet possède son propre state
Le principe : un module, plusieurs projets
Section intitulée « Le principe : un module, plusieurs projets »Le schéma classique est le suivant :
mon-infra/├── modules-partages/ ← bibliothèque de modules│ └── reseau/│ ├── versions.tf│ ├── variables.tf│ ├── main.tf│ └── outputs.tf├── projet-dev/ ← projet 1 (son propre state)│ ├── versions.tf│ ├── main.tf│ └── outputs.tf└── projet-staging/ ← projet 2 (son propre state) ├── versions.tf ├── main.tf └── outputs.tfLe dossier modules-partages/reseau/ contient le code du module une seule fois. Les deux projets le consomment via un chemin relatif (../modules-partages/reseau). Chaque projet a son propre terraform.tfstate — les ressources sont totalement indépendantes.
Créer le module partagé
Section intitulée « Créer le module partagé »Le module réseau reprend la structure standard vue dans les guides précédents :
-
versions.tf— contraintes de version sansprovider:terraform {required_version = ">= 1.11.0"required_providers {libvirt = {source = "dmacvicar/libvirt"version = "~> 0.8"}}} -
variables.tf— les entrées du module avec validations :variable "nom" {type = stringdescription = "Nom du réseau libvirt"validation {condition = length(var.nom) >= 3 && length(var.nom) <= 30error_message = "Le nom doit contenir entre 3 et 30 caractères."}}variable "cidr" {type = object({adresse = stringmasque = string})description = "Bloc CIDR du réseau"}variable "plage_dhcp" {type = object({debut = stringfin = string})description = "Plage DHCP"}variable "activer_dns" {type = booldefault = true}variable "mode_forward" {type = stringdefault = "nat"validation {condition = contains(["nat", "route", "bridge", "none"], var.mode_forward)error_message = "Le mode doit être nat, route, bridge ou none."}} -
main.tf— la ressource paramétrée :resource "libvirt_network" "this" {name = var.nomautostart = trueforward = {mode = var.mode_forward}dns = {enabled = var.activer_dns}ips = [{address = var.cidr.adressenetmask = var.cidr.masquedhcp = {ranges = [{start = var.plage_dhcp.debutend = var.plage_dhcp.fin}]}}]} -
outputs.tf— les sorties exploitables par l’appelant :output "id" {value = libvirt_network.this.iddescription = "UUID du réseau"}output "nom" {value = libvirt_network.this.namedescription = "Nom du réseau"}output "configuration" {value = {adresse = var.cidr.adressemasque = var.cidr.masquedns_actif = var.activer_dnsmode = var.mode_forward}description = "Résumé de la configuration"}
Consommer le module depuis un projet
Section intitulée « Consommer le module depuis un projet »Projet dev
Section intitulée « Projet dev »Le projet-dev crée un réseau dev-local en mode nat (valeur par défaut) :
module "reseau" { source = "../modules-partages/reseau" # ↑ chemin relatif vers le module
nom = "dev-local"
cidr = { adresse = "10.10.85.0" masque = "255.255.255.0" }
plage_dhcp = { debut = "10.10.85.10" fin = "10.10.85.100" }}Les outputs du module sont exposés dans outputs.tf :
output "reseau_id" { value = module.reseau.id}
output "reseau_config" { value = module.reseau.configuration}Projet staging
Section intitulée « Projet staging »Le même module avec des paramètres différents — DNS désactivé et mode route :
module "reseau" { source = "../modules-partages/reseau"
nom = "staging-local"
cidr = { adresse = "10.10.86.0" masque = "255.255.255.0" }
plage_dhcp = { debut = "10.10.86.10" fin = "10.10.86.100" }
activer_dns = false mode_forward = "route"}La seule différence est dans les valeurs passées au bloc module. Le code du module reste identique.
Déployer les deux projets
Section intitulée « Déployer les deux projets »Initialisation
Section intitulée « Initialisation »Lors du terraform init, Terraform détecte le module local :
$ cd projet-dev$ terraform init
Initializing modules...- reseau in ../modules-partages/reseauLa ligne - reseau in ../modules-partages/reseau confirme que le module est bien chargé depuis le chemin relatif.
Déploiement projet-dev
Section intitulée « Déploiement projet-dev »$ terraform apply
module.reseau.libvirt_network.this: Creating...module.reseau.libvirt_network.this: Creation complete after 0s [id=57a3795a-...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:reseau_config = { "adresse" = "10.10.85.0" "dns_actif" = true "masque" = "255.255.255.0" "mode" = "nat"}Déploiement projet-staging
Section intitulée « Déploiement projet-staging »$ cd ../projet-staging$ terraform init && terraform apply
module.reseau.libvirt_network.this: Creating...module.reseau.libvirt_network.this: Creation complete after 0s [id=59a1dd31-...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:reseau_config = { "adresse" = "10.10.86.0" "dns_actif" = false "masque" = "255.255.255.0" "mode" = "route"}Vérification
Section intitulée « Vérification »Les deux réseaux coexistent sur l’hyperviseur :
$ virsh net-list dev-local active yes yes staging-local active yes yesStates indépendants
Section intitulée « States indépendants »Chaque projet possède son propre fichier terraform.tfstate. Les terraform state list le confirment :
$ terraform -chdir=projet-dev state listmodule.reseau.libvirt_network.this
$ terraform -chdir=projet-staging state listmodule.reseau.libvirt_network.thisLa même adresse dans le state (module.reseau.libvirt_network.this), mais deux fichiers state séparés, dans deux dossiers différents. Détruire le projet-dev n’affecte pas le projet-staging.
Quand les chemins sont résolus
Section intitulée « Quand les chemins sont résolus »Un point technique important : avec un module local, terraform init résout le chemin source et enregistre cette information dans .terraform/modules/modules.json. Contrairement aux modules du Registry, il n’y a pas de téléchargement de paquet versionné : Terraform référence directement le dossier local indiqué dans source.
Le chemin source est résolu par rapport au fichier qui contient le bloc module :
| Depuis | Source | Résolution |
|---|---|---|
projet-dev/main.tf | ../modules-partages/reseau | modules-partages/reseau/ |
projet-staging/main.tf | ../modules-partages/reseau | modules-partages/reseau/ (identique) |
infra/prod/main.tf | ../../modules/reseau | Remonte de 2 niveaux |
Comment vérifier qu’un chemin local est correct
Section intitulée « Comment vérifier qu’un chemin local est correct »Avant même le terraform init, vous pouvez vérifier le chemin manuellement depuis le dossier du projet appelant :
cd projet-devls ../modules-partages/reseauSi le chemin est correct, vous devez voir les fichiers du module (versions.tf, variables.tf, main.tf, outputs.tf). Si ls échoue, terraform init échouera aussi.
Après terraform init, vous pouvez vérifier la résolution réelle dans le manifeste Terraform :
cat .terraform/modules/modules.jsonVous y verrez une entrée du type :
{ "Key": "reseau", "Source": "../modules-partages/reseau", "Dir": "../modules-partages/reseau"}Cette vérification est utile quand plusieurs projets consomment le même module et que vous n’êtes plus sûr du bon niveau de ../.
Quand path.module devient utile
Section intitulée « Quand path.module devient utile »Le chemin du bloc module est résolu depuis la configuration racine. En revanche, à l’intérieur du module lui-même, si vous devez lire un fichier local (template, script cloud-init, fichier JSON), basez-vous sur path.module pour éviter les chemins fragiles.
locals { cloud_init = file("${path.module}/templates/cloud-init.yaml")}Sans path.module, un chemin relatif comme file("templates/cloud-init.yaml") dépend trop du contexte d’exécution et devient plus difficile à maintenir quand le module est réutilisé depuis plusieurs emplacements.
Module local vs module intégré
Section intitulée « Module local vs module intégré »| Aspect | Module intégré (./modules/) | Module local partagé (../modules-partages/) |
|---|---|---|
| Emplacement | Dans le projet | Hors du projet, dossier partagé |
| Réutilisation | Un seul projet | Plusieurs projets |
| Versionnement | Même commit que le projet | Géré séparément (ou même dépôt) |
terraform init | Copie locale | Lecture depuis le chemin |
| Cas d’usage | Module spécifique au projet | Bibliothèque d’équipe |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
Module not installed | terraform init n’a pas été relancé | Relancer terraform init |
Module source ... not found | Chemin relatif incorrect | Vérifier le chemin depuis le fichier main.tf |
Error: Module not installed après déplacement | Le dossier .terraform/modules pointe vers l’ancien chemin | Supprimer .terraform/ et relancer terraform init |
| Modification du module non prise en compte | Terraform cache la résolution | Relancer terraform init puis terraform plan |
À retenir
Section intitulée « À retenir »- Un module local est référencé par un chemin relatif dans
source - Le module partagé ne contient jamais de bloc
provider— c’est le projet appelant qui le configure - Chaque projet a son propre state — les ressources sont totalement indépendantes
- Le chemin est résolu par rapport au fichier contenant le bloc
module - Après modification du module partagé, relancer
terraform initdans chaque projet - Le modèle
modules-partages/+projet-X/est idéal pour les équipes qui partagent des conventions