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

Utiliser un module Terraform local partagé entre projets

12 min de lecture

logo terraform

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.

  • Organiser un dossier de modules partagés entre projets
  • Référencer un module local avec source et un chemin relatif
  • Déployer deux projets indépendants à partir du même module
  • Comprendre que chaque projet possède son propre state

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.tf

Le 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.

Le module réseau reprend la structure standard vue dans les guides précédents :

  1. versions.tf — contraintes de version sans provider :

    terraform {
    required_version = ">= 1.11.0"
    required_providers {
    libvirt = {
    source = "dmacvicar/libvirt"
    version = "~> 0.8"
    }
    }
    }
  2. variables.tf — les entrées du module avec validations :

    variable "nom" {
    type = string
    description = "Nom du réseau libvirt"
    validation {
    condition = length(var.nom) >= 3 && length(var.nom) <= 30
    error_message = "Le nom doit contenir entre 3 et 30 caractères."
    }
    }
    variable "cidr" {
    type = object({
    adresse = string
    masque = string
    })
    description = "Bloc CIDR du réseau"
    }
    variable "plage_dhcp" {
    type = object({
    debut = string
    fin = string
    })
    description = "Plage DHCP"
    }
    variable "activer_dns" {
    type = bool
    default = true
    }
    variable "mode_forward" {
    type = string
    default = "nat"
    validation {
    condition = contains(["nat", "route", "bridge", "none"], var.mode_forward)
    error_message = "Le mode doit être nat, route, bridge ou none."
    }
    }
  3. main.tf — la ressource paramétrée :

    resource "libvirt_network" "this" {
    name = var.nom
    autostart = true
    forward = {
    mode = var.mode_forward
    }
    dns = {
    enabled = var.activer_dns
    }
    ips = [{
    address = var.cidr.adresse
    netmask = var.cidr.masque
    dhcp = {
    ranges = [{
    start = var.plage_dhcp.debut
    end = var.plage_dhcp.fin
    }]
    }
    }]
    }
  4. outputs.tf — les sorties exploitables par l’appelant :

    output "id" {
    value = libvirt_network.this.id
    description = "UUID du réseau"
    }
    output "nom" {
    value = libvirt_network.this.name
    description = "Nom du réseau"
    }
    output "configuration" {
    value = {
    adresse = var.cidr.adresse
    masque = var.cidr.masque
    dns_actif = var.activer_dns
    mode = var.mode_forward
    }
    description = "Résumé de la configuration"
    }

Le projet-dev crée un réseau dev-local en mode nat (valeur par défaut) :

projet-dev/main.tf
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 :

projet-dev/outputs.tf
output "reseau_id" {
value = module.reseau.id
}
output "reseau_config" {
value = module.reseau.configuration
}

Le même module avec des paramètres différentsDNS désactivé et mode route :

projet-staging/main.tf
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.

Lors du terraform init, Terraform détecte le module local :

Fenêtre de terminal
$ cd projet-dev
$ terraform init
Initializing modules...
- reseau in ../modules-partages/reseau

La ligne - reseau in ../modules-partages/reseau confirme que le module est bien chargé depuis le chemin relatif.

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

Les deux réseaux coexistent sur l’hyperviseur :

$ virsh net-list
dev-local active yes yes
staging-local active yes yes

Chaque projet possède son propre fichier terraform.tfstate. Les terraform state list le confirment :

$ terraform -chdir=projet-dev state list
module.reseau.libvirt_network.this
$ terraform -chdir=projet-staging state list
module.reseau.libvirt_network.this

La 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.

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 :

DepuisSourceRésolution
projet-dev/main.tf../modules-partages/reseaumodules-partages/reseau/
projet-staging/main.tf../modules-partages/reseaumodules-partages/reseau/ (identique)
infra/prod/main.tf../../modules/reseauRemonte 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 :

Fenêtre de terminal
cd projet-dev
ls ../modules-partages/reseau

Si 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 :

Fenêtre de terminal
cat .terraform/modules/modules.json

Vous 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 ../.

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.

AspectModule intégré (./modules/)Module local partagé (../modules-partages/)
EmplacementDans le projetHors du projet, dossier partagé
RéutilisationUn seul projetPlusieurs projets
VersionnementMême commit que le projetGéré séparément (ou même dépôt)
terraform initCopie localeLecture depuis le chemin
Cas d’usageModule spécifique au projetBibliothèque d’équipe
SymptômeCause probableSolution
Module not installedterraform init n’a pas été relancéRelancer terraform init
Module source ... not foundChemin relatif incorrectVérifier le chemin depuis le fichier main.tf
Error: Module not installed après déplacementLe dossier .terraform/modules pointe vers l’ancien cheminSupprimer .terraform/ et relancer terraform init
Modification du module non prise en compteTerraform cache la résolutionRelancer terraform init puis terraform plan
  1. Un module local est référencé par un chemin relatif dans source
  2. Le module partagé ne contient jamais de bloc provider — c’est le projet appelant qui le configure
  3. Chaque projet a son propre state — les ressources sont totalement indépendantes
  4. Le chemin est résolu par rapport au fichier contenant le bloc module
  5. Après modification du module partagé, relancer terraform init dans chaque projet
  6. Le modèle modules-partages/ + projet-X/ est idéal pour les équipes qui partagent des conventions

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