Aller au contenu
Infrastructure as Code medium

Utiliser un module du Terraform Registry

35 min de lecture

logo terraform

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.

  • D'où vient l'adresse en trois parties, et pourquoi elle n'est pas arbitraire
  • Ce que init té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=

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.

PartieD'où elle vientExemple
namespacel'organisation du dépôtcloudposse
namele <NAME> du dépôtlabel
providerle <PROVIDER> du dépôtnull

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.

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/etiquette

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

Fenêtre de terminal
ls -A .terraform/modules/etiquette/
descriptors.tf docs examples exports .git .github LICENSE main.tf
Makefile outputs.tf README.md README.yaml test variables.tf

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

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 :

Fenêtre de terminal
grep -c cloudposse .terraform.lock.hcl
0

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

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.24
Downloading registry.terraform.io/cloudposse/label/null 0.24.1 for etiquette...
$ terraform init # second init, rien ne bouge
Initializing 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 -upgrade
Upgrading 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'initCe que fait init seul
aucune copie installéetélécharge la plus récente qui satisfait
la copie installée satisfait la contraintela conserve, même si plus récent existe
la copie installée ne satisfait plustélécharge une version conforme
avec -upgraderéé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.

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.

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

# 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"

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.

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 :

SourceFormat sourceversionCe 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"nondé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"nonclone Mercurial
HTTP"https://exemple.com/module"nonindirection via terraform-get=1
Archive"https://exemple.com/mod.zip"nonextensions reconnues, ou ?archive=zip
S3"s3::https://bucket/mod.zip"nonauthentification AWS, pas un simple HTTP
GCS"gcs::https://storage.googleapis.com/..."nonauthentification 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.

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 :

ArtefactCe qu'il donne
.terraform/modules/modules.jsonla version résolue, réellement installée
terraform show -json <plan>la contrainte écrite dans la configuration
Fenêtre de terminal
terraform plan -out=plan.tfplan
terraform 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 ?

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 modules
required 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é.

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

SituationCe qui convient
brique standard (VPC, IAM, réseau)module de registre, épinglé
besoin spécifique à votre organisationmodule local ou registre privé
découverte d'un service cloudmodule de registre, comme code de référence
infrastructure critiqueregistre épinglé, après lecture du code
SymptômeCause probableCorrection
Unresolvable module version constraintaucune version publiée ne satisfait la contraintele message donne la plus récente, ajuster
Invalid registry module source addressargument version sur une source localeretirer version
Invalid module source addresschemin 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 versionversion référençant une variable sans const = trueajouter const = true à la variable
Module not installedinit non joué, ou modules.json suppriméterraform init
la version installée ne bouge pasla copie installée satisfait encore la contrainteterraform init -upgrade
Error downloading modulesréseau, proxy, ou dépôt privévérifier l'accès à registry.terraform.io, les clés SSH, le .netrc

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.

  • L'adresse namespace/name/provider se déduit du dépôt terraform-<PROVIDER>-<NAME>, elle ne s'invente pas.
  • Un module de registre est téléchargé dans .terraform/modules/, et modules.json porte sa version résolue.
  • L'argument version est 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, init conserve la copie installée ; -upgrade est 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.
  • validate exige un init réussi : hors ligne, une configuration à module distant ne se valide pas.

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

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn