Aller au contenu
Infrastructure as Code medium

Organiser un dépôt Terraform

30 min de lecture

logo terraform

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.

  • Le socle de fichiers que la documentation nomme, et ceux qu'elle ignore
  • Comment prouver qu'un découpage n'a rien changé, puisque validate ne le fait pas
  • Le seul nom de fichier qui ait un effet fonctionnel
  • Pourquoi -recursive décide de la valeur de votre contrôle de formatage
  • Le motif de .gitignore que presque tout le monde rate
  • Terraform 1.11 ou plus récent (installer Terraform).
  • Une configuration qui fonctionne, même monolithique : ce guide range, il ne réécrit pas.

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 :

FichierCe qu'il contient
terraform.tfun seul bloc terraform : required_version, required_providers
providers.tftous les blocs provider, dont une configuration par défaut
variables.tftous les variable, en ordre alphabétique
outputs.tftous les output, en ordre alphabétique
main.tfles resource et les data source
backend.tfla configuration du backend, quand il y en a une
locals.tfles 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.

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.

Voici le point que la plupart des guides ratent. terraform validate ne prouve pas qu'un découpage est neutre :

The validate command 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 warnings

La configuration est valide, et le plan n'a plus rien à voir. La seule preuve d'invariance est la comparaison de deux plans :

  1. Avant de toucher au code, enregistrer le plan de référence :

    Fenêtre de terminal
    terraform plan -out=avant.tfplan
    terraform show -json avant.tfplan > avant.json
  2. Découper, en déplaçant les blocs sans les réécrire.

  3. Rejouer le plan, et comparer ce qui doit être stable :

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

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.

Fenêtre de terminal
terraform fmt -check -recursive

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

Trois familles ne se committent jamais, et une doit toujours l'être :

FichierVersionné ?
.terraform/non, répertoire de travail
terraform.tfstate, *.tfstate.backupnon, l'état contient les valeurs
un plan enregistré par -out=non, il contient les valeurs résolues
.terraform.lock.hcloui, toujours

Le piège tient au nom du plan enregistré :

Fenêtre de terminal
terraform plan -out=tfplan

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

Enfin, un .gitignore ne se vérifie pas en le lisant, mais en le faisant travailler :

Fenêtre de terminal
git check-ignore -v projet/tfplan projet/terraform.tfstate
git check-ignore projet/.terraform.lock.hcl && echo "PROBLEME : le verrou est ignore"

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.tf
SymptômeCause probableCorrection
le plan change après un découpageun bloc perdu ou une valeur déplacéecomparer les deux JSON, validate ne le dira pas
la CI est verte, le code est mal formatéfmt -check sans -recursiveajouter l'option, tester un code non nul
un plan enregistré part dans un commitmotif *.tfplan seulajouter tfplan, sans extension
.terraform.lock.hcl absent du dépôtmotif trop large sur .terraform*ajouter !.terraform.lock.hcl
une surcharge locale part dans le dépôtoverride.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>

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.

  • Le socle officiel nomme terraform.tf et providers.tf ; versions.tf n'apparaît nulle part dans la documentation.
  • Variables et outputs se déclarent en ordre alphabétique.
  • validate ne 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_values et resource_changes entre deux plans.
  • override.tf est le seul nom de fichier qui change le résultat : chargé en dernier, il fusionne.
  • fmt -check sans -recursive ne voit qu'un répertoire, et sort en 3 en cas d'écart.
  • Un plan enregistré s'appelle souvent tfplan, sans extension : le .gitignore doit le nommer.
  • Le .terraform.lock.hcl se committe toujours, et se vérifie avec git check-ignore.

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.

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