
Le Terraform Registry est le catalogue officiel de modules réutilisables : au
lieu d'écrire un VPC ou une politique IAM, vous appelez un module publié et
maintenu par un tiers. Le geste tient en deux lignes, source et version, mais
la mécanique derrière mérite d'être connue : un module de registre est
téléchargé, il porte une version résolue, et le fichier de verrouillage
qui protège vos providers ne le protège pas.
Tout ce qui suit a été exécuté sur Terraform v1.15.4 avec un module réel,
cloudposse/label/null, qui ne déclare aucun provider et s'applique sans
compte cloud : sorties, erreurs et contenu de modules.json compris. Deux idées
très répandues n'y survivent pas, celle de l'argument version obligatoire, et
celle du .terraform.lock.hcl qui figerait les modules.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- D'où vient l'adresse en trois parties, et pourquoi elle n'est pas arbitraire
- Ce que
inittélécharge exactement, et dans quel fichier il l'enregistre - Pourquoi le verrou ignore les modules, et ce qui les fige à sa place
- Comment débloquer une contrainte souple restée sur une vieille version
- Pourquoi deux appels du même module peuvent installer deux versions
- Où placer un sous-répertoire
//face à un?ref=
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Savoir appeler un module et connaître le cas du module local, dont ce guide est le pendant distant.
D'où vient l'adresse en trois parties
Section intitulée « D'où vient l'adresse en trois parties »Une source de registre s'écrit namespace/name/provider, sans préfixe ./ ni
../ :
module "etiquette" { source = "cloudposse/label/null" version = "0.25.0"
namespace = "demo" name = "ouest"}Ce format n'est pas une convention de nommage arbitraire, il se déduit du
dépôt publié. Les règles de publication sont explicites : « Module repositories
must use this three-part name format », terraform-<PROVIDER>-<NAME>, le dépôt
doit être public sur GitHub, respecter la structure standard, et porter des
tags de version sémantique. Le dépôt cloudposse/terraform-null-label donne
donc l'adresse cloudposse/label/null.
| Partie | D'où elle vient | Exemple |
|---|---|---|
namespace | l'organisation du dépôt | cloudposse |
name | le <NAME> du dépôt | label |
provider | le <PROVIDER> du dépôt | null |
Deux remarques utiles. Le <PROVIDER> n'est pas forcément un cloud, ici c'est
null, et le module reste utilisable sans compte. Un détail explique par
ailleurs bien des versions introuvables : « Tags that don't look like version
numbers are ignored », un v1.2.0 étant accepté mais un release-mars
invisible pour le registre.
Ce que terraform init télécharge
Section intitulée « Ce que terraform init télécharge »Contrairement au module local, l'init télécharge vraiment, et il le dit :
$ terraform init
Initializing modules...Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...- etiquette in .terraform/modules/etiquetteLe registre des modules, .terraform/modules/modules.json, enregistre trois
informations là où un module local n'en avait que deux :
{ "Key": "etiquette", "Source": "registry.terraform.io/cloudposse/label/null", "Version": "0.25.0", "Dir": ".terraform/modules/etiquette"}La clé Version est celle qui compte, et elle n'existe que pour un module
de registre : c'est la version résolue, celle qui tourne réellement. Dir
pointe cette fois sous .terraform/, la copie étant bien réelle.
Ce dossier téléchargé n'a rien de spécial, c'est le dépôt tel quel :
ls -A .terraform/modules/etiquette/descriptors.tf docs examples exports .git .github LICENSE main.tfMakefile outputs.tf README.md README.yaml test variables.tfVous y trouvez donc les fichiers du module, mais aussi ses exemples, ses
tests et son .git. C'est le meilleur endroit pour lire le code que
vous venez d'adopter.
L'argument version est facultatif, et c'est un piège
Section intitulée « L'argument version est facultatif, et c'est un piège »Beaucoup d'articles le donnent pour obligatoire. Il ne l'est pas : retirez-le, et
l'init passe.
Initializing modules...Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...Terraform a retenu la plus récente disponible, sans un mot d'avertissement.
Rien ne l'interdit, et c'est justement ce qui rend l'omission coûteuse : le
même code, initialisé demain sur un autre poste, prendra une autre version.
Écrire version n'est donc pas une contrainte de l'outil, c'est une décision
qui vous appartient.
Le verrou ne couvre pas les modules
Section intitulée « Le verrou ne couvre pas les modules »Voici le point le plus mal connu du sujet, et la documentation ne le cache pas : « At present, the dependency lock file tracks only provider dependencies. Terraform does not remember version selections for remote modules. »
Deux observations le prouvent. Dans un projet qui déclare un provider, le verrou ne parle que de lui :
grep -c cloudposse .terraform.lock.hcl0Et dans un projet dont la seule dépendance est un module de registre, il n'y a
aucun fichier de verrouillage : l'init n'en crée même pas.
Pourquoi votre ~> 5.0 reste bloqué
Section intitulée « Pourquoi votre ~> 5.0 reste bloqué »Ce comportement ne se devine pas, et tout le monde finit par le rencontrer.
Avec une contrainte souple, la version retenue au premier init reste
installée aux suivants :
$ terraform init # premier init, contrainte ~> 0.24Downloading registry.terraform.io/cloudposse/label/null 0.24.1 for etiquette...
$ terraform init # second init, rien ne bougeInitializing modules...modules.json porte toujours 0.24.1, alors que 0.25.0 existe et
satisfait la contrainte. La documentation l'énonce : « Terraform uses the
newest installed version of the module that meets the constraint. » Une seule
option débloque la situation :
$ terraform init -upgradeUpgrading modules...Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...Le cas symétrique est instructif. Faites reculer la contrainte vers une
version antérieure : la copie installée ne satisfait plus la contrainte, donc
l'init télécharge, sans -upgrade.
Situation au moment de l'init | Ce que fait init seul |
|---|---|
| aucune copie installée | télécharge la plus récente qui satisfait |
| la copie installée satisfait la contrainte | la conserve, même si plus récent existe |
| la copie installée ne satisfait plus | télécharge une version conforme |
avec -upgrade | réévalue dans tous les cas |
La conséquence pratique est brutale : sur un poste vierge ou dans une CI, aucune copie n'est installée, donc la plus récente s'installe. Le même code n'utilise pas la même version chez vous et dans le pipeline, et c'est exactement ce que le verrou empêcherait s'il couvrait les modules.
La résolution se fait par appel, pas par projet
Section intitulée « La résolution se fait par appel, pas par projet »Deux blocs module du même module, avec deux contraintes différentes, installent
deux versions dans un seul projet :
module "etiquette" { source = "cloudposse/label/null" version = "~> 0.24"
namespace = "atelier" name = "large"}
module "etiquette_patch" { source = "cloudposse/label/null" version = "~> 0.24.1"
namespace = "atelier" name = "etroit"}Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...Downloading registry.terraform.io/cloudposse/label/null 0.24.1 for etiquette_patch...Chaque appel a son entrée dans modules.json et son dossier dans le cache.
C'est utile pour migrer progressivement un projet d'une version à l'autre, et
c'est un piège de lecture : le numéro visible sur la page du registre ne dit rien
de ce que votre appel a installé.
Les formes de contraintes elles-mêmes sont détaillées dans
contraintes de version ;
retenez ici que ~> laisse bouger le composant le plus à droite.
Le sous-répertoire //, et l'ordre qui compte
Section intitulée « Le sous-répertoire //, et l'ordre qui compte »// n'est pas une notion de registre mais de paquet : elle désigne un
sous-répertoire à l'intérieur de ce que Terraform a téléchargé, dépôt Git ou
archive compris.
module "iam_account" { source = "terraform-aws-modules/iam/aws//modules/iam-account" version = "5.52.2"}Sur une source Git, elle cohabite avec un argument de requête comme
?ref=, et l'ordre des deux n'est pas libre : « If the source address has
arguments, such as the ref argument [...] the sub-directory portion must be
before those arguments ».
# correctsource = "git::https://example.com/reseau.git//modules/vpc?ref=v1.2.0"
# correct aussi, raccourci GitHubsource = "github.com/hashicorp/example//modules/vpc?ref=v1.2.0"L'ordre inverse produit une erreur qui nomme le vrai coupable, la révision :
Error: Failed to download module
error downloading '...?ref=0.25.0%2F%2Fexports': invalid ref: "0.25.0//exports"Terraform a pris 0.25.0//exports pour un nom de révision, ce qu'il n'est
évidemment pas.
Toutes les sources acceptées par source
Section intitulée « Toutes les sources acceptées par source »Le tableau ci-dessous reprend la liste officielle. Les deux premières lignes sont celles que vous écrirez presque toujours, les autres méritent d'être connues pour les reconnaître dans le code d'autrui :
| Source | Format source | version | Ce que fait Terraform |
|---|---|---|---|
| Local | "./modules/reseau" | refusé | lecture sur place |
| Registre public | "namespace/name/provider" | accepté | téléchargement depuis registry.terraform.io |
| Registre privé | "app.terraform.io/org/name/provider" | accepté | jeton dans la configuration CLI |
| GitHub | "github.com/org/repo//sous-dir?ref=tag" | non, ?ref= | clone Git |
| Bitbucket | "bitbucket.org/org/repo" | non | dépôts publics seulement, l'API décidant Git ou Mercurial |
| Git générique | "git::https://exemple.com/repo.git" | non, ?ref= | clone Git, ?depth=1 possible |
| Mercurial | "hg::https://exemple.com/repo" | non | clone Mercurial |
| HTTP | "https://exemple.com/module" | non | indirection via terraform-get=1 |
| Archive | "https://exemple.com/mod.zip" | non | extensions reconnues, ou ?archive=zip |
| S3 | "s3::https://bucket/mod.zip" | non | authentification AWS, pas un simple HTTP |
| GCS | "gcs::https://storage.googleapis.com/..." | non | authentification Google |
Deux pièges valent d'être signalés. Sur une source Git, ?depth=1 accélère le
clone mais change la sémantique de ref : « setting the depth argument makes
Terraform pass your ref argument [...] to the --branch argument to git clone
», donc un commit brut n'est plus accepté, seulement une branche ou un
tag. Et s3:: n'est pas un téléchargement anonyme, le module installer cherche
des identifiants AWS, variables d'environnement puis profil puis rôle
d'instance.
Prouver ce qui est réellement installé
Section intitulée « Prouver ce qui est réellement installé »La sortie humaine de l'init défile et disparaît. Deux artefacts, en revanche,
restent, et ils ne disent pas la même chose :
| Artefact | Ce qu'il donne |
|---|---|
.terraform/modules/modules.json | la version résolue, réellement installée |
terraform show -json <plan> | la contrainte écrite dans la configuration |
terraform plan -out=plan.tfplanterraform show -json plan.tfplan | jq '.configuration.root_module.module_calls'{ "etiquette": { "source": "cloudposse/label/null", "version_constraint": "0.25.0", "expressions": { "namespace": { "constant_value": "demo" } } }}Notez que source est ici l'adresse telle que vous l'avez écrite, sans l'hôte
du registre que modules.json ajoute. Le champ version_constraint n'existe
pas pour un module local, et c'est lui qui permet à une revue ou une CI de
refuser un appel non épinglé. La comparaison des deux fichiers répond à la seule
question qui compte : ma contrainte est-elle souple, et ai-je une version
périmée installée ?
terraform validate ne remplace pas l'init
Section intitulée « terraform validate ne remplace pas l'init »On lit souvent que validate vérifie une configuration à module distant sans
rien télécharger. C'est faux, et la mesure est immédiate sur un dossier
neuf :
$ terraform validate
Error: Module not installed
This module is not yet installed. Run "terraform init" to install all modulesrequired by this configuration.Ce qui est vrai, c'est que validate n'a besoin d'aucun credential de cloud.
Mais il exige un init réussi, donc le téléchargement du module et du
provider. Hors ligne, une configuration à module de registre ne se valide pas,
et c'est une contrainte à connaître pour une CI en réseau fermé.
Quand préférer le registre au module local
Section intitulée « Quand préférer le registre au module local »La documentation répartit les deux mécanismes en une phrase : « We recommend using local file paths for closely-related modules used primarily for the purpose of factoring out repeated code elements, and using a native Terraform module registry for modules intended to be shared by multiple calling configurations. »
| Situation | Ce qui convient |
|---|---|
| brique standard (VPC, IAM, réseau) | module de registre, épinglé |
| besoin spécifique à votre organisation | module local ou registre privé |
| découverte d'un service cloud | module de registre, comme code de référence |
| infrastructure critique | registre épinglé, après lecture du code |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Correction |
|---|---|---|
Unresolvable module version constraint | aucune version publiée ne satisfait la contrainte | le message donne la plus récente, ajuster |
Invalid registry module source address | argument version sur une source locale | retirer version |
Invalid module source address | chemin local sans préfixe ./ ou ../ | ajouter le préfixe |
invalid ref: "0.25.0//exports" | // placé après le ?ref= | remettre le sous-répertoire avant |
Unknown module version | version référençant une variable sans const = true | ajouter const = true à la variable |
Module not installed | init non joué, ou modules.json supprimé | terraform init |
| la version installée ne bouge pas | la copie installée satisfait encore la contrainte | terraform init -upgrade |
Error downloading modules | réseau, proxy, ou dépôt privé | vérifier l'accès à registry.terraform.io, les clés SSH, le .netrc |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab module du registre remet trois projets et un seul module public. Il faut
en épingler une version exacte, en laisser une souple, garder un troisième
appel dans une fenêtre de correctifs, et adresser un sous-répertoire Git
sur un tag. Les tests ne lisent aucun .tf : ils comparent les trois
modules.json, la version_constraint du plan, et l'absence de fichier de
verrouillage là où le projet ne déclare aucun provider. Il exige un accès
réseau.
À retenir
Section intitulée « À retenir »- L'adresse
namespace/name/providerse déduit du dépôtterraform-<PROVIDER>-<NAME>, elle ne s'invente pas. - Un module de registre est téléchargé dans
.terraform/modules/, etmodules.jsonporte sa version résolue. - L'argument
versionest facultatif : sans lui, la plus récente s'installe silencieusement. - Le verrou ne suit que les providers : un projet sans provider n'en produit même pas.
- Sous contrainte souple,
initconserve la copie installée ;-upgradeest le seul moyen de la faire bouger. - Une CI part d'un cache vide et prend donc la plus récente admissible : seule une version exacte garantit l'identité.
- La résolution se fait par appel : deux appels du même module peuvent installer deux versions.
- Le sous-répertoire
//appartient au paquet, et se place avant les arguments de requête. validateexige uninitréussi : hors ligne, une configuration à module distant ne se valide pas.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui casse en pratique : la version qui ne bouge pas, le verrou qui ne verrouille rien, et l'adresse Git dont l'ordre est refusé.
Facultatif pour l'outil, indispensable en pratique
Mesuré sur Terraform 1.15.4 : un blocmodule de registre sans version s'initialise normalement.Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...
Terraform a retenu la plus récente disponible. Le risque n'est donc pas une erreur, c'est une dérive silencieuse : le même code, initialisé demain ailleurs, prendra une autre version.La documentation recommande la forme la plus stricte : « You can use an exact version constraint to ensure that Terraform will always select the same module version. »Le verrou ignore les modules, et cela se vérifie
« At present, the dependency lock file tracks only provider dependencies. Terraform does not remember version selections for remote modules. »Deux mesures sur 1.15.4 :- dans un projet qui déclare un provider,
grep -c cloudposse .terraform.lock.hclrend 0 ; - dans un projet dont la seule dépendance est un module de registre, aucun
.terraform.lock.hcln'est créé.
provider non épinglé reste reproductible grâce au verrou, un module non épinglé ne l'est pas. Seule votre contrainte le fige.init conserve, -upgrade réévalue
Mesuré sur 1.15.4 avec une contrainte~> 0.24 :$ terraform init # premier init
Downloading registry.terraform.io/cloudposse/label/null 0.24.1 for etiquette...
$ terraform init # second init : rien ne bouge
Initializing modules...
$ terraform init -upgrade
Upgrading modules...
Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...
La règle officielle : « Terraform uses the newest installed version of the module that meets the constraint. »| Situation | init seul |
|---|---|
| aucune copie installée | télécharge la plus récente admissible |
| copie installée conforme | la conserve |
| copie installée non conforme | télécharge |
Une résolution par bloc module
module "etiquette" {
source = "cloudposse/label/null"
version = "~> 0.24"
namespace = "atelier"
name = "large"
}
module "etiquette_patch" {
source = "cloudposse/label/null"
version = "~> 0.24.1"
namespace = "atelier"
name = "etroit"
}
L'init télécharge les deux :Downloading registry.terraform.io/cloudposse/label/null 0.25.0 for etiquette...
Downloading registry.terraform.io/cloudposse/label/null 0.24.1 for etiquette_patch...
C'est le mécanisme qui permet une migration progressive d'un projet d'une version de module à l'autre, appel par appel.Le sous-répertoire précède les arguments
# correct
source = "git::https://example.com/reseau.git//modules/vpc?ref=v1.2.0"
# correct aussi, raccourci GitHub
source = "github.com/hashicorp/example//modules/vpc?ref=v1.2.0"
La règle est écrite : « the sub-directory portion must be before those arguments ». L'ordre inverse donne, mesuré sur 1.15.4 :Error: Failed to download module
error downloading '...?ref=0.25.0%2F%2Fexports': invalid ref: "0.25.0//exports"
À noter : // n'est pas une notion de registre mais de paquet. Elle s'applique à tout dépôt de gestion de version ou archive téléchargée.Deux artefacts, deux informations
| Artefact | Ce qu'il donne |
|---|---|
.terraform/modules/modules.json |
la version résolue, installée |
terraform show -json <plan> |
la contrainte écrite |
{
"Key": "etiquette",
"Source": "registry.terraform.io/cloudposse/label/null",
"Version": "0.25.0",
"Dir": ".terraform/modules/etiquette"
}
terraform show -json plan.tfplan | jq '.configuration.root_module.module_calls'
Le champ version_constraint du plan n'existe pas pour un module local. Comparer les deux répond à la seule question qui compte : ma contrainte est-elle souple, et ai-je une version périmée installée ?version n'existe que pour un registre
« You can only use theversion argument when the source argument points to a module listed in a registry. »Sur une source locale, mesuré sur 1.15.4 :Error: Invalid registry module source address
Terraform assumed that you intended a module registry source address because
you also set the argument "version", which applies only to registry modules.
Sur une source Git, la révision se fige autrement :source = "git::https://example.com/reseau.git?ref=v1.2.0"
Attention si vous ajoutez ?depth=1 : Terraform passe alors ref à git clone --branch, donc un commit brut n'est plus accepté, seulement une branche ou un tag.L'adresse se déduit, elle ne s'invente pas
« Module repositories must use this three-part name format »,terraform-<PROVIDER>-<NAME>, avec trois conditions : dépôt GitHub public, structure standard respectée, et tags de version sémantique.| Partie de l'adresse | D'où elle vient |
|---|---|
namespace |
l'organisation du dépôt |
name |
le <NAME> du dépôt |
provider |
le <PROVIDER> du dépôt |
<PROVIDER> n'est pas forcément un cloud, et « Tags that don't look like version numbers are ignored », ce qui explique une version publiée mais introuvable côté registre.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Tester un module Terraform : valider un module avant de l'adopter, avec
terraform test. - Anti-patterns des modules : ce qui rend un module du registre impossible à monter de version.
- Bonnes pratiques des modules : les critères qui distinguent un module réutilisable d'un module jetable.
- Sources de modules : la référence officielle : toutes les formes de
source, protocoles compris. - Publier un module sur le registre : les conditions de nommage, de structure et de tags.
- Le fichier de verrouillage : ce qu'il suit, et ce qu'il ne suit pas.