
Versionner un module partagé, c'est découpler le rythme du module de celui des projets qui le consomment. Le mécanisme tient en trois gestes : un commit, un tag, une référence côté appelant. Ce que l'on enseigne rarement, c'est ce que ce mécanisme ne garantit pas, et c'est là que se jouent les mauvaises surprises.
Tout ce qui suit a été exécuté sur Terraform v1.15.4 contre un dépôt Git
local, donc hors ligne : sorties, erreurs et contenu installé compris. Deux
croyances n'y survivent pas, celle du tag qui figerait une version, et celle
du .terraform.lock.hcl qui protégerait un module comme il protège un
provider.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Publier une version avec un tag annoté, et ce que le numéro doit dire
- Écrire une source Git complète : dépôt, sous-répertoire, révision
- Ce que
refaccepte, et ce qui arrive quand on l'oublie - Pourquoi un tag ne fige pas le code, et quelle référence le fige
- Ce qu'un module réutilisable ne doit surtout pas contraindre
- Mener une montée de version sans emporter tous les environnements
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Les bases des tags Git, annotés comme légers.
- Avoir vu le module local et le module de registre.
Pourquoi versionner, concrètement
Section intitulée « Pourquoi versionner, concrètement »Un module local partagé entre projets n'a pas de version : ses fichiers sont lus sur place, donc la moindre modification part immédiatement chez tous ses consommateurs, y compris celui que vous aviez oublié.
commun/etiquette/ ← une modification ici...├── projet-dev/ ← ...arrive ici├── projet-staging/ ← ...et ici└── projet-prod/ ← ...et ici aussiLe versionnement casse ce couplage. Chaque projet déclare la révision
qu'il consomme, et décide quand il monte. C'est tout l'objet d'une source
Git avec ?ref=.
SemVer : le numéro dit la nature du changement
Section intitulée « SemVer : le numéro dit la nature du changement »| Composant | Quand l'incrémenter | Exemple |
|---|---|---|
| MAJOR | changement incompatible | 1.1.0 → 2.0.0 |
| MINOR | ajout rétrocompatible | 1.0.1 → 1.1.0 |
| PATCH | correction rétrocompatible | 1.0.0 → 1.0.1 |
Le critère est mécanique, et il ne se discute pas : tout changement qui
oblige l'appelant à modifier son bloc module est majeur. Renommer une
variable, en supprimer une, rendre obligatoire une variable qui ne l'était pas,
renommer une sortie. À l'inverse, ajouter une variable avec un default est
mineur, précisément parce que l'appelant n'a rien à changer.
Publier une version
Section intitulée « Publier une version »-
Commiter l'état à publier, comme n'importe quel changement :
Fenêtre de terminal git add -Agit commit -m "feat(etiquette): suffixe facultatif" -
Poser un tag annoté, qui porte l'auteur, la date et un message :
Fenêtre de terminal git tag -a v1.1.0 -m "1.1.0 : suffixe facultatif"git push origin v1.1.0 -
Documenter la version dans un
CHANGELOG.md, au format Keep a Changelog :## [1.1.0]### Added- Variable `suffixe`, facultative (`default = ""`). Une configuration écritepour la 1.0.0 continue de s'appliquer sans changement.
Le CHANGELOG n'est pas une formalité : c'est ce qu'un consommateur lit
avant de monter de version, et la seule trace lisible de ce qui distingue
une 1.1.0 d'une 2.0.0.
Référencer une version côté consommateur
Section intitulée « Référencer une version côté consommateur »Une source Git se lit en trois morceaux :
module "etiquette" { source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.1.0"
prefixe = "atelier"}Le dépôt, puis le sous-répertoire derrière //, puis la révision
derrière ?ref=. L'ordre n'est pas libre : le sous-répertoire vient avant les
arguments de requête, faute de quoi la révision devient v1.1.0//etiquette et le
clone échoue.
Ce que ref accepte, et ce qui arrive sans lui
Section intitulée « Ce que ref accepte, et ce qui arrive sans lui »L'argument accepte « any value supported by the git checkout command », donc un
tag, une branche ou un SHA-1. Son absence, elle, n'est pas neutre :
Downloading git::file:///.../depot-modules for plaque...Sans ref, Terraform clone la branche par défaut. Mesuré sur une
bibliothèque locale : après un simple commit sur main, un terraform init -upgrade chez le consommateur ramène le nouveau code, alors qu'aucune
version n'a été publiée. C'est le pire réglage possible pour un module partagé,
et c'est le défaut.
Le fait qui change tout : un tag se déplace
Section intitulée « Le fait qui change tout : un tag se déplace »Le tag v1.0.0 désigne un commit, et cette association n'a rien de gravé.
Un git tag -f la change :
git tag -f v1.0.0 -m "meme version, autre code"Côté consommateur, rien n'a bougé dans la configuration. Un terraform init
seul ne change rien non plus, la copie installée étant conservée. Mais un
terraform init -upgrade ramène un autre code sous le même numéro :
Upgrading modules...Downloading git::file:///.../depot-modules?ref=v1.0.0 for plaque...Le contenu installé, avant et après, mesuré sur le même ?ref=v1.0.0 :
avant : value = "plaque ${var.etiquette} revision 1"apres : value = "plaque ${var.etiquette} revision 2"Aucun message, aucun avertissement : la version affichée est la même, le code ne l'est plus.
La seule référence immuable : le SHA-1
Section intitulée « La seule référence immuable : le SHA-1 »Reprenons l'expérience avec le SHA-1 du commit à la place du tag :
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=72a7572e7a9ede8d490c7611e0455491655301fd"Après le déplacement du tag et un init -upgrade, le contenu installé est
inchangé. Un SHA-1 désigne un contenu, pas un nom : c'est la seule
référence qu'on ne peut pas redéfinir.
Forme de ?ref= | Ce qu'elle garantit | Quand l'employer |
|---|---|---|
| absente | rien, la branche par défaut bouge à chaque commit | jamais sur un module partagé |
| une branche | rien de plus, avec un nom rassurant | jamais en production |
| un tag | la version publiée, tant que personne ne le déplace | environnements qui suivent les versions |
| un SHA-1 | le contenu, définitivement | socle figé, base d'audit, reproductibilité stricte |
Ce qui fige vraiment vos modules
Section intitulée « Ce qui fige vraiment vos modules »Un réflexe hérité des providers trompe beaucoup de monde. Le fichier
.terraform.lock.hcl ne verrouille que les providers : « the dependency lock
file tracks only provider dependencies ». Aucune sélection de version de
module n'y est enregistrée, et un projet sans provider n'en produit même
aucun.
La conséquence est directe. Un ~> posé sur un provider est rattrapé par le
verrou, qui rejoue la même version partout. Le même ~> posé sur un
module n'est rattrapé par rien : deux postes, ou deux exécutions de CI,
peuvent installer deux versions différentes sans qu'aucun fichier ne le trace. Le détail du
mécanisme de résolution est traité dans
utiliser un module du registre.
L'argument version n'existe pas hors registre
Section intitulée « L'argument version n'existe pas hors registre »Recopier un bloc de registre en ne changeant que la source est l'erreur la plus fréquente du sujet :
module "etiquette" { source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.1.0" version = "1.1.0"}Error: Invalid registry module source address
Failed to parse module registry address: a module registry source addressmust have either three or four slash-separated components.
Terraform assumed that you intended a module registry source address becauseyou also set the argument "version", which applies only to registry modules.Le message explique le raisonnement de l'outil : voyant version, il a
supposé une adresse de registre, et l'a analysée comme telle. La règle
officielle est nette, « You can only use the version argument when the source
argument points to a module listed in a registry », et elle vaut aussi pour les
chemins locaux, dont la version est par construction celle de l'appelant.
Ce qu'un module réutilisable ne doit pas imposer
Section intitulée « Ce qu'un module réutilisable ne doit pas imposer »La documentation distingue deux rôles, et c'est une distinction qu'on
oublie en écrivant le versions.tf d'un module : « Reusable modules should
constrain only their minimum allowed versions », tandis que « Root modules
should use a ~> constraint to set both a lower and upper bound ».
La raison se mesure. Un module qui pose une borne haute la propage à tous ses consommateurs :
- Finding hashicorp/local versions matching "~> 2.4.0, >= 2.9.0"...
Error: Failed to query available provider packages
Could not retrieve the list of available versions for providerhashicorp/local: no available releases match the given constraints ~> 2.4.0,>= 2.9.0Les contraintes du module et de la racine s'intersectent. Ici, le module a bloqué un projet qui avait besoin d'une version plus récente, et le seul correctif possible est de publier une nouvelle version du module.
| Où | Contrainte recommandée | Pourquoi |
|---|---|---|
| module réutilisable | >= 2.4.0 | annonce un plancher, laisse l'appelant décider |
| module racine | ~> 2.9.0 | fixe les deux bornes, c'est lui qui assume le risque |
Le workflow d'une montée de version
Section intitulée « Le workflow d'une montée de version »-
Lire le
CHANGELOGde la version visée, et décider si le changement est mineur ou majeur pour votre appel. -
Changer la révision dans un seul projet, celui qui coûte le moins cher à réparer :
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.2.0" -
Réinstaller le module, ce qu'un
initseul ne fait pas toujours :Fenêtre de terminal terraform init -upgrade -
Planifier, et lire le plan comme un diff : une destruction inattendue à ce stade signale un changement majeur mal documenté.
Fenêtre de terminal terraform plan -
Appliquer, puis recommencer sur l'environnement suivant.
Accélérer l'init : le paramètre depth
Section intitulée « Accélérer l'init : le paramètre depth »Sur un dépôt de modules à l'historique long, le clone domine le temps
d'init. L'argument depth le réduit, et la documentation recommande
« Setting depth to 1 is suitable for most cases » :
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.1.0&depth=1"La contrepartie n'est pas intuitive, et elle est mesurable : avec depth, la
révision est passée à git clone --branch, qui n'accepte qu'une branche ou
un tag.
fatal: Remote branch 72a7572e7a9ede8d490c7611e0455491655301fd not foundUn depth=1 et un épinglage par SHA-1 sont donc incompatibles : il faut
choisir entre la vitesse d'init et l'immuabilité de la référence.
Organiser un dépôt de modules
Section intitulée « Organiser un dépôt de modules »Un tag versionne tout le dépôt, pas un module. Ce détail dicte le choix d'organisation :
| Approche | Avantage | Coût |
|---|---|---|
| mono-dépôt, un tag pour tout | mise en place simple, une seule discipline | un changement dans un module retague l'ensemble |
| un dépôt par module | versions indépendantes | plus de dépôts, plus de CI, plus de droits |
Commencez en mono-dépôt, avec //<module> dans les sources pour cibler le
bon sous-répertoire. Passez au découpage le jour où deux modules évoluent à
des rythmes franchement différents, pas avant.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Correction |
|---|---|---|
| le module ne change pas après un nouveau tag | la copie installée est conservée | terraform init -upgrade |
| le code a changé sans changement de version | le tag a été déplacé | épingler un SHA-1, ou rétablir la discipline de tag |
Invalid registry module source address | argument version sur une source Git ou locale | retirer version, garder ?ref= |
invalid ref: "v1.1.0//etiquette" | // placé après le ?ref= | remettre le sous-répertoire avant |
Remote branch <sha> not found | depth combiné à un SHA-1 | retirer depth, ou référencer un tag |
no available releases match the given constraints | un module impose une borne haute | publier une version du module qui l'élargit |
le module suit main sans qu'on l'ait demandé | ?ref= absent | ajouter la révision |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab versionner ses modules vous fait publier trois versions d'un même
module, une mineure et une majeure, puis les consommer depuis trois projets : un
épinglé par SHA-1, un sur le tag mineur, un sur le tag majeur dont il
faut adapter les arguments. Les tests ne lisent aucun .tf : ils comparent les
modules.json, les sorties du state et le code installé, pour prouver
qu'une mineure reste rétrocompatible là où une majeure ne l'est pas. Il se joue
hors ligne.
À retenir
Section intitulée « À retenir »- Un tag annoté publie une version, un
CHANGELOGexplique ce qu'elle change. - Le numéro suit SemVer, et le critère est mécanique : ce qui oblige l'appelant à modifier son bloc est majeur.
- Une source Git s'écrit dépôt,
//sous-répertoire,?ref=révision, dans cet ordre. - Sans
ref, Terraform suit la branche par défaut : le pire réglage possible. - Un tag se déplace : même
?ref=, autre code, après un simpleinit -upgrade. - Seul le SHA-1 est immuable, mais il est incompatible avec
depth=1. - Le verrou ne suit que les providers : rien ne mémorise la révision d'un module.
- L'argument
versionn'existe que pour un registre, jamais sur Git ni en local. - Un module réutilisable ne contraint que son plancher ; la borne haute appartient à la racine.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui casse en pratique : le module qui
ne bouge pas, le code qui change sans changement de version, et l'argument
version refusé.
Même référence, autre code
Mesuré sur Terraform 1.15.4 contre un dépôt Git local. Après ungit tag -f v1.0.0 sur un nouveau commit :$ terraform init # rien ne bouge, la copie installée est conservée
Initializing modules...
$ terraform init -upgrade
Upgrading modules...
Downloading git::file:///.../depot-modules?ref=v1.0.0 for plaque...
Le contenu installé, avant et après, pour le même ?ref=v1.0.0 :avant : value = "plaque ${var.etiquette} revision 1"
apres : value = "plaque ${var.etiquette} revision 2"
Le tag reste utile pour la lisibilité, à condition d'une discipline de publication : un tag posé ne se déplace pas. Là où cette discipline n'est pas garantie, seul le SHA-1 protège.Les quatre formes de ?ref=
| Forme | Ce qu'elle garantit |
|---|---|
| absente | rien : la branche par défaut bouge à chaque commit |
| une branche | rien de plus, avec un nom rassurant |
| un tag | la version publiée, tant que personne ne le déplace |
| un SHA-1 | le contenu, définitivement |
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=72a7572e7a9ede8d490c7611e0455491655301fd"
Une réserve mesurée : le SHA-1 est incompatible avec depth=1, car la révision est alors passée à git clone --branch.Le verrou ignore les modules
« At present, the dependency lock file tracks only provider dependencies. Terraform does not remember version selections for remote modules. »La conséquence est directe :- un
~>posé sur un provider est rattrapé par le verrou, qui rejoue la même version partout ; - le même
~>posé sur un module n'est rattrapé par rien : deux postes, ou deux exécutions de CI, peuvent installer deux versions différentes sans qu'aucun fichier ne le trace.
.terraform.lock.hcl.version bascule Terraform en mode registre
module "etiquette" {
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.1.0"
version = "1.1.0"
}
Error: Invalid registry module source address
Failed to parse module registry address: a module registry source address
must have either three or four slash-separated components.
Terraform assumed that you intended a module registry source address because
you also set the argument "version", which applies only to registry modules.
La règle : « You can only use the version argument when the source argument points to a module listed in a registry. » Un module local, lui, « always shares the same version as its caller ».Le critère est mécanique
| Modification | Version |
|---|---|
| corriger un message d'erreur | PATCH |
ajouter une variable avec default |
MINOR |
| ajouter une sortie | MINOR |
| renommer une variable | MAJOR |
| supprimer une variable | MAJOR |
retirer le default d'une variable |
MAJOR |
Sans référence, le module suit la branche
Downloading git::file:///.../depot-modules for plaque...
« When ref is omitted, Terraform clones and uses the default branch referenced by HEAD. »Mesuré sur 1.15.4 : après un simple commit sur main, un init -upgrade chez le consommateur installe le nouveau code, alors qu'aucun tag n'a été posé. Un module partagé doit donc toujours porter une révision explicite, tag ou SHA-1.Deux rôles, deux contraintes
« Reusable modules should constrain only their minimum allowed versions », tandis que « Root modules should use a~> constraint to set both a lower and upper bound ».Mesuré sur 1.15.4, avec un module en ~> 2.4.0 et une racine en >= 2.9 :- Finding hashicorp/local versions matching "~> 2.4.0, >= 2.9.0"...
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/local: no available releases match the given constraints ~> 2.4.0,
>= 2.9.0
Le seul correctif possible est de publier une version du module qui élargit sa contrainte : le consommateur, lui, est bloqué.Le workflow, dans l'ordre
- Lire le
CHANGELOGde la version visée. - Changer la révision dans le projet le moins coûteux à réparer :
source = "git::https://exemple.fr/infra/modules.git//etiquette?ref=v1.2.0" - Réinstaller le module :
terraform init -upgrade - Planifier et lire le plan comme un diff : une destruction inattendue signale un changement majeur mal documenté.
- Appliquer, puis passer à l'environnement suivant.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Bonnes pratiques des modules : les règles de conception qui rendent une montée de version supportable.
- Anti-patterns des modules : les erreurs qui obligent à casser l'interface, donc à publier une majeure.
- Sources de modules : la référence officielle :
ref,depth, sous-répertoires et protocoles. - Contraintes de version : la référence officielle : la sémantique de
~>et le rôle des modules réutilisables.