Aller au contenu
Infrastructure as Code medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

Style guide Terraform : conventions et bonnes pratiques HCL

11 min de lecture

logo terraform

Six mois après l’avoir écrit, votre configuration Terraform devient illisible : res1, my_vm_2, des blocs mélangés dans main.tf, des noms de variables incohérents d’un module à l’autre. La review est douloureuse, les diffs incompréhensibles.

Analogie : votre codebase Terraform est comme une maison partagée. Sans règles communes de nettoyage et d’organisation, elle devient vite un chaos où personne ne retrouve ses affaires. Les conventions HashiCorp jouent ce rôle : ce sont les règles d’hygiène collective qui rendent le code lisible pour n’importe quel membre de l’équipe qui le rejoindra.

Les conventions HashiCorp et les pratiques adoptées par les équipes DevOps codifient les règles qui évitent ce scénario. Elles couvrent le nommage des ressources, la structure des fichiers .tf, l’ordre des blocs, et le formatage automatique avec terraform fmt. Ces règles s’appliquent quel que soit le provider utilisé — libvirt, AWS, Kubernetes. Les adopter dès le premier projet évite les refactors douloureux et rend le code immédiatement compréhensible par n’importe quel membre de l’équipe.

  • Structure des fichiers : main.tf, variables.tf, outputs.tf, versions.tf — ce qui va où
  • Nommage : conventions snake_case, préfixes de ressources, noms locaux
  • Formatage : terraform fmt et son usage en CI
  • Ordre des blocs dans une ressource : lifecycle, depends_on, count/for_each en tête
  • Commentaires : quand (et comment) documenter
module/
├── main.tf # Ressources principales
├── variables.tf # Déclarations de variables
├── outputs.tf # Déclarations d'outputs
├── versions.tf # required_providers + required_version
└── README.md # Documentation du module

Pour les modules complexes, découper main.tf par domaine :

module/
├── versions.tf
├── variables.tf
├── outputs.tf
├── network.tf # Ressources réseau
├── compute.tf # VMs, instances
├── storage.tf # Volumes, buckets
└── iam.tf # Rôles, permissions
  • terraform.tfvars dans les modules réutilisables (c’est au projet appelant de fournir les valeurs)
  • multiplier les fichiers trop tôt dans un module minuscule sans gain réel de lisibilité

locals.tf et providers.tf sont des conventions acceptables dès qu’elles clarifient la structure. Sur un petit projet, main.tf peut suffire ; sur un projet plus large, séparer locals, providers ou les domaines fonctionnels est souvent plus lisible.

resource "aws_instance" "web_server" {}
# TYPE NOM
  • Type : imposé par le provider
  • Nom : snake_case, descriptif, sans répéter le type
# ✅ Bon
resource "aws_instance" "web" {}
resource "libvirt_volume" "base_disk" {}
resource "aws_security_group" "allow_http" {}
# ❌ Mauvais
resource "aws_instance" "aws_instance" {} # répétition inutile
resource "libvirt_volume" "LibvirtVolume" {} # PascalCase
resource "aws_security_group" "sg1" {} # non descriptif

Si le module ne crée qu’une seule ressource d’un type, appeler le nom this :

resource "aws_vpc" "this" {
cidr_block = var.cidr_block
}

Pour un groupe logique fonctionnel, utiliser des noms descriptifs :

resource "aws_subnet" "public" {}
resource "aws_subnet" "private" {}
resource "aws_subnet" "database" {}
variable "instance_type" {} # ✅ snake_case
variable "instanceType" {} # ❌ camelCase
variable "InstanceType" {} # ❌ PascalCase
  • Noms clairs et descriptifs — pas d’abréviations cryptiques
  • Toujours avec description et type
# ✅ Complet
variable "vm_memory_mib" {
type = number
description = "Mémoire allouée à la VM en MiB."
default = 512
validation {
condition = var.vm_memory_mib >= 256
error_message = "La mémoire doit être au moins 256 MiB."
}
}
# ❌ Incomplet
variable "mem" {
default = 512
}

Même règle que les variables — snake_case, descriptif.

Pour les modules réutilisables, préfixer par le type de ressource quand c’est ambigu :

output "instance_id" {} # ✅
output "instance_public_ip" {} # ✅
output "id" {} # ❌ ambigu dans un module

Toujours utiliser terraform fmt avant de commiter. Cela normalise automatiquement l’indentation et l’alignement des =.

Fenêtre de terminal
# Formater récursivement
terraform fmt -recursive
# Vérifier sans appliquer (pour CI)
terraform fmt -check -recursive
# Alignement des = dans un bloc (appliqué par terraform fmt)
resource "libvirt_domain" "vm" {
name = "web"
memory = 512
memory_unit = "MiB"
vcpu = 1
}
# Pas d'espace dans les accolades de maps inline
os = { type = "hvm", type_arch = "x86_64" } # ✅
os = {type = "hvm", type_arch = "x86_64"} # ❌
# Listes multi-lignes : virgule finale
default = [
"dev",
"staging",
"prod", # ← virgule finale : diffs git plus propres
]

Ordre recommandé par HashiCorp :

resource "type" "nom" {
# 1. Meta-arguments (count, for_each, provider)
for_each = var.vms
# 2. Attributs requis / identifiants
name = "vm-${each.key}"
# 3. Attributs optionnels
memory = each.value.memory
memory_unit = "MiB"
# 4. Blocs imbriqués
devices = { ... }
# 5. lifecycle (toujours dernier)
lifecycle {
ignore_changes = [memory]
}
}

Ordre conseillé dans main.tf :

  1. data sources (lecture de l’infrastructure existante)
  2. locals (valeurs dérivées)
  3. resource (de la plus dépendante à la moins dépendante)
variable "db_password" {
type = string
sensitive = true # ← masqué dans les outputs et logs
}

Ne jamais hardcoder un mot de passe ou une clé dans le code HCL. Utiliser des variables sensitive = true et les injecter via l’environnement :

Fenêtre de terminal
export TF_VAR_db_password="mon_mot_de_passe"
terraform apply
# Commentaire sur une seule ligne — préféré en HCL
/*
Commentaire multi-lignes
pour des blocs entiers
*/

Commenter le pourquoi, pas le quoi :

# ❌ Inutile
# Crée un réseau NAT
resource "libvirt_network" "lab" { ... }
# ✅ Utile
# Réseau isolé du trafic externe — les VMs sortent via NAT
# mais ne sont pas accessibles depuis le LAN de lab.
resource "libvirt_network" "lab" { ... }
projet/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
└── modules/
└── vm/
├── main.tf
├── variables.tf
└── outputs.tf
module "web" {
source = "./modules/vm"
vm_name = "web"
memory = 512
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # ← toujours fixer la version
}
✅ versions.tf avec required_version et required_providers
✅ terraform fmt -check passe en CI
✅ terraform validate passe
✅ Pas de credentials dans le code (sensitive = true ou env vars)
✅ README.md avec les inputs/outputs documentés
✅ Variables avec description et type
✅ Un fichier par domaine fonctionnel (network.tf, compute.tf...)
✅ Nommage snake_case cohérent
  1. terraform fmt -recursive : formater avant chaque commit
  2. snake_case partout — ressources, variables, outputs
  3. Nom this pour la ressource unique d’un module
  4. Variables avec type, description, optionnellement validation
  5. sensitive = true pour les secrets — jamais de valeurs en dur dans le code
  6. Structure fichiers : versions.tf + variables.tf + outputs.tf + domaines fonctionnels

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn