
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Le socle officiel, et pourquoi le
READMEen fait partie - Où placer un module imbriqué, et comment l'appeler
- Pourquoi un exemple autonome se range sous
examples/ - Ce qu'un
typesur un output change dans la sortie machine - Le seul nom de fichier qui ait un effet fonctionnel
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Savoir créer un module et l'appeler depuis une configuration racine.
Existe-t-il une norme officielle ?
Section intitulée « Existe-t-il une norme officielle ? »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.
Le socle recommandé
Section intitulée « Le socle recommandé »Quatre fichiers, et le README en fait partie :
minimal-module/├── README.md├── main.tf├── variables.tf└── outputs.tf| Fichier | Ce qu'il contient |
|---|---|
README.md | ce que fait le module et comment l'appeler |
main.tf | toutes les ressources et data sources |
variables.tf | les variables, par ordre alphabétique |
outputs.tf | les 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é.
Le LICENSE, au même niveau que le README
Section intitulée « Le LICENSE, au même niveau que le README »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.
Les modules imbriqués
Section intitulée « Les modules imbriqués »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.mdis 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 :
terraform plan -out=p.tfplanterraform 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é.
Le répertoire examples/
Section intitulée « Le répertoire examples/ »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 :
terraform -chdir=examples/minimal initterraform -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.
Un type sur chaque output
Section intitulée « Un type sur chaque output »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.
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.
Le seul nom de fichier qui ait un effet
Section intitulée « Le seul nom de fichier qui ait un effet »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 :
terraform show -json | jq '.values.root_module.resources[] | select(.address == "local_file.registre") | .values.file_permission'"0600"Dépannage
Section intitulée « Dépannage »Les erreurs ci-dessous sont celles que produit un découpage mal mené.
| Symptôme | Cause probable | Solution |
|---|---|---|
Duplicate resource configuration | Le même bloc dans deux fichiers | Fusionner, ou renommer en override.tf si la surcharge est voulue |
L'output ressort en ["object", ...] | Pas de type déclaré sur l'output | Ajouter la ligne type |
| Le sous-module n'apparaît pas dans le registre | Pas de README.md dans modules/<nom>/ | L'ajouter : sans lui, le sous-module est réputé interne |
| Terraform retélécharge un sous-module | source distant au lieu d'un chemin relatif | Utiliser ./modules/<nom> |
| L'exemple ne se valide pas seul | Il dépend de la racine | Le rendre autonome sous examples/ |
| Le module n'est pas adopté en interne | Pas de LICENSE | En ajouter un, même non libre |
Mettre en pratique
Section intitulée « Mettre en pratique »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.
À retenir
Section intitulée « À retenir »- 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. LeREADMEen fait partie, et n'a pas à documenter entrées et sorties. - Le bloc
terraformva dansterraform.tfselon le style guide ;versions.tfn'apparaît dans aucune page officielle. - Un
LICENSEconditionne l'adoption d'un module, même non libre. - Un sous-module de
modules/n'est public que s'il porte son propreREADME.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 unsourced'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.tfet*_override.tfsont chargés en dernier et fusionnés : ce sont les seuls noms de fichiers à avoir un effet.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »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.
Une norme documentée, pas une contrainte du moteur
Terraform charge tous les.tf d'un répertoire, donc le découpage ne change rien au résultat. La documentation le recommande pourtant sans ambiguïté : « These are the recommended filenames for a minimal module, even if they're empty. »La raison est technique :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 conforme est indexable et documentable sans travail supplémentaire.
Le socle officiel
minimal-module/
├── README.md
├── main.tf
├── variables.tf
└── outputs.tf
| Fichier | Ce qu'il contient |
|---|---|
README.md |
ce que fait le module et comment l'appeler |
main.tf |
toutes les ressources et data sources |
variables.tf |
les variables, par ordre alphabétique |
outputs.tf |
les outputs, par ordre alphabétique |
main.tf, le style guide y rangeant « all resource and data source blocks ».terraform.tf, selon le style guide
La recommandation est littérale : « Aterraform.tf file that contains a single terraform block which defines your required_version and required_providers ».Le bloc terraform ne fait d'ailleurs pas partie du socle minimal, qui se limite à README.md, main.tf, variables.tf et outputs.tf.À noter : une configuration peut comporter plusieurs blocs terraform, et la liste officielle des noms de fichiers mentionne également backend.tf et providers.tf.Le README décide de la visibilité
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.
L'appel se fait par chemin relatif :module "carte" {
source = "./modules/carte"
}
La raison est technique : « so that Terraform will consider them to be part of the same repository or package, rather than downloading them again separately ».Quand un dépôt contient plusieurs sous-modules, ils devraient être composables par l'appelant plutôt que s'appeler entre eux.Des exemples qui tiennent debout seuls
terraform -chdir=examples/minimal init
terraform -chdir=examples/minimal validate -json
{"valid": true, "error_count": 0}
La documentation les attend « under the examples/ subdirectory at the root of the repository », y compris pour les sous-modules.Une règle particulière s'y applique : « any module blocks should have their source set to the address an external caller would use, not to a relative path ». Ces exemples sont faits pour être copiés-collés.Ce n'est pas cosmétique
Le style guide 1.15 l'exige : « Like you would for variables, provide atype and description for each output ».Mesuré sur Terraform 1.15.4, sur la même valeur :| Déclaration | terraform output -json |
|---|---|
type = map(string) |
["map", "string"] |
pas de type |
["object", {"nord": "string", "sud": "string"}] |
Type, Description, Value, Sensitive.Une seule exception, et elle est fonctionnelle
// main.tf
resource "local_file" "registre" {
file_permission = "0644"
}
// override.tf
resource "local_file" "registre" {
file_permission = "0600"
}
Vérifié sur 1.15.4 : le state porte "0600", et le fichier sur le disque aussi. La doc l'énonce ainsi : « Terraform loads this and all files ending with _override.tf last. »Renommez-le en surcharge.tf et la configuration casse :Error: Duplicate resource "local_file" configuration
La documentation déconseille l'usage courant de ces fichiers : réservez-les aux cas où vous ne pouvez pas modifier le fichier d'origine.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Utiliser un module local partagé : sortir ce module du projet pour le partager, et ce que
initen enregistre. - Versionner ses modules : figer cette structure derrière un tag Git.
- Bonnes pratiques pour les modules : ce qui distingue un module tenable d'un module jetable.
- Publier un module sur le registre : ce que la structure standard permet une fois publiée.
- Standard Module Structure : la référence officielle : l'arbre complet et les règles de visibilité.
- Style guide Terraform : les noms de fichiers et l'ordre des arguments.
- Fichiers de surcharge : la sémantique de fusion de
override.tf.