Aller au contenu
Infrastructure as Code medium

Structure standard d'un module Terraform

30 min de lecture

logo terraform

Terraform charge tous les fichiers .tf d'un répertoire et les traite comme un seul document : découper en plusieurs fichiers ne change donc rien au résultat. Ce découpage n'en est pas moins normé, parce que l'outillage s'appuie dessus pour générer la documentation et indexer les modules. Ce guide s'adresse à qui veut publier ou partager un module : vous y verrez le socle de fichiers recommandé, la place des modules imbriqués et des exemples, et la seule famille de noms de fichiers qui modifie réellement la configuration.

Tous les comportements de ce guide ont été exécutés sur Terraform v1.15.4 avec les providers local et random : sorties, erreurs et types JSON compris.

  • Le socle officiel, et pourquoi le README en fait partie
  • placer un module imbriqué, et comment l'appeler
  • Pourquoi un exemple autonome se range sous examples/
  • Ce qu'un type sur un output change dans la sortie machine
  • Le seul nom de fichier qui ait un effet fonctionnel

Oui. On lit souvent que Terraform n'impose aucune convention de nommage, ce qui est vrai du moteur mais faux de la documentation : une page entière s'intitule Standard Module Structure, et le style guide comporte une section File names. La recommandation est explicite : « These are the recommended filenames for a minimal module, even if they're empty. »

La raison est technique, pas esthétique :

Terraform tooling is built to understand the standard module structure and use that structure to generate documentation, index modules for the module registry, and more.

Un module qui suit cette structure est indexable et documentable sans travail supplémentaire. Un module qui s'en écarte oblige chaque outil, et chaque relecteur, à deviner.

Quatre fichiers, et le README en fait partie :

minimal-module/
├── README.md
├── main.tf
├── variables.tf
└── outputs.tf
FichierCe qu'il contient
README.mdce que fait le module et comment l'appeler
main.tftoutes les ressources et data sources
variables.tfles variables, par ordre alphabétique
outputs.tfles outputs, par ordre alphabétique

Deux précisions que l'on rate souvent. D'abord, le README n'a pas à documenter les entrées et les sorties : « The README doesn't need to document inputs or outputs of the module because tooling will automatically generate this ». Ensuite, les data sources vont dans main.tf : le style guide y range « all resource and data source blocks », le découpage par domaine n'étant admis que lorsque le fichier devient ingérable, sans seuil chiffré.

C'est le fichier que l'on oublie, et la documentation en fait une condition d'adoption : « many organizations will not adopt a module unless a clear license is present. We recommend always having a license file, even if it is not an open source license. »

Ajoutez-y la convention de nommage du dépôt, imposée par le registre : terraform-<PROVIDER>-<NAME>. Un module local peut s'en passer ; un module publié, non.

Un module peut en contenir d'autres, sous modules/. La documentation attache à ce répertoire une règle de visibilité qui tient à un seul fichier :

Any nested module with a README.md is considered usable by an external user. If a README doesn't exist, it is considered for internal use only.

Autrement dit, le README décide si un sous-module fait partie de l'interface publique. Rien d'autre ne le signale.

L'appel se fait par un chemin relatif :

module "carte" {
source = "./modules/carte"
for_each = var.zones
zone = each.key
}

Là encore, la raison est technique, et elle porte sur le téléchargement : « they should use relative paths like ./modules/consul-cluster so that Terraform will consider them to be part of the same repository or package, rather than downloading them again separately ». Le JSON du plan permet de le vérifier :

Fenêtre de terminal
terraform plan -out=p.tfplan
terraform show -json p.tfplan | jq '.configuration.root_module.module_calls.carte.source'
"./modules/carte"

Un dernier point de conception : quand un dépôt contient plusieurs sous-modules, ils devraient être composables par l'appelant plutôt que s'appeler entre eux, afin d'éviter un arbre profondément imbriqué.

La documentation attend des exemples d'utilisation « under the examples/ subdirectory at the root of the repository ». Leur intérêt est qu'ils se valident seuls, indépendamment de la racine :

Fenêtre de terminal
terraform -chdir=examples/minimal init
terraform -chdir=examples/minimal validate -json
{"valid": true, "error_count": 0}

Une règle particulière s'y applique : dans un exemple, « any module blocks should have their source set to the address an external caller would use, not to a relative path ». La raison est simple, ces exemples finissent copiés-collés ailleurs, où un chemin relatif n'aurait aucun sens.

Depuis la 1.15, le style guide aligne les sorties sur les variables : « Like you would for variables, provide a type and description for each output », dans l'ordre Type, Description, Value, Sensitive.

output "emplacements" {
type = map(string)
description = "Chemin de la carte, par zone."
value = { for nom, instance in module.carte : nom => instance.emplacement }
}

Ce n'est pas une coquetterie de style : le type déclaré change ce que rend la sortie machine, celle que consomment vos scripts.

Fenêtre de terminal
terraform output -json | jq '.emplacements.type'
["map", "string"]

Retirez la ligne type, et la même valeur ressort autrement, sous son type inféré, décrit clé par clé :

["object", {"nord": "string", "sud": "string"}]

Un consommateur qui attendait une map reçoit un objet dont la forme change à chaque ajout de zone.

Voici l'exception à « les noms de fichiers sont cosmétiques », et elle est fonctionnelle. Terraform charge en dernier les fichiers nommés override.tf et tous ceux terminant par _override.tf, puis fusionne leurs blocs par-dessus les autres.

Une ressource déclarée dans main.tf :

resource "local_file" "registre" {
filename = "${path.root}/registre.txt"
content = "registre des cartes\n"
file_permission = "0644"
}

Un override.tf qui redéclare la même ressource, avec le seul attribut à changer :

resource "local_file" "registre" {
file_permission = "0600"
}

Le second gagne, et l'état appliqué le confirme :

Fenêtre de terminal
terraform show -json | jq '.values.root_module.resources[]
| select(.address == "local_file.registre") | .values.file_permission'
"0600"

Les erreurs ci-dessous sont celles que produit un découpage mal mené.

SymptômeCause probableSolution
Duplicate resource configurationLe même bloc dans deux fichiersFusionner, ou renommer en override.tf si la surcharge est voulue
L'output ressort en ["object", ...]Pas de type déclaré sur l'outputAjouter la ligne type
Le sous-module n'apparaît pas dans le registrePas de README.md dans modules/<nom>/L'ajouter : sans lui, le sous-module est réputé interne
Terraform retélécharge un sous-modulesource distant au lieu d'un chemin relatifUtiliser ./modules/<nom>
L'exemple ne se valide pas seulIl dépend de la racineLe rendre autonome sous examples/
Le module n'est pas adopté en internePas de LICENSEEn ajouter un, même non libre

Le lab structure d'un module remet un fichier unique où le bloc terraform, trois variables, deux ressources et une sortie sont empilés dans le désordre. Il faut l'éclater selon la structure standard, y ajouter un module imbriqué appelé par chemin relatif, un exemple autonome, et démontrer qu'un override.tf change l'état appliqué là où aucun autre nom de fichier ne le peut. Il se joue hors ligne.

  • La Standard Module Structure existe bel et bien : c'est une page officielle, et l'outillage s'appuie dessus.
  • Le socle minimal est README.md, main.tf, variables.tf, outputs.tf. Le README en fait partie, et n'a pas à documenter entrées et sorties.
  • Le bloc terraform va dans terraform.tf selon le style guide ; versions.tf n'apparaît dans aucune page officielle.
  • Un LICENSE conditionne l'adoption d'un module, même non libre.
  • Un sous-module de modules/ n'est public que s'il porte son propre README.md.
  • Les sous-modules s'appellent par chemin relatif, pour rester dans le même paquet.
  • Les exemples vont sous examples/ et doivent se valider seuls, avec un source d'appelant externe.
  • Chaque output porte un type : sans lui, la sortie machine expose le type inféré, qui change de forme avec les données.
  • override.tf et *_override.tf sont chargés en dernier et fusionnés : ce sont les seuls noms de fichiers à avoir un effet.

Les questions ci-dessous portent sur ce qui revient le plus : le nom du fichier qui porte le bloc terraform, l'utilité réelle du découpage, et ce que override.tf fait de particulier.

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