Terraform : les bonnes pratiques pour structurer, sécuriser et maintenir un projet
Guide complet et sourcé des bonnes pratiques Terraform : structure de projet, gestion du state, versioning, secrets, modules, qualité de code, tests et CI/CD. Chaque recommandation est étayée par la documentation officielle HashiCorp, Google Cloud, AWS et Gruntwork.
Dans les articles précédents sur cloud-init et le setup d’une VM Scaleway, on a utilisé Terraform pour créer de l’infrastructure. Le code fonctionnait, mais on a pris quelques raccourcis : un seul répertoire, un terraform.tfvars avec des secrets en clair, pas de remote state, pas de modules.
Ça marche pour un projet personnel. Ça ne passe pas à l’échelle.
Cet article compile les bonnes pratiques Terraform issues de sources officielles et reconnues : le style guide HashiCorp, les best practices Google Cloud, les guides AWS, et les recommandations de Gruntwork (les créateurs de Terragrunt et Terratest). On couvre la structure du projet, le state, le versioning, les secrets, les modules, la qualité de code, les tests, et le CI/CD.
1. Structure du projet
1.1 Les fichiers de base
Le style guide HashiCorp recommande cette organisation :
| Fichier | Contenu |
|---|---|
main.tf | Ressources et data sources |
variables.tf | Déclarations de variables (par ordre alphabétique) |
outputs.tf | Déclarations d’outputs (par ordre alphabétique) |
locals.tf | Valeurs locales |
providers.tf | Configuration des providers |
terraform.tf | Bloc terraform (versions requises) |
backend.tf | Configuration du backend |
Pour un petit projet, tout peut rester dans main.tf. Dès que la configuration grossit, on découpe par responsabilité logique — network.tf, storage.tf, compute.tf — avec un nommage « immediately clear to maintainers ». Source : Style Guide
C’est ce qu’on a fait dans l’article Scaleway : ssh.tf, network.tf, storage.tf, instance.tf, cloudinit.tf, outputs.tf, variables.tf, versions.tf. Chaque fichier correspond à une responsabilité claire.
Google Cloud ajoute quelques conventions complémentaires :
- Les scripts appelés par Terraform vont dans un répertoire
scripts/ - Les fichiers traités par
templatefile()vont danstemplates/avec l’extension.tftpl - Les fichiers statiques référencés mais non exécutés vont dans
files/ - La documentation supplémentaire va dans
docs/
Source : Google Cloud — General style and structure
1.2 Structure multi-environnements
Quand on a plusieurs environnements (dev, staging, prod), deux approches existent :
Approche par répertoires (recommandée par Google Cloud et Gruntwork) :
project/
modules/
app/
main.tf
variables.tf
outputs.tf
database/
environments/
dev/
main.tf
backend.tf
terraform.tfvars
staging/
main.tf
backend.tf
terraform.tfvars
prod/
main.tf
backend.tf
terraform.tfvars
Chaque environnement a son propre state, ses propres credentials, et sa propre configuration. Une erreur en dev ne peut pas casser la prod. C’est l’isolation par le filesystem. Source : Google Cloud — Root modules
Gruntwork pousse cette logique plus loin en isolant aussi par composant :
prod/
vpc/
main.tf
services/
webserver-cluster/
main.tf
data-stores/
mysql/
main.tf
staging/
vpc/
services/
data-stores/
global/
iam/
s3/
Chaque composant a son propre state file. Le blast radius d’une erreur est limité à un seul composant dans un seul environnement. Source : Gruntwork — How to manage Terraform state
Approche par workspaces (voir section 8.1 pour les détails et limitations) : un seul répertoire, plusieurs workspaces Terraform. Plus simple, mais moins de garanties d’isolation.
1.3 Taille du state
Google Cloud recommande de ne pas dépasser 100 ressources par state (idéalement quelques dizaines) pour éviter des terraform plan trop lents. HashiCorp suggère un maximum de 5 000 objets par workspace. Au-delà, chaque plan nécessite de rafraîchir l’état de toutes les ressources via les APIs du provider — ça peut prendre 10 à 15 minutes. Source : Google Cloud — Root modules, Source : HashiCorp — Workspace size
1.4 Ce qu’on commit, ce qu’on ignore
Le style guide est explicite :
Toujours committer :
- Tous les fichiers
.tf .terraform.lock.hcl(le lock file des dépendances).gitignoreREADME.md
Ne jamais committer :
terraform.tfstateetterraform.tfstate.*.terraform.tfstate.lock.info- Le répertoire
.terraform/ - Les fichiers de plan (créés avec
-out) - Les fichiers
.tfvarscontenant des secrets
Le .gitignore recommandé par GitHub pour Terraform ignore par défaut tous les *.tfvars. Source : HashiCorp — Sensitive variables
2. Gestion du state
Le state est le cœur de Terraform. C’est le fichier qui fait le lien entre votre configuration HCL et les ressources réelles chez le provider. Mal géré, il devient un problème de sécurité et de collaboration.
2.1 Remote state
Par défaut, le state est stocké localement dans terraform.tfstate. C’est l’erreur n°1 des débutants selon Pipetail : pas de partage entre membres de l’équipe, pas de verrouillage, pas de chiffrement, pas de backup.
La solution : un backend distant. Le bloc backend se configure dans le bloc terraform :
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}
Points importants :
- Un seul backend par configuration — « A configuration can only provide one backend block »
- Le bloc
backendne supporte pas les variables, les locals, ni les data sources. C’est une limitation volontaire (le backend doit être résolu avant toute évaluation). Pour paramétrer le backend, on utilise la configuration partielle via-backend-config=PATHau moment duterraform init - HashiCorp recommande de passer les credentials du backend via des variables d’environnement
Convention de nommage pour les fichiers de config partielle : *.backendname.tfbackend (ex. config.s3.tfbackend).
Source : Backend block, Source : Backend S3
2.2 Verrouillage du state
Le verrouillage empêche deux terraform apply concurrents de corrompre le state :
« If supported by your backend, Terraform will lock your state for all operations that could write state. This prevents others from acquiring the lock and potentially corrupting your state. » Source : State locking
Si le verrouillage échoue, Terraform arrête l’exécution plutôt que de continuer sans verrou. Le flag -lock=false peut le désactiver, mais HashiCorp le déconseille.
Pour le backend S3, deux mécanismes existent :
- Verrouillage natif S3 (recommandé) :
use_lockfile = true— utilise un fichier.tflockdans le bucket - DynamoDB (déprécié) :
dynamodb_tableavec une partition keyLockID(String)
2.3 Chiffrement du state
Le state contient des valeurs sensibles en clair (mots de passe, tokens, clés API). Le chiffrement au repos est indispensable.
Pour S3, trois options :
| Mode | Configuration | Détail |
|---|---|---|
| SSE-S3 | encrypt = true | Chiffrement par défaut avec les clés gérées par S3 |
| SSE-KMS | kms_key_id = "arn:..." | Chiffrement avec une clé KMS dédiée (nécessite kms:Encrypt, kms:Decrypt, kms:GenerateDataKey) |
| SSE-C | sse_customer_key | Clé fournie par le client (base64, 256 bits) |
HashiCorp recommande aussi d’activer le versioning du bucket S3 pour pouvoir récupérer un state en cas d’erreur. Source : Backend S3
Google Cloud recommande de restreindre l’accès au bucket de state : « Make sure that only the build system and highly privileged administrators can access the bucket. » Source : Google Cloud — Security
2.4 terraform_remote_state : avec prudence
La data source terraform_remote_state permet de lire les outputs d’un autre state. C’est pratique pour partager des informations entre composants (l’ID du VPC, l’ARN d’un rôle IAM, etc.).
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "eu-west-1"
}
}
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id
}
Mais HashiCorp met en garde : « Any user or server which has enough access to read the root module output values will also always have access to the full state snapshot data. » Autrement dit, quiconque peut lire un output peut potentiellement lire tout le state.
L’alternative recommandée : partager les données via un store dédié (SSM Parameter Store, Consul, un config map Kubernetes) plutôt que via le state directement. Source : terraform_remote_state
3. Versioning et gestion des providers
3.1 Contraintes de version
Terraform utilise un système de contraintes de version flexible :
| Opérateur | Signification | Exemple |
|---|---|---|
= (ou rien) | Version exacte | = 1.0.0 |
!= | Exclut une version | != 1.0.0 |
>, >=, <, <= | Comparaison | >= 1.2.0, < 2.0.0 |
~> | Pessimiste — seul le dernier composant peut incrémenter | ~> 1.0.4 autorise 1.0.5 à 1.0.x, bloque 1.1.0 |
L’opérateur ~> est le plus utilisé. ~> 1.1 autorise 1.2, 1.10, mais bloque 2.0. ~> 1.1.0 autorise 1.1.1 à 1.1.x, mais bloque 1.2.0.
Bonnes pratiques selon le contexte :
- Root modules : utiliser
~>pour borner les versions des providers (ex.~> 5.34.0) - Modules réutilisables : utiliser uniquement un minimum (
>= 0.12.0), sans borne supérieure — « Do not use~>for modules you intend to reuse across many configurations » car cela forcerait tous les consommateurs à upgrader simultanément
3.2 Pinning des versions
# terraform.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
scaleway = {
source = "scaleway/scaleway"
version = "~> 2.41.0"
}
cloudinit = {
source = "hashicorp/cloudinit"
version = "~> 2.3"
}
}
}
On retrouve cette structure dans notre configuration Scaleway. Trois éléments sont pinnés :
- La version de Terraform (
required_version) — empêche quelqu’un d’utiliser une version incompatible - Les providers (
required_providers) — fixe la source et la version de chaque provider - Les modules (dans les appels de module) — fixe la version du module consommé
Ne pas pinner ses versions est l’un des 10 erreurs les plus courantes identifiées par Pipetail : un terraform init peut silencieusement télécharger une version majeure incompatible.
3.3 Le fichier .terraform.lock.hcl
Ce fichier trace les versions exactes des providers sélectionnés par Terraform. Il fonctionne sur un modèle de « trust on first use » : la première installation enregistre les checksums, les suivantes les vérifient.
« You should include this file in your version control repository so that you can discuss potential changes to your external dependencies via code review. » Source : Dependency Lock File
Règles clés :
- Toujours committer
.terraform.lock.hcl - Ne jamais l’éditer manuellement — utiliser
terraform init -upgrade - Pour les équipes multi-plateformes (macOS + Linux), utiliser
terraform providers lockpour pré-calculer les checksums de chaque OS
4. Variables et conventions de nommage
4.1 Conventions de nommage
Le style guide HashiCorp est clair :
« Use nouns for resource names and do not include the resource type in the name. Use underscores to separate multiple words in names. »
# Mauvais
resource "aws_instance" "webAPI-aws-instance" { ... }
# Bon
resource "aws_instance" "web_api" { ... }
Règles complémentaires de Google Cloud :
- Pour une ressource unique de son type, utiliser le nom
main(ex.resource "google_compute_instance" "main") - Garder les noms de ressources au singulier
- Nommer les variables numériques avec leur unité :
disk_size_gb,memory_size_mb - Donner aux booléens des noms positifs :
enable_external_accessplutôt quedisable_external_access
4.2 Déclaration des variables
L’ordre recommandé des attributs dans un bloc variable :
variable "instance_type" {
description = "Type commercial de l'instance Scaleway"
type = string
default = "DEV1-S"
validation {
condition = contains(["DEV1-S", "DEV1-M", "DEV1-L"], var.instance_type)
error_message = "Le type d'instance doit être DEV1-S, DEV1-M ou DEV1-L."
}
}
Ordre : description → type → default → sensitive → validation. Source : Style Guide
Quelques règles importantes :
- Toujours inclure
typeetdescription— même quand ça paraît évident. La description est utilisée pour la génération automatique de documentation - Définir des
defaultraisonnables pour les variables optionnelles sensitive = truepour les mots de passe et clés privées (voir section 5)- Pas de
defaultpour les secrets — forcer l’utilisateur à fournir la valeur explicitement nullable = falsepour les variables qui ne doivent jamais êtrenull
4.3 Les blocs de validation
Les blocs validation sont exécutés immédiatement, avant la génération du plan. C’est une première ligne de défense contre les erreurs de configuration :
variable "environment" {
type = string
description = "Environnement de déploiement"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être dev, staging ou prod."
}
}
variable "allowed_ssh_cidrs" {
type = list(string)
description = "CIDRs autorisés pour SSH"
validation {
condition = length(var.allowed_ssh_cidrs) > 0
error_message = "Au moins un CIDR doit être autorisé pour SSH."
}
}
Pour des validations plus avancées (au niveau des ressources), Terraform offre aussi precondition et postcondition dans les blocs lifecycle :
data "aws_vpc" "app" {
id = var.vpc_id
lifecycle {
postcondition {
condition = self.enable_dns_support == true
error_message = "Le VPC doit avoir le support DNS activé."
}
}
}
4.4 locals vs variables
variable | local | |
|---|---|---|
| Visibilité | API externe du module | Interne au module uniquement |
| Source | L’appelant fournit la valeur | Calculé dans le module |
| Usage type | Paramètres configurables | Expressions réutilisées, valeurs dérivées |
Les locals sont utiles pour factoriser des expressions complexes et leur donner un nom lisible :
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
Mais le style guide met en garde : « locals can obscure where values originate, potentially reducing readability ». À utiliser avec modération. Source : Local Values
4.5 Ordre de précédence des variables
Quand une même variable est définie à plusieurs endroits, Terraform applique cet ordre (du plus faible au plus fort) :
- Variables d’environnement (
TF_VAR_*) terraform.tfvarsterraform.tfvars.json- Fichiers
*.auto.tfvars(par ordre lexicographique) - Flags
-varet-var-fileen ligne de commande
5. Gestion des secrets
C’est le sujet le plus critique et le plus souvent mal géré. HashiCorp identifie la gestion des secrets comme l’une de ses 5 pratiques de sécurité fondamentales.
5.1 Le problème : les secrets dans le state
Terraform stocke tout dans le state, y compris les valeurs sensibles, en clair. Ce n’est pas un bug, c’est le fonctionnement normal : Terraform a besoin de connaître l’état complet des ressources pour calculer les différences.
« Terraform stores values with the sensitive argument in both state and plan files, and anyone who can access those files can access your sensitive values. » Source : HashiCorp — Sensitive variables
Un simple grep "password" terraform.tfstate suffit à retrouver les credentials. Google Cloud avertit que de nombreuses ressources Terraform stockent des valeurs sensibles en clair dans le state, notamment tls_private_key et google_service_account_key. Source : Google Cloud — Security
5.2 sensitive = true : nécessaire mais insuffisant
Déclarer une variable comme sensitive masque sa valeur dans les outputs CLI (elle apparaît comme (sensitive value)), mais ne protège pas le state :
variable "ghcr_pat" {
description = "Personal Access Token GitHub (scope: read:packages)"
type = string
sensitive = true # masqué dans les logs, mais en clair dans le state
}
C’est ce qu’on a utilisé dans notre configuration Scaleway. Le PAT GitHub est marqué sensitive, donc il n’apparaît pas dans le terraform plan. Mais il reste en clair dans le state et dans le user-data de l’instance.
5.3 Les valeurs éphémères (Terraform 1.10+)
Terraform 1.10 (novembre 2024) a introduit les valeurs éphémères : « available at runtime, but Terraform omits them from state and plan files entirely. » C’est une avancée majeure.
variable "db_password" {
type = string
ephemeral = true # jamais stocké dans le state ni le plan
}
Terraform 1.11 a ajouté les write-only arguments : des attributs de ressource qui ne peuvent qu’être écrits, jamais lus, et qui ne sont pas non plus stockés dans le state.
Source : Terraform 1.10 ephemeral values, Source : Terraform 1.11 write-only arguments
5.4 Les variables d’environnement TF_VAR_*
C’est la méthode la plus sûre pour passer des secrets : les valeurs restent en mémoire et ne touchent jamais le disque.
export TF_VAR_ghcr_pat="ghp_xxxxxxxxxxxx"
export TF_VAR_ssh_public_key="ssh-ed25519 AAAA..."
terraform plan
Les variables TF_VAR_* sont au bas de la chaîne de précédence : un .tfvars ou un -var les surcharge. C’est la méthode privilégiée en CI/CD car elle s’intègre naturellement avec les systèmes de secrets (GitHub Secrets, Vault, etc.). Source : Spacelift — Terraform .tfvars
5.5 Les fichiers .tfvars
Pour le développement local, un fichier terraform.tfvars (ou secrets.tfvars) est pratique :
# terraform.tfvars — NE PAS COMMITTER
ghcr_pat = "ghp_xxxxxxxxxxxx"
ssh_public_key = "ssh-ed25519 AAAA..."
ssh_public_key_deploy = "ssh-ed25519 AAAA..."
Rappel : le .gitignore recommandé par GitHub ignore tous les *.tfvars par défaut. Si vous utilisez un fichier terraform.tfvars.example (avec des valeurs factices) pour documenter les variables attendues, nommez-le avec l’extension .example, pas .tfvars.
5.6 Intégration avec un gestionnaire de secrets
La meilleure approche est de récupérer les secrets au runtime depuis un gestionnaire dédié, plutôt que de les passer via des variables.
HashiCorp Vault :
data "vault_aws_access_credentials" "creds" {
backend = "aws"
role = "deploy"
type = "sts"
}
provider "aws" {
access_key = data.vault_aws_access_credentials.creds.access_key
secret_key = data.vault_aws_access_credentials.creds.secret_key
token = data.vault_aws_access_credentials.creds.security_token
}
Le provider Vault génère des credentials temporaires (TTL de 20 minutes par défaut) via le secrets engine AWS. Les credentials sont automatiquement révoqués après expiration. Attention : « Terraform has no mechanism to redact or protect secrets returned via data sources, so secrets read via this provider will be persisted into the Terraform state. » Source : Vault Provider
AWS Secrets Manager :
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "main" {
password = jsondecode(
data.aws_secretsmanager_secret_version.db_password.secret_string
)["password"]
}
Source : AWS — Using Secrets Manager and Terraform
Dans notre article Scaleway, on a montré une approche similaire avec Scaleway Secret Manager : le PAT GitHub est stocké dans un secret, et l’instance le récupère au runtime via l’API.
5.7 La défense en profondeur
Aucune mesure seule ne suffit. La stratégie complète combine :
sensitive = truesur les variables et outputs → masque les valeurs dans la CLI- Variables d’environnement (
TF_VAR_*) → les secrets ne touchent pas le disque - Gestionnaire de secrets (Vault, AWS SM, Scaleway SM) → les secrets ne sont pas dans le code
- State chiffré en remote → les secrets en clair dans le state sont protégés au repos
- Accès restreint au state → seul le pipeline CI/CD et les admins y accèdent
- Valeurs éphémères (Terraform 1.10+) → les secrets ne sont pas du tout dans le state
Source : HashiCorp — 5 foundational practices
6. Modules
6.1 Quand créer un module ?
HashiCorp recommande de créer des modules qui « raise the level of abstraction by describing a new concept in your architecture ». Un bon test : si le nom du module est identique au type de ressource principale qu’il contient, il manque probablement d’abstraction. Utilisez la ressource directement. Source : Creating Modules
Gruntwork va plus loin : « Large modules should be considered harmful » — les gros modules sont lents, difficiles à tester, à revoir en code review, et fragiles. Source : Gruntwork — Reusable infrastructure with Terraform modules
Trois principes de scoping pour concevoir un module (recommandation officielle HashiCorp) :
- Encapsulation : regrouper l’infrastructure qui est toujours déployée ensemble
- Privilèges : respecter les frontières de permissions — ne pas mélanger des ressources nécessitant des niveaux d’accès différents
- Volatilité : séparer l’infrastructure à longue durée de vie (bases de données) de celle à courte durée de vie (serveurs d’application)
Source : Module creation — recommended pattern
6.2 Structure d’un module
La structure standard recommandée :
module-name/
README.md # Documentation obligatoire
main.tf # Point d'entrée principal
variables.tf # Toutes les variables
outputs.tf # Tous les outputs
LICENSE # Recommandé pour les modules publics
modules/ # Sous-modules
nested-a/
README.md # Un README = module utilisable en externe
main.tf
variables.tf
outputs.tf
nested-b/ # Pas de README = module interne uniquement
examples/ # Exemples d'utilisation
basic/
main.tf
complete/
main.tf
Les sous-modules sous modules/ avec un README.md sont considérés comme utilisables par des consommateurs externes. Ceux sans README sont internes. Les exemples dans examples/ doivent utiliser des sources externes (pas des chemins relatifs) pour refléter l’usage réel.
Gruntwork ajoute un fichier dependencies.tf optionnel pour lister les ressources qui doivent pré-exister. Source : Gruntwork — Terraform Style Guide
6.3 Sources de modules
Terraform supporte plusieurs types de sources :
# Registry (public ou privé)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
# Chemin local (doit commencer par ./ ou ../)
module "network" {
source = "./modules/network"
}
# Git (avec pinning via ref)
module "app" {
source = "git::https://github.com/org/modules.git//app?ref=v1.2.0"
}
Pour les modules Git, le paramètre ref accepte tout ce que git checkout accepte : tag, branche, SHA-1. Source : Module Sources
6.4 Bonnes pratiques de conception
- Viser 80% des use cases avec les inputs par défaut, pas les cas limites
- Maximiser les outputs même sans usage immédiat — les consommateurs chaîneront les outputs d’un module vers les inputs d’un autre
- Les inputs obligatoires (sans
default) représentent des choix de configuration délibérés - Les inputs optionnels ont des defaults sensés, adaptés à la majorité des cas
- Garder l’arbre de modules plat : « Keep the module tree relatively flat and use module composition as an alternative to a deeply-nested tree of modules. » Ne pas dépasser deux niveaux d’imbrication
Convention de nommage pour publier sur le registry : terraform-<PROVIDER>-<NAME> (ex. terraform-aws-vpc). Source : Modules overview
7. Qualité de code et outillage
7.1 terraform fmt
Le formateur intégré applique la mise en forme canonique du HCL : indentation cohérente, alignement des =, suppression des whitespace superflus.
terraform fmt # formate le répertoire courant
terraform fmt -check # vérifie sans modifier (utile en CI)
terraform fmt -diff # affiche les différences
terraform fmt -recursive # formate les sous-répertoires
Le style guide recommande de l’exécuter avant chaque commit. Source : Style Guide
7.2 terraform validate
Vérifie que la configuration est syntaxiquement valide et internement cohérente. Ne contacte pas le provider (pas de vérification API) et n’évalue pas le state existant.
terraform init # requis avant validate
terraform validate
7.3 TFLint
Un linter open source qui va plus loin que terraform validate. Il détecte :
- Les types d’instance invalides (ex. un type AWS qui n’existe pas)
- La syntaxe dépréciée
- Les violations de bonnes pratiques
- Les problèmes spécifiques aux providers (via des plugins)
Configuration .tflint.hcl :
plugin "aws" {
enabled = true
region = "eu-west-1"
}
rule "terraform_required_version" {
enabled = true
}
Plugins disponibles : AWS (tflint-ruleset-aws), Azure (tflint-ruleset-azurerm), GCP (tflint-ruleset-google).
tflint --init # installe les plugins
tflint # exécute le linting
7.4 Pre-commit hooks
Le projet pre-commit-terraform d’Anton Babenko fournit une collection de hooks Git pour Terraform. Configuration .pre-commit-config.yaml :
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.1
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_docs
- id: terraform_trivy # scan de sécurité (remplace tfsec)
- id: terraform_checkov # analyse statique de sécurité
pre-commit install # installe les hooks
pre-commit run -a # exécute tous les hooks sur tous les fichiers
Les hooks disponibles incluent aussi infracost_breakdown (estimation des coûts) et terragrunt_fmt.
7.5 terraform-docs
Génère automatiquement la documentation d’un module à partir des variables, outputs, providers et ressources.
terraform-docs markdown table ./modules/vpc
Configuration .terraform-docs.yml :
formatter: "markdown table"
output:
file: "README.md"
mode: "inject" # injecte entre les marqueurs
sections:
show:
- header
- inputs
- outputs
- providers
- resources
Le mode inject insère la documentation entre des marqueurs dans le README :
<!-- BEGIN_TF_DOCS -->
(contenu auto-généré)
<!-- END_TF_DOCS -->
Combiné avec les pre-commit hooks, la documentation reste synchronisée avec le code automatiquement. Source : terraform-docs
7.6 Scan de sécurité
Checkov : scanner open source avec plus de 1 000 politiques de sécurité intégrées couvrant CIS, NIST, PCI-DSS, HIPAA, SOC2 et GDPR. Il analyse le code HCL (pas le plan) et détecte les stockages non chiffrés, les permissions excessives, les security groups ouverts, etc.
checkov -d .
checkov --framework terraform --check CKV_AWS_18 # check spécifique
tfsec (maintenant intégré dans Trivy) : analyse statique spécifiquement conçue pour Terraform. tfsec n’est pas officiellement déprécié mais son développement est intégré dans Trivy (Aqua Security). Source : tfsec GitHub
La pyramide de tests recommandée :
┌─────────────────┐
│ End-to-end │ ← Plus lent, plus cher
├─────────────────┤
│ Intégration │ terraform test (apply)
├─────────────────┤
│ Plan-based │ terraform test (plan)
├─────────────────┤
│ Analyse statique│ ← Plus rapide, moins cher
└─────────────────┘
validate, tflint,
checkov, trivy
8. Patterns avancés
8.1 Workspaces : quand les utiliser, quand les éviter
Un workspace Terraform est une instance séparée du state dans le même répertoire de travail. Tous les workspaces partagent le même code et le même backend.
terraform workspace new staging
terraform workspace select staging
terraform workspace list
Le nom du workspace est accessible via terraform.workspace :
resource "aws_instance" "web" {
count = terraform.workspace == "prod" ? 3 : 1
}
Les workspaces sont adaptés pour :
- Les environnements éphémères (feature branches, tests, PR previews)
- Les petits projets où les environnements sont quasi identiques
- Le prototypage local
Les workspaces ne sont PAS adaptés pour (avertissement officiel HashiCorp) :
« Workspaces are not appropriate for system decomposition or deployments requiring separate credentials and access controls. » Source : Workspaces
Limitations concrètes :
- Pas d’isolation des credentials : tous les workspaces partagent le même backend et la même authentification
- Versions verrouillées : une mise à jour de module affecte tous les workspaces simultanément
- Risque d’erreur : un
terraform applydans le mauvais workspace peut être désastreux
Pour des environnements permanents (dev/staging/prod), préférer l’isolation par répertoires (section 1.2).
8.2 Terragrunt : DRY multi-environnements
Terragrunt (de Gruntwork) est un wrapper autour de Terraform qui résout plusieurs limitations :
- Le
backendne supporte pas les variables : Terragrunt permet de définir la configuration du backend une seule fois et de l’hériter - La duplication de code entre environnements : Terragrunt référence des modules partagés avec des inputs spécifiques à l’environnement
- Les dépendances entre modules : Terragrunt modélise un graphe de dépendances et exécute les modules dans le bon ordre
L’idée : séparer en deux repositories — un pour les modules (le code réutilisable) et un pour la configuration live (les valeurs par environnement) :
# live/prod/app/terragrunt.hcl
terraform {
source = "git::git@github.com:org/modules.git//app?ref=v1.0.0"
}
inputs = {
instance_count = 10
instance_type = "m8g.large"
}
# live/staging/app/terragrunt.hcl
terraform {
source = "git::git@github.com:org/modules.git//app?ref=v1.1.0"
}
inputs = {
instance_count = 2
instance_type = "t4g.micro"
}
Chaque environnement peut utiliser une version différente du module (via ?ref=). La promotion est un changement de tag dans le fichier terragrunt.hcl. Le rollback est un retour au tag précédent.
| Aspect | Workspaces | Terragrunt |
|---|---|---|
| Isolation des credentials | Non | Oui (par environnement) |
| Versioning indépendant des modules | Non | Oui (via ?ref=) |
| Gestion des dépendances | Manuelle | Automatique |
| Blast radius | Partagé | Isolé par composant |
| Courbe d’apprentissage | Faible | Outil supplémentaire |
Quand ne pas utiliser Terragrunt : petit projet avec un seul environnement, équipe pas prête pour un outil supplémentaire, ou déjà sur HCP Terraform qui fournit sa propre orchestration.
Source : Terragrunt — Keep your Terraform code DRY
8.3 Import, refactoring et migration
import blocks (Terraform 1.5+) : importation déclarative, versionnée, compatible CI/CD :
import {
to = aws_s3_bucket.this
id = "my-existing-bucket"
}
resource "aws_s3_bucket" "this" {
bucket = "my-existing-bucket"
}
Avec terraform plan -generate-config-out=generated.tf, Terraform génère automatiquement le bloc resource correspondant. Source : import block
moved blocks (Terraform 1.1+) : refactoring sans destruction :
moved {
from = aws_instance.web
to = module.web.aws_instance.this
}
Permet de renommer des ressources, de les déplacer dans un module, ou de passer de count à for_each sans recréer l’infrastructure. HashiCorp recommande de conserver les blocs moved pendant au moins un cycle de release. Source : moved block
removed blocks (Terraform 1.7+) : arrêter de gérer une ressource sans la détruire :
removed {
from = aws_instance.legacy
lifecycle {
destroy = false # la ressource continue d'exister
}
}
Alternative déclarative et versionnée à terraform state rm. Source : removed block
9. Tests
9.1 terraform test (natif, Terraform 1.6+)
Terraform 1.6 a introduit un framework de test natif utilisant des fichiers .tftest.hcl :
# tests/instance.tftest.hcl
variables {
instance_type = "DEV1-S"
}
run "valid_instance_type" {
command = plan # test unitaire : pas de ressource créée
assert {
condition = scaleway_instance_server.main.type == "DEV1-S"
error_message = "Le type d'instance ne correspond pas"
}
}
run "create_and_verify" {
command = apply # test d'intégration : crée de vraies ressources
assert {
condition = scaleway_instance_server.main.id != ""
error_message = "L'instance n'a pas été créée"
}
}
Deux modes :
command = plan: test unitaire rapide, aucune ressource crééecommand = apply: test d’intégration, crée de vraies ressources (et les détruit après)
terraform test # exécute tous les tests
terraform test -filter=tests/s3 # filtre par fichier
9.2 Terratest
Terratest (Gruntwork) est une bibliothèque Go pour écrire des tests d’infrastructure. Contrairement à terraform test, Terratest interroge les APIs du provider pour vérifier l’état réel des ressources, pas seulement le state Terraform.
func TestTerraformExample(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/basic",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
output := terraform.Output(t, terraformOptions, "instance_id")
assert.NotEmpty(t, output)
}
Plus de confiance dans les résultats, mais tests plus lents et nécessite Go. Source : Terratest
10. Sécurité avancée
10.1 Principe du moindre privilège
Le compte de service utilisé par Terraform pour interagir avec le provider doit avoir le minimum de permissions nécessaires :
- AWS : préférer les IAM Roles aux IAM Users pour la rotation automatique des credentials. Utiliser
assume_roledans la configuration du provider. Utiliser IAM Access Analyzer pour identifier et supprimer les permissions inutilisées. Source : AWS — Security best practices - Google Cloud : « Using the default service account is not recommended, because by default the default service account is highly privileged. » Créer des comptes de service dédiés avec des permissions limitées. Utiliser l’impersonation plutôt que des clés de service account. Source : Google Cloud — Security
10.2 OIDC : authentification sans secrets statiques
OpenID Connect (OIDC) élimine les credentials statiques en CI/CD. GitHub Actions s’authentifie directement auprès du cloud provider avec des tokens éphémères :
# .github/workflows/terraform.yml
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::111122223333:role/terraform-role
aws-region: eu-west-1
Le workflow crée un JWT unique, l’échange contre des credentials temporaires (durée par défaut : 1 heure), et les utilise pour le provider AWS. Aucun secret stocké dans GitHub Secrets ni dans le code. Source : GitHub — OIDC for AWS
10.3 Policy as Code
Sentinel (HashiCorp, intégré à Terraform Cloud/Enterprise) : évalue les policies après terraform plan et avant terraform apply. Trois niveaux d’enforcement :
- Advisory : warning seulement
- Soft-mandatory : bloquant sauf exception configurée
- Hard-mandatory : bloquant sans exception possible
Open Policy Agent (OPA) : policies écrites en Rego, évaluées contre le plan Terraform converti en JSON :
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json
11. CI/CD
11.1 Le workflow standard
Le pattern recommandé pour Terraform en CI/CD :
- Pull request →
terraform fmt -check+terraform validate+terraform plan+ scan de sécurité (Checkov/Trivy) → le plan est posté en commentaire de la PR - Review → un humain vérifie le plan
- Merge →
terraform applyavec le plan sauvegardé en artefact (pour garantir que ce qui est appliqué est exactement ce qui a été revu)
Source : HashiCorp — Automate Terraform with GitHub Actions
11.2 HCP Terraform (ex-Terraform Cloud)
La plateforme SaaS de HashiCorp pour gérer Terraform en équipe. Elle fournit :
- Remote operations : Terraform s’exécute sur des VMs jetables dans l’infrastructure HashiCorp
- Dynamic provider credentials : basées sur OIDC, des credentials éphémères sont générées pour chaque run
- Workspace management : RBAC, triggers inter-workspaces, intégration VCS
- Sentinel : policy as code intégrée
- Cost estimation : estimation des coûts avant
apply
Source : HCP Terraform — Plans and Features
11.3 Atlantis
Atlantis est un outil open source d’automatisation des pull requests Terraform, auto-hébergé. Son workflow :
- Le développeur ouvre une PR avec des changements Terraform
- Atlantis exécute automatiquement
terraform planet poste le résultat en commentaire - L’équipe review et discute ; chaque push déclenche un nouveau plan
- Un reviewer autorisé tape
atlantis applydans un commentaire - Atlantis verrouille le workspace, exécute
terraform apply, commente le résultat - La PR est mergée, Atlantis libère le verrou
11.4 Détection de drift
Un workflow GitHub Actions planifié détecte le drift de configuration :
on:
schedule:
- cron: "0 8 * * 1-5" # tous les jours ouvrés à 8h
jobs:
drift-detection:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform plan -detailed-exitcode
# exit code 2 = des changements détectés (drift)
Si terraform plan retourne un exit code 2, des changements ont été faits en dehors de Terraform. On peut créer automatiquement une issue GitHub pour traçabilité. Source : DevOps Tooling — Terraform GitHub Actions Guide
12. Anti-patterns à éviter
Le monolithe de state (« Terralith »)
Un seul root module gérant toute l’infrastructure d’un projet. Conséquences : plans lents (10-15 min), blast radius maximal, un seul pipeline peut modifier le state à la fois, couplage fort entre composants sans rapport. Source : Masterpoint — The Terralith
Versions non pinnées
Ne pas pinner les providers ni la version de Terraform expose aux breaking changes silencieuses. Toujours utiliser required_version, required_providers, et version sur les appels de modules.
Secrets en clair
Stocker des mots de passe dans les .tf, les .tfvars commités, ou le state non chiffré. Voir section 5 pour les solutions.
Modules wrappers
Des modules qui exposent chaque attribut de la ressource sous-jacente comme variable. Ils n’ajoutent aucune abstraction et compliquent la maintenance. Si le nom du module est le nom du type de ressource, il n’a probablement pas lieu d’exister.
terraform.workspace comme logique métier
Utiliser terraform.workspace dans des conditionnels pour différencier les environnements crée des différences implicites, difficiles à tracer et à auditer. Préférer des variables explicites.
Pas de plan avant apply
Toujours exécuter terraform plan et le relire avant d’appliquer. En CI/CD, sauvegarder le plan comme artefact et l’appliquer — jamais de terraform apply sans plan préalable.
Source : Pipetail — 10 most common mistakes
Récapitulatif
| Pratique | Ce qu’il faut faire |
|---|---|
| Structure | Un fichier par responsabilité, un répertoire par environnement |
| State | Remote, chiffré, verrouillé, accès restreint |
| Versioning | Pinner Terraform, les providers et les modules. Committer le lock file |
| Secrets | sensitive = true + TF_VAR_* + gestionnaire de secrets + state chiffré |
| Modules | Créer quand il y a une vraie abstraction. Garder l’arbre plat |
| Qualité | fmt + validate + TFLint + pre-commit + terraform-docs |
| Tests | terraform test (plan + apply) pour les modules critiques |
| Sécurité | Moindre privilège, OIDC, scan statique, policy as code |
| CI/CD | Plan sur PR, review humaine, apply sur merge, détection de drift |
Sources principales
Documentation officielle HashiCorp
- Style Guide
- Standard Module Structure
- Creating Modules
- Module Sources
- Module Composition
- Module creation — recommended pattern
- Backend block
- Backend S3
- State locking
- terraform_remote_state
- Version Constraints
- Dependency Lock File
- Provider Requirements
- Input Variables
- Local Values
- Custom conditions
- Sensitive variables
- Manage sensitive data
- Workspaces
- Tests
- import block
- moved block
- removed block
- Automate Terraform with GitHub Actions
- HCP Terraform — Plans and Features
Blog HashiCorp
- Terraform security: 5 foundational practices
- Terraform 1.10 — ephemeral values
- Terraform 1.11 — write-only arguments
- Terraform 1.5 — config-driven import
- Dynamic Provider Credentials
Google Cloud
- Best practices — General style and structure
- Best practices — Root modules
- Best practices — Security
AWS
Gruntwork
- How to manage Terraform state
- How to create reusable infrastructure with Terraform modules
- Terraform Style Guide
- Terragrunt — Keep your Terraform code DRY
- Terratest
Outils
- Vault Provider
- TFLint
- pre-commit-terraform
- terraform-docs
- Checkov
- tfsec / Trivy
- Atlantis
- Sentinel
- Open Policy Agent — Terraform