
Un module local est un module posé sur le disque, appelé par un chemin
relatif. C'est la façon la plus directe de partager du code entre plusieurs
projets, sans Registry ni dépôt Git : le dossier est déjà là, chaque
projet le désigne par source = "../chemin/vers/module". Encore faut-il savoir ce
que Terraform en fait, parce que la mécanique n'est pas celle d'un module distant.
Tout ce qui suit a été exécuté sur Terraform v1.15.4 avec les providers
local et random : sorties, erreurs et contenu de modules.json
compris. Deux idées répandues n'y survivent pas, celle du cache à invalider
après avoir modifié un module local, et celle du chemin absolu qui serait un
chemin local comme un autre.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- La règle qui fait qu'un chemin est local, et rien d'autre
- Le montage bibliothèque plus projets, avec des states séparés
- Ce que
initenregistre vraiment, dans quel fichier - Comment un module en appelle un autre, et depuis où le chemin se résout
- Quelle modification exige un nouvel
init, et laquelle n'en demande aucun - Pourquoi
path.moduleest indispensable dans un module partagé - Ce qu'un chemin absolu change, jusqu'à casser un module imbriqué
- Comment prouver, JSON du plan en main, ce que l'appelant a passé
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Savoir créer un module et écrire son interface.
Qu'est-ce qu'un chemin local pour Terraform ?
Section intitulée « Qu'est-ce qu'un chemin local pour Terraform ? »La documentation officielle est normative sur ce point, et c'est la phrase à
retenir de tout ce guide : « A local path must begin with either ./ or
../ ». Le préfixe n'est pas une convention de lisibilité, c'est le
signal qui oriente Terraform vers le disque plutôt que vers un
téléchargement.
Enlevez ce préfixe, et Terraform part chercher un paquet distant :
module "gabarit" { source = "commun/gabarit"}Error: Invalid module source address
Terraform failed to determine your intended installation method for remotemodule package "commun/gabarit".
If you intended this as a path relative to the current module, use"./commun/gabarit" instead. The "./" prefix indicates that the address is arelative filesystem path.Le message porte lui-même la correction, et nomme le mécanisme qu'il a supposé. Ce tableau résume ce que Terraform fait des quatre formes que l'on écrit le plus souvent, chacune vérifiée dans ce guide :
| Ce que vous écrivez | Comment Terraform le traite |
|---|---|
./modules/gabarit | chemin local, lu sur place dans le projet |
../commun/gabarit | chemin local, lu sur place hors du projet |
commun/gabarit | refusé : Invalid module source address |
/opt/commun/gabarit | paquet distant recopié dans le cache de modules |
Le montage : une bibliothèque, plusieurs projets
Section intitulée « Le montage : une bibliothèque, plusieurs projets »Le schéma classique sépare la bibliothèque des projets qui la consomment :
mon-infra/├── commun/ ← bibliothèque de modules│ └── gabarit/│ ├── versions.tf│ ├── variables.tf│ ├── main.tf│ └── outputs.tf├── atelier-nord/ ← projet 1, son propre state│ ├── versions.tf│ ├── main.tf│ └── outputs.tf└── atelier-sud/ ← projet 2, son propre state ├── versions.tf ├── main.tf └── outputs.tfLe dossier commun/gabarit/ porte le code une seule fois. Les deux ateliers
le désignent par le même chemin relatif ../commun/gabarit, avec des
valeurs différentes, et chacun garde son propre state.
Écrire le module partagé
Section intitulée « Écrire le module partagé »Le module produit une plaque, un fichier nommé d'après une étiquette, avec un numéro de série facultatif. Sa structure suit le socle standard :
-
versions.tf, les contraintes de version, sans blocprovider:terraform {required_version = ">= 1.11.0"required_providers {local = { source = "hashicorp/local", version = ">= 2.5" }random = { source = "hashicorp/random", version = ">= 3.6" }}} -
variables.tf, l'interface d'entrée, une variable obligatoire et une facultative :variable "etiquette" {type = stringdescription = "Étiquette portée par la plaque."validation {condition = length(var.etiquette) >= 3error_message = "L'étiquette doit contenir au moins 3 caractères."}}variable "numeroter" {type = booldescription = "Ajoute un numéro de série au nom du fichier."default = true} -
main.tf, les ressources paramétrées par ces variables :resource "random_integer" "serie" {min = 100max = 999}resource "local_file" "plaque" {filename = "${path.root}/plaques/${var.etiquette}${var.numeroter ? "-${random_integer.serie.result}" : ""}.txt"content = "plaque ${var.etiquette}\n"} -
outputs.tf, ce que l'appelant pourra lire :output "plaque" {value = local_file.plaque.filenamedescription = "Chemin de la plaque produite."}output "configuration" {value = {etiquette = var.etiquettenumerotee = var.numeroter}description = "Résumé de ce que l'appelant a demandé."}
Consommer le module depuis deux projets
Section intitulée « Consommer le module depuis deux projets »L'atelier nord ne fournit que la variable obligatoire :
module "gabarit" { source = "../commun/gabarit"
etiquette = "nord"}L'atelier sud consomme le même dossier, avec une autre étiquette et le numérotage désactivé :
module "gabarit" { source = "../commun/gabarit"
etiquette = "sud" numeroter = false}Les deux projets exposent ensuite les sorties du module, ce qui rend la comparaison lisible :
output "plaque" { value = module.gabarit.plaque}
output "configuration" { value = module.gabarit.configuration}Ce que terraform init fait, et ne fait pas
Section intitulée « Ce que terraform init fait, et ne fait pas »L'init annonce le module et le chemin retenu, sur une ligne facile à
manquer :
$ cd atelier-nord$ terraform init
Initializing modules...- gabarit in ../commun/gabaritLa preuve que rien n'a été copié tient dans le répertoire de travail. Après
l'init, .terraform/modules/ ne contient qu'un fichier, et aucun
dossier :
ls -A .terraform/modules/modules.jsonCe modules.json, écrit par Terraform et par lui seul, est le registre
des modules du projet. Il tient sur une ligne, avec une entrée vide qui
représente la configuration racine :
{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"gabarit","Source":"../commun/gabarit","Dir":"../commun/gabarit"}]}Deux champs comptent. Source est ce que vous avez écrit, Dir est le
dossier que Terraform lira. Pour un chemin relatif, les deux sont
identiques et pointent hors du cache : c'est la signature d'un module lu sur
place. La documentation le dit dans les mêmes termes, « the files are already
present on local disk ».
Un module partagé qui en appelle un autre
Section intitulée « Un module partagé qui en appelle un autre »Une bibliothèque finit toujours par se factoriser elle-même : un module
appelle un voisin par ../, comme le fait ici gabarit avec etiquette.
L'init annonce alors les deux, avec une clé hiérarchique :
Initializing modules...- gabarit in ../commun/gabarit- gabarit.etiquette in ../commun/etiquetteLe registre montre où se joue la résolution, et c'est le point que l'on comprend mal une fois sur deux :
[ { "Key": "gabarit", "Source": "../commun/gabarit", "Dir": "../commun/gabarit" }, { "Key": "gabarit.etiquette", "Source": "../etiquette", "Dir": "../commun/etiquette" }]Source reste tel quel, relatif au module qui l'écrit, tandis que Dir est
normalisé depuis la racine. Autrement dit, un chemin source se résout depuis
le dossier du fichier qui le contient, jamais depuis celui où vous lancez la
commande. La documentation borne cette liberté à un paquet : « it is safe for a
module in a sub-directory of a package to use a local path to another module as
long as it is in the same package ». Retenez cette phrase, elle explique l'échec
du chemin absolu vu plus loin.
Les deux states restent indépendants
Section intitulée « Les deux states restent indépendants »Chaque atelier applique sa configuration, et obtient une plaque différente du même module :
$ cd atelier-nord && terraform apply
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
configuration = { "etiquette" = "nord" "numerotee" = true}plaque = "./plaques/nord-346.txt"$ cd ../atelier-sud && terraform apply
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
configuration = { "etiquette" = "sud" "numerotee" = false}plaque = "./plaques/sud.txt"Notez que l'atelier nord obtient numerotee = true sans avoir rien passé :
c'est la valeur par défaut déclarée dans le module qui s'applique. Le nom
du fichier produit en porte la trace, avec le numéro tiré par
random_integer.
Les adresses dans le state, elles, sont rigoureusement les mêmes de part et d'autre :
$ terraform -chdir=atelier-nord state listmodule.gabarit.local_file.plaquemodule.gabarit.random_integer.serie
$ terraform -chdir=atelier-sud state listmodule.gabarit.local_file.plaquemodule.gabarit.random_integer.serieAucune collision pour autant : une adresse n'a de portée que dans son state, et ces deux states vivent dans deux dossiers différents. Détruire l'atelier nord ne touche pas l'atelier sud, ce qui est exactement l'isolation recherchée quand on sépare des environnements.
Prouver ce que l'appelant a vraiment passé
Section intitulée « Prouver ce que l'appelant a vraiment passé »Une revue de code lit le .tf, un contrôle lit ce que Terraform en a
compris. Le plan converti en JSON expose l'appel de module, et tranche une
question que la sortie humaine laisse ouverte : cet argument a-t-il été passé
ou laissé à son défaut ?
terraform plan -out=plan.tfplanterraform show -json plan.tfplan > plan.jsonLa clé à ouvrir est configuration.root_module.module_calls.gabarit :
{ "source": "../commun/gabarit", "expressions": { "etiquette": { "constant_value": "nord" } }}Trois enseignements dans ces quelques lignes. Le champ version est absent,
puisqu'il n'a pas de sens sur une source locale. Les expressions ne listent que
etiquette, ce qui prouve que numeroter n'a pas été passé. Et le
sous-arbre module de cet appel donne, lui, les deux variables déclarées :
c'est la comparaison des deux listes qui établit ce qui vient du défaut.
Quelle modification exige un nouvel init ?
Section intitulée « Quelle modification exige un nouvel init ? »Voici l'idée fausse la plus répandue sur les modules locaux. Modifiez le
contenu du module partagé, sans relancer init, puis replanifiez depuis
un atelier :
content = "plaque ${var.etiquette} (revision 2)\n" # module.gabarit.local_file.plaque must be replaced ~ content = <<-EOT # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.terraform plan -detailed-exitcode; echo $?2Le changement est vu immédiatement. Il n'y a aucun cache à invalider pour
le contenu d'un module local, puisque Dir désigne le dossier réel : Terraform le
relit à chaque commande. Revenez en arrière, et le plan rend 0 aussitôt,
toujours sans init.
Ce que l'init relit, c'est le champ source et lui seul. Trois
situations le rendent donc obligatoire, et chacune se reconnaît à son message,
qu'il vaut la peine de savoir distinguer :
Error: Module not installed
This module is not yet installed. Run "terraform init" to install all modulesrequired by this configuration.Celui-ci tombe quand aucun init n'a été joué, et à l'identique quand vous
supprimez .terraform/modules/modules.json : ce fichier est l'installation.
Error: Module not installed
This module's local cache directory ../commun/gabarit could not be read. Run"terraform init" to install all modules required by this configuration.Celui-là est plus intéressant, il nomme le dossier devenu illisible. Vous l'obtenez en déplaçant ou en renommant le module partagé : le registre pointe encore vers l'ancien emplacement.
| Ce que vous changez | init nécessaire ? |
|---|---|
| le contenu du module (ressources, variables, outputs) | non, vu au plan suivant |
le champ source de l'appel | oui |
| l'emplacement du dossier du module | oui |
.terraform/ supprimé, dépôt fraîchement cloné | oui |
Lire un fichier depuis l'intérieur du module
Section intitulée « Lire un fichier depuis l'intérieur du module »Un module partagé lit souvent un fichier qu'il embarque : un template, un
cloud-init, un JSON de référence. À l'intérieur du module, path.module vaut
exactement ce que porte Dir, ce qu'une sortie de démonstration suffit à
montrer :
output "emplacement_du_module" { value = path.module}$ terraform output -raw emplacement_du_module../commun/gabaritUn chemin nu, lui, n'est pas résolu depuis le dossier du module mais depuis la racine d'exécution, et l'erreur tombe au plan :
Error: Invalid function argument
on ../commun/gabarit/modele.tf line 2, in locals: 2: modele = file("gabarits/exemple.txt") ├──────────────── │ while calling file(path)
Invalid value for "path" parameter: no file exists at "gabarits/exemple.txt";this function works only with files that are distributed as part of theconfiguration source code, so if this file will be created by a resource inthis configuration you must instead obtain this result from an attribute ofthat resource.La forme correcte ancre le chemin sur le module, et reste juste quel que soit le projet qui l'appelle :
locals { modele = file("${path.module}/gabarits/exemple.txt")}Ce n'est pas une préférence de style : c'est la seule écriture qui survit
au partage du module entre plusieurs projets, dont chacun a une racine
différente. Les autres valeurs path.* sont détaillées dans
expressions Terraform.
Le cas qui trompe : le chemin absolu
Section intitulée « Le cas qui trompe : le chemin absolu »Un chemin absolu désigne pourtant bien un dossier présent sur le disque, et lisible. Terraform ne le considère pas local pour autant, et la documentation le dit sans détour : « Terraform does not consider an absolute filesystem path to be a local path. Instead, Terraform will treat that in a similar way as a remote module and copy it into the local module cache. »
L'init change de vocabulaire, il télécharge :
Initializing modules...Downloading file:///home/bob/mon-infra/commun/gabarit for gabarit...- gabarit in .terraform/modules/gabaritEt le registre enregistre autre chose que ce que vous avez écrit :
{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"gabarit","Source":"file:///home/bob/mon-infra/commun/gabarit","Dir":".terraform/modules/gabarit"}]}Source est devenu une URL en file://, et Dir pointe désormais dans
.terraform/. Une nuance mesurée sur 1.15.4 sous Linux : ce Dir est un
lien symbolique vers la source, pas la copie profonde que le mot « copy »
laisse imaginer.
$ ls -la .terraform/modules/lrwxrwxrwx 1 bob bob 34 juil. 30 07:32 gabarit -> /home/bob/mon-infra/commun/gabarit-rw-rw-r-- 1 bob bob 238 juil. 30 07:32 modules.jsonDeux conséquences, dont la seconde coûte cher. La configuration devient non
portable, ce que la documentation déconseille explicitement : « We don't
recommend using absolute filesystem paths [...] because it will tend to couple your
configuration to the filesystem layout of a particular computer ». Et un module
devenu paquet ne peut plus atteindre ce qui est au-dessus de lui : s'il
appelle un voisin par ../autre, comme le gabarit de la section précédente,
l'init s'arrête net.
Error: Local module path escapes module packageL'argument version n'a pas de sens ici
Section intitulée « L'argument version n'a pas de sens ici »Dernier écueil, celui d'un réflexe pris avec les modules du Registry.
Ajoutez version à un appel dont la source est un dossier, et l'init refuse
la configuration en expliquant pourquoi il a changé d'interprétation :
Error: Invalid registry module source address
Failed to parse module registry address: can't use local directory"../commun/gabarit" as a module registry address.
Terraform assumed that you intended a module registry source address becauseyou also set the argument "version", which applies only to registry modules.Un module local n'a pas de version, il a l'état de son dossier à cet instant. C'est précisément ce qui pousse, au delà de deux ou trois consommateurs, vers un module versionné par Git ou par le Registry.
Un source piloté par une variable, depuis la 1.15
Section intitulée « Un source piloté par une variable, depuis la 1.15 »Longtemps, source n'acceptait qu'une chaîne littérale, ce qui interdisait de
paramétrer l'emplacement d'une bibliothèque. La 1.15 ouvre une brèche
étroite : « The source attribute can reference constant input variables and
local values. Any input variable referenced in source must declare const = true ». Sans ce mot-clé, l'init refuse, et son message est instructif :
Error: Unknown module source
on main.tf line 13, in module "gabarit": 13: source = var.bibliotheque
Only literal values and const variables can be evaluated during init.La raison est structurelle : l'init s'exécute avant toute évaluation de
variables ordinaires, il ne peut donc lire que ce qui est constant. La forme
acceptée déclare la contrainte :
variable "bibliotheque" { type = string const = true default = "../commun/gabarit"}
module "gabarit" { source = var.bibliotheque
etiquette = "nord"}L'init passe alors normalement, et modules.json enregistre le chemin
résolu, pas la variable. Utile pour un dépôt de labs ou un monorepo dont la
bibliothèque change de place selon le contexte, à réserver aux cas où ce besoin est
réel : un source indirect coûte en lisibilité ce qu'il gagne en souplesse.
Module intégré, module partagé : que choisir ?
Section intitulée « Module intégré, module partagé : que choisir ? »Le mécanisme est identique, le couplage ne l'est pas. Un module intégré au
projet, en ./modules/gabarit, suit le même commit que le code qui l'appelle :
les modifier ensemble garantit qu'ils restent cohérents. Un module posé hors
du projet perd cette garantie, une modification se propage immédiatement à
tous ses consommateurs, y compris celui dont vous avez oublié l'existence.
| Aspect | Module intégré (./modules/) | Module partagé (../commun/) |
|---|---|---|
| Chargement | lu sur place | lu sur place, identique |
| Réutilisation | un seul projet | plusieurs projets |
| Versionnement | même commit que le projet | aucun, l'état du dossier |
| Propagation d'un changement | maîtrisée par le commit | immédiate, chez tous |
| Cas d'usage | module spécifique au projet | bibliothèque d'équipe |
La documentation officielle répartit d'ailleurs les deux mécanismes sans détour : les chemins locaux « for closely-related modules used primarily for the purpose of factoring out repeated code elements », et le registre « for modules intended to be shared by multiple calling configurations ». Un module partagé entre projets emploie donc l'outil du premier cas pour servir le second, ce qui tient un certain temps, puis cesse.
Le module partagé est ainsi un excellent point de départ, et une mauvaise destination finale. Il convient parfaitement à un monorepo dont tous les projets avancent ensemble, comme dans séparer dev, staging et prod. Dès que deux équipes veulent des rythmes différents, le versionnement devient nécessaire, arbitrage détaillé dans monorepo ou un repo par stack.
Dépannage
Section intitulée « Dépannage »Le fichier à ouvrir en premier est toujours le même,
.terraform/modules/modules.json : il dit quel dossier Terraform lit
vraiment, ce qui tranche la moitié des questions ci-dessous sans la moindre
hypothèse.
| Symptôme | Cause probable | Correction |
|---|---|---|
Module not installed, « not yet installed » | aucun init joué, ou modules.json supprimé | terraform init |
Module not installed, « local cache directory ... could not be read » | le module a été déplacé ou renommé | corriger source, puis terraform init |
Invalid module source address | préfixe ./ ou ../ absent | ajouter le préfixe, comme le suggère le message |
Unreadable module directory | chemin préfixé mais inexistant | vérifier avec ls ../commun/gabarit |
Invalid registry module source address | argument version sur une source locale | retirer version |
Local module path escapes module package | source absolue sur un module qui appelle un voisin | repasser en chemin relatif |
Unknown module source | source référençant une variable sans const = true | ajouter const = true à la variable |
| Modification du module non prise en compte | Dir pointe ailleurs que ce que vous éditez | lire modules.json, puis terraform init |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab module local remet trois projets à brancher sur une bibliothèque
partagée, dont l'un doit servir de contre-exemple en chemin absolu. Les tests
ne lisent aucun .tf : ils ouvrent les trois modules.json, comparent les
couples Source et Dir, et modifient le module partagé dans une copie pour
exiger un plan à 2 sans init. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Un chemin local doit commencer par
./ou../, c'est le préfixe qui définit le mécanisme. - Un module local n'est pas installé :
SourceetDirsont identiques dansmodules.json, hors du cache. - Modifier le contenu du module n'exige aucun
init, le plan suivant le voit. - Seul un changement de
source, de place du dossier, ou un.terraform/absent réclame uninit, queterraform getsait aussi jouer. - Un
sourcede module imbriqué est relatif au fichier qui l'écrit, etDirle donne normalisé depuis la racine. - Un fichier embarqué dans le module se lit avec
path.module, jamais par un chemin nu. - Le JSON du plan prouve ce que l'appelant a passé, et donc ce qui vient du défaut.
- Un chemin absolu devient un paquet en
file://, recopié dans.terraform/modules/, et casse tout module imbriqué appelé en relatif. - L'argument
versionne s'applique qu'aux modules de registre, pas à un dossier. - Un
sourcepeut référencer une variable depuis la 1.15, à condition qu'elle déclareconst = true. - Chaque projet garde son state : mêmes adresses, aucune collision.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui casse en pratique : le source
refusé pour une histoire de préfixe, l'init qu'on relance pour rien, et
le chemin absolu qui semblait plus sûr.
Un dossier lu sur place, pas un paquet installé
La documentation officielle est explicite : « Local paths are special in that they are not 'installed' in the same sense that other sources are: the files are already present on local disk. »Après unterraform init, le registre des modules le montre :{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"gabarit","Source":"../commun/gabarit","Dir":"../commun/gabarit"}]}
Sourceest ce que vous avez écrit dans le blocmodule.Direst le dossier que Terraform lira réellement.
.terraform/. Le dossier .terraform/modules/ ne contient alors qu'un seul fichier, modules.json, et aucune copie du module.Le préfixe décide du mécanisme
La règle officielle est normative : « A local path must begin with either./ or ../ ». Sans préfixe, Terraform cherche un module distant :Error: Invalid module source address
Terraform failed to determine your intended installation method for remote
module package "commun/gabarit".
If you intended this as a path relative to the current module, use
"./commun/gabarit" instead. The "./" prefix indicates that the address is a
relative filesystem path.
La correction est celle que le message donne :module "gabarit" {
source = "./commun/gabarit"
}
Mesuré sur Terraform 1.15.4. Le chemin se résout depuis le dossier qui contient le bloc module, pas depuis celui où vous lancez la commande.Le contenu n'est pas mis en cache
Vérifié sur 1.15.4 : après modification du contenu du module partagé, sans relancerinit, le plan réagit immédiatement. # module.gabarit.local_file.plaque must be replaced
~ content = <<-EOT # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.
terraform plan -detailed-exitcode; echo $?
2
Ce qu'init relit, c'est le champ source, et lui seul.| Ce que vous changez | init nécessaire ? |
|---|---|
| contenu du module | non |
champ source de l'appel |
oui |
| emplacement du dossier du module | oui |
.terraform/ supprimé ou dépôt cloné |
oui |
Un chemin absolu devient un paquet distant
La documentation prévient : « Terraform does not consider an absolute filesystem path to be a local path. Instead, Terraform will treat that in a similar way as a remote module and copy it into the local module cache. »L'init change de vocabulaire :Downloading file:///home/bob/mon-infra/commun/gabarit for gabarit...
- gabarit in .terraform/modules/gabarit
Et le registre enregistre autre chose que ce que vous avez écrit :{"Key":"gabarit","Source":"file:///home/bob/mon-infra/commun/gabarit","Dir":".terraform/modules/gabarit"}
Sur 1.15.4 sous Linux, ce Dir est un lien symbolique vers la source, pas une copie profonde. Deux conséquences : la configuration ne fonctionne que sur votre machine, et un module devenu paquet ne peut plus atteindre un voisin appelé par ../, ce qui donne Error: Local module path escapes module package.Deux messages, deux causes
Aucuninit joué, ou modules.json supprimé :Error: Module not installed
This module is not yet installed. Run "terraform init" to install all modules
required by this configuration.
Module partagé déplacé ou renommé depuis le dernier init :Error: Module not installed
This module's local cache directory ../commun/gabarit could not be read. Run
"terraform init" to install all modules required by this configuration.
La seconde variante est la plus utile : elle nomme le dossier. Le réflexe de diagnostic tient en une commande, qui affiche le dossier que Terraform lit vraiment :cat .terraform/modules/modules.json
version bascule Terraform en mode registre
module "gabarit" {
source = "../commun/gabarit"
version = "~> 1.0"
}
Error: Invalid registry module source address
Failed to parse module registry address: can't use local directory
"../commun/gabarit" as a module registry address.
Terraform assumed that you intended a module registry source address because
you also set the argument "version", which applies only to registry modules.
La correction est de retirer version. C'est aussi le signal qu'un module local n'offre aucun verrouillage : toute modification se propage immédiatement à ses consommateurs. Dès que ce risque gêne, passez à un module versionné par un tag Git ou publié sur un registre.path.module est la seule écriture portable
Dans un module appelé par../commun/gabarit, path.module vaut exactement ce chemin. Sans lui, le fichier est cherché depuis la racine :Error: Invalid function argument
on ../commun/gabarit/modele.tf line 2, in locals:
2: modele = file("gabarits/exemple.txt")
Invalid value for "path" parameter: no file exists at "gabarits/exemple.txt"
La forme correcte ancre le chemin sur le module :locals {
modele = file("${path.module}/gabarits/exemple.txt")
}
Ce n'est pas une préférence de style : un module partagé est appelé depuis plusieurs projets, dont chacun a une racine différente. Seule la forme ancrée reste juste dans tous les cas.Le mécanisme est le même, le couplage non
| Aspect | Module local partagé | Module versionné (Git, registre) |
|---|---|---|
source |
chemin en ./ ou ../ |
URL Git ou adresse de registre |
| Chargement | lu sur place | téléchargé dans .terraform/modules/ |
| Verrouillage | aucun | tag ou contrainte version |
| Propagation d'un changement | immédiate, chez tous | choisie par le consommateur |
| Mise en place | instantanée | dépôt et publication à prévoir |
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Versionner ses modules Terraform : le tag Git qui remplace le chemin relatif dès que plusieurs équipes consomment le module.
- Anti-patterns des modules : les découpages qui transforment un module partagé en dette.
- Tester un module Terraform : les tests natifs qui sécurisent un module partagé entre projets.
- Bonnes pratiques des modules : ce qui rend un module lisible et réutilisable au delà de son auteur.
- Sources de modules : la référence officielle : toutes les formes de
source, chemins locaux compris. - Le bloc module : la référence officielle : les arguments acceptés par un appel de module.