
Un dépôt Terraform bien rangé se lit en quelques secondes : on sait où sont
les variables, où est le backend, ce qui est versionné. Le découpage
qui y mène est simple, mais il traîne deux idées fausses tenaces, celle du nom de
fichier « standard » qui n'est pas celui de la documentation, et celle du
découpage sans risque parce que terraform validate passe.
Tout ce qui suit a été exécuté sur Terraform v1.15.4 : sorties, codes de
retour et verdicts de git check-ignore compris.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Le socle de fichiers que la documentation nomme, et ceux qu'elle ignore
- Comment prouver qu'un découpage n'a rien changé, puisque
validatene le fait pas - Le seul nom de fichier qui ait un effet fonctionnel
- Pourquoi
-recursivedécide de la valeur de votre contrôle de formatage - Le motif de
.gitignoreque presque tout le monde rate
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Une configuration qui fonctionne, même monolithique : ce guide range, il ne réécrit pas.
Le socle de fichiers, et les noms qui surprennent
Section intitulée « Le socle de fichiers, et les noms qui surprennent »Terraform charge tous les .tf d'un répertoire et les traite comme un seul
document. Aucun nom n'est donc imposé par le moteur, ce qui rend d'autant plus
utile de suivre la convention publiée plutôt que la sienne. Le style guide
officiel nomme ceci :
| Fichier | Ce qu'il contient |
|---|---|
terraform.tf | un seul bloc terraform : required_version, required_providers |
providers.tf | tous les blocs provider, dont une configuration par défaut |
variables.tf | tous les variable, en ordre alphabétique |
outputs.tf | tous les output, en ordre alphabétique |
main.tf | les resource et les data source |
backend.tf | la configuration du backend, quand il y en a une |
locals.tf | les valeurs locales, si elles deviennent nombreuses |
Deux surprises pour beaucoup de lecteurs. Le nom versions.tf, très répandu,
n'apparaît nulle part dans la documentation : celui qui y figure est
terraform.tf. Et le bloc provider a son fichier, il ne partage pas celui du
bloc terraform.
Le seul nom de fichier qui ait un effet
Section intitulée « Le seul nom de fichier qui ait un effet »Il existe une exception à « les noms sont cosmétiques », et elle est rarement
énoncée : override.tf, override.tf.json et tout fichier terminé par
_override.tf. Terraform les charge en dernier et fusionne leur contenu
par-dessus les blocs existants.
Ce n'est pas un fichier généré, contrairement à ce qu'on lit parfois : le style guide le range parmi les fichiers du module. Il sert à surcharger localement, et c'est précisément pour cela qu'on l'exclut souvent du dépôt.
Découper sans changer le plan
Section intitulée « Découper sans changer le plan »Voici le point que la plupart des guides ratent. terraform validate ne
prouve pas qu'un découpage est neutre :
The
validatecommand does not check if argument values are valid for a specific provider, but it will verify that they are the correct type. It does not evaluate any existing state.
Mesuré sur un découpage où une ressource et sa sortie ont disparu :
Success! The configuration is valid, but there were some validation warningsLa configuration est valide, et le plan n'a plus rien à voir. La seule preuve d'invariance est la comparaison de deux plans :
-
Avant de toucher au code, enregistrer le plan de référence :
Fenêtre de terminal terraform plan -out=avant.tfplanterraform show -json avant.tfplan > avant.json -
Découper, en déplaçant les blocs sans les réécrire.
-
Rejouer le plan, et comparer ce qui doit être stable :
Fenêtre de terminal terraform plan -out=apres.tfplanterraform show -json apres.tfplan > apres.jsondiff <(jq -S '.planned_values, .resource_changes' avant.json) \<(jq -S '.planned_values, .resource_changes' apres.json)
La section configuration du JSON, elle, change légitimement : c'est la
représentation du code, qui reflète le découpage. Ce sont planned_values,
resource_changes et output_changes qui doivent rester identiques.
Formater toute l'arborescence
Section intitulée « Formater toute l'arborescence »terraform fmt -check ne regarde que le répertoire courant. Sur un dépôt à
plusieurs niveaux, c'est un contrôle qui rassure sans rien vérifier.
terraform fmt -check -recursiveLa documentation nomme l'option pour ce cas : « The terraform fmt command can use
the -recursive flag for subdirectories. » Deux réflexes pour l'intégration
continue : toujours -recursive, et tester un code de retour différent de
zéro plutôt qu'égal à 1, puisque -check sort en 3 quand un fichier doit
être reformaté.
Terraform n'embarque par ailleurs aucun linter : fmt met en forme,
validate vérifie la cohérence interne, et les règles d'équipe relèvent
d'un outil séparé, TFLint en tête.
Le .gitignore, et le piège du plan enregistré
Section intitulée « Le .gitignore, et le piège du plan enregistré »Trois familles ne se committent jamais, et une doit toujours l'être :
| Fichier | Versionné ? |
|---|---|
.terraform/ | non, répertoire de travail |
terraform.tfstate, *.tfstate.backup | non, l'état contient les valeurs |
un plan enregistré par -out= | non, il contient les valeurs résolues |
.terraform.lock.hcl | oui, toujours |
Le piège tient au nom du plan enregistré :
terraform plan -out=tfplanCe fichier n'a aucune extension. Un .gitignore qui ne connaît que *.tfplan
ne l'attrape pas, et le fichier contient les valeurs résolues, secrets
compris. Le motif doit donc couvrir les deux formes :
.terraform/*.tfstate*.tfstate.*
tfplan*.tfplan
!.terraform.lock.hclEnfin, un .gitignore ne se vérifie pas en le lisant, mais en le faisant
travailler :
git check-ignore -v projet/tfplan projet/terraform.tfstategit check-ignore projet/.terraform.lock.hcl && echo "PROBLEME : le verrou est ignore"Ranger un dépôt qui contient des modules
Section intitulée « Ranger un dépôt qui contient des modules »Un module vit sous modules/<nom>/, et il porte son socle, README.md
compris : la doc attache une conséquence à ce fichier, « nested modules with a
README are considered publicly usable, those without are internal-only ». Un module
sans README est donc déclaré interne, ce qui est rarement l'intention.
Les exemples d'usage vont sous examples/, avec une règle propre : leur
source doit pointer l'adresse externe du module, pas un chemin relatif,
puisqu'ils sont faits pour être recopiés ailleurs. Et l'appel d'un module imbriqué
depuis la racine se fait par un chemin relatif, ./modules/reseau, ce qui le
garde dans le même paquet que son appelant.
depot/├── terraform.tf├── providers.tf├── variables.tf├── outputs.tf├── main.tf├── .gitignore├── modules/│ └── reseau/│ ├── README.md│ ├── main.tf│ ├── variables.tf│ └── outputs.tf└── examples/ └── minimal/ └── main.tfDépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Correction |
|---|---|---|
| le plan change après un découpage | un bloc perdu ou une valeur déplacée | comparer les deux JSON, validate ne le dira pas |
| la CI est verte, le code est mal formaté | fmt -check sans -recursive | ajouter l'option, tester un code non nul |
| un plan enregistré part dans un commit | motif *.tfplan seul | ajouter tfplan, sans extension |
.terraform.lock.hcl absent du dépôt | motif trop large sur .terraform* | ajouter !.terraform.lock.hcl |
| une surcharge locale part dans le dépôt | override.tf versionné | l'ignorer, en sachant que ce n'est pas un fichier généré |
| un module imbriqué est retéléchargé | source distant au lieu d'un chemin relatif | ./modules/<nom> |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab organiser un dépôt Terraform remet un main.tf monolithique à découper
selon le socle officiel. Les tests replanifient la configuration d'origine dans
un répertoire temporaire et comparent les deux empreintes de plan : un
découpage qui perd une ressource passe validate avec Success! et tombe ici.
Ils mettent aussi le .gitignore à l'épreuve avec git check-ignore, dans un dépôt
jetable. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Le socle officiel nomme
terraform.tfetproviders.tf;versions.tfn'apparaît nulle part dans la documentation. - Variables et outputs se déclarent en ordre alphabétique.
validatene prouve pas qu'un découpage est neutre : il ignore l'état et la valeur des arguments.- La preuve, c'est le diff de
planned_valuesetresource_changesentre deux plans. override.tfest le seul nom de fichier qui change le résultat : chargé en dernier, il fusionne.fmt -checksans-recursivene voit qu'un répertoire, et sort en 3 en cas d'écart.- Un plan enregistré s'appelle souvent
tfplan, sans extension : le.gitignoredoit le nommer. - Le
.terraform.lock.hclse committe toujours, et se vérifie avecgit check-ignore.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui casse en pratique : le découpage qui change le plan sans prévenir, la CI verte qui n'a rien formaté, et le plan enregistré qui part dans un commit.
Le socle publié
| Fichier | Ce qu'il contient |
|---|---|
terraform.tf |
un seul bloc terraform |
providers.tf |
tous les blocs provider |
variables.tf |
les variable, en ordre alphabétique |
outputs.tf |
les output, en ordre alphabétique |
main.tf |
les resource et les data source |
backend.tf |
le backend, quand il y en a un |
.tf d'un répertoire et les traite comme un seul document. C'est justement pour cela qu'il vaut mieux suivre la convention publiée que la sienne.Mesuré : un bloc perdu passe validate
Sur un découpage où une ressource et sa sortie ont disparu, Terraform 1.15.4 répond :Success! The configuration is valid, but there were some validation warnings
« The validate command does not check if argument values are valid for a specific provider [...] It does not evaluate any existing state. »Le contrôle qui prouve quelque chose :terraform plan -out=apres.tfplan
terraform show -json apres.tfplan > apres.json
diff <(jq -S '.planned_values, .resource_changes' avant.json) \
<(jq -S '.planned_values, .resource_changes' apres.json)
La section configuration change légitimement, puisqu'elle reflète le découpage.-recursive, et un code de retour à 3
terraform fmt -check -recursive
« The terraform fmt command can use the -recursive flag for subdirectories. »Deuxième détail, mesuré : en cas d'écart, -check sort en 3, pas en 1. Un pipeline qui teste -eq 1 ne détecte donc rien. Testez un code différent de zéro.Et rappelez-vous que fmt met en forme sans rien juger d'autre : Terraform n'embarque aucun linter, les règles d'équipe relèvent d'un outil séparé comme TFLint.Le motif que presque tout le monde rate
.terraform/
*.tfstate
*.tfstate.*
tfplan
*.tfplan
!.terraform.lock.hcl
Le plan enregistré est le piège : -out=tfplan produit un fichier sans extension, et ce fichier contient les valeurs résolues, secrets compris.À l'inverse, .terraform.lock.hcl doit toujours être versionné : il fige les versions de providers pour toute l'équipe.Vérifiez en faisant travailler le fichier, pas en le lisant :git check-ignore -v projet/tfplan
git check-ignore projet/.terraform.lock.hcl && echo "PROBLEME"
L'exception qui contredit la règle
Terraform charge tous les.tf d'un répertoire comme un seul document, à ceci près : « Terraform loads override.tf and all files ending with _override.tf last », et leur contenu fusionne par-dessus les blocs déjà déclarés.Deux conséquences :- un
override.tfchange le résultat de l'apply, ce qu'aucun autre nom ne fait ; - ce n'est pas un fichier généré : le style guide le range parmi les fichiers du module.
Deux répertoires, deux règles
depot/
├── modules/
│ └── reseau/
│ ├── README.md
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── examples/
└── minimal/
└── main.tf
Le README.md d'un module imbriqué n'est pas décoratif : « nested modules with a README are considered publicly usable, those without are internal-only ». Un module sans README est donc déclaré interne.Côté exemples, leur source doit pointer l'adresse externe du module, pas un chemin relatif, parce qu'un exemple finit copié dans un autre dépôt.Des racines, pas des dossiers
Chaque répertoire contenant un blocterraform et des ressources est une configuration racine : il a son terraform init, son backend et son état.Conséquence directe, souvent découverte trop tard : aucune expression ne traverse la frontière. module.reseau.id fonctionne entre un appelant et son module, jamais entre deux racines.La voie officielle est la lecture de l'état distant de l'autre racine, via ses outputs. C'est aussi pourquoi une valeur partagée entre environnements gagne à vivre dans un module commun ou une source de données, plutôt que dans un output à republier.Le seul fichier généré qui se committe
Le fichier de verrouillage enregistre, pour chaque provider, la version retenue, la contrainte qui l'a permise et des empreintes de vérification.provider "registry.terraform.io/hashicorp/local" {
version = "2.9.0"
constraints = ">= 2.5.0"
hashes = [...]
}
Sans lui, deux postes peuvent installer deux versions différentes du même provider.Une limite à connaître : il ne suit que les providers. Aucune sélection de version de module n'y figure, et un projet dont la seule dépendance est un module n'en produit même pas.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Structure standard d'un module : le socle d'un module, ses exemples et son README.
- Quiz Organiser les environnements Terraform : un contrôle des acquis sur l'organisation et l'isolation.
- Le style guide officiel : noms de fichiers, ordre des blocs et recommandations d'outillage.
- Structure des modules : la référence officielle :
modules/,examples/et la règle du README.