Aller au contenu
Infrastructure as Code medium

Versionner ses modules Terraform avec Git

35 min de lecture

logo terraform

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.

  • 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 ref accepte, 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

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 aussi

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

ComposantQuand l'incrémenterExemple
MAJORchangement incompatible1.1.02.0.0
MINORajout rétrocompatible1.0.11.1.0
PATCHcorrection rétrocompatible1.0.01.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.

  1. Commiter l'état à publier, comme n'importe quel changement :

    Fenêtre de terminal
    git add -A
    git commit -m "feat(etiquette): suffixe facultatif"
  2. 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
  3. Documenter la version dans un CHANGELOG.md, au format Keep a Changelog :

    ## [1.1.0]
    ### Added
    - Variable `suffixe`, facultative (`default = ""`). Une configuration écrite
    pour 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.

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.

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 tag v1.0.0 désigne un commit, et cette association n'a rien de gravé. Un git tag -f la change :

Fenêtre de terminal
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.

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 garantitQuand l'employer
absenterien, la branche par défaut bouge à chaque commitjamais sur un module partagé
une brancherien de plus, avec un nom rassurantjamais en production
un tagla version publiée, tant que personne ne le déplaceenvironnements qui suivent les versions
un SHA-1le contenu, définitivementsocle figé, base d'audit, reproductibilité stricte

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.

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

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.

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 provider
hashicorp/local: no available releases match the given constraints ~> 2.4.0,
>= 2.9.0

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

Contrainte recommandéePourquoi
module réutilisable>= 2.4.0annonce un plancher, laisse l'appelant décider
module racine~> 2.9.0fixe les deux bornes, c'est lui qui assume le risque
  1. Lire le CHANGELOG de la version visée, et décider si le changement est mineur ou majeur pour votre appel.

  2. 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"
  3. Réinstaller le module, ce qu'un init seul ne fait pas toujours :

    Fenêtre de terminal
    terraform init -upgrade
  4. 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
  5. Appliquer, puis recommencer sur l'environnement suivant.

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 found

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

Un tag versionne tout le dépôt, pas un module. Ce détail dicte le choix d'organisation :

ApprocheAvantageCoût
mono-dépôt, un tag pour toutmise en place simple, une seule disciplineun changement dans un module retague l'ensemble
un dépôt par moduleversions indépendantesplus 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.

SymptômeCause probableCorrection
le module ne change pas après un nouveau tagla copie installée est conservéeterraform init -upgrade
le code a changé sans changement de versionle tag a été déplacéépingler un SHA-1, ou rétablir la discipline de tag
Invalid registry module source addressargument version sur une source Git ou localeretirer version, garder ?ref=
invalid ref: "v1.1.0//etiquette"// placé après le ?ref=remettre le sous-répertoire avant
Remote branch <sha> not founddepth combiné à un SHA-1retirer depth, ou référencer un tag
no available releases match the given constraintsun module impose une borne hautepublier une version du module qui l'élargit
le module suit main sans qu'on l'ait demandé?ref= absentajouter la révision

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.

  • Un tag annoté publie une version, un CHANGELOG explique 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 simple init -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 version n'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.

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

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