Terraform est devenu l'outil de référence pour gérer l'Infrastructure as Code (IaC). Mais à mesure que vos projets grandissent, sans discipline ni conventions, votre code Terraform peut rapidement devenir un cauchemar de maintenance. Des fichiers monolithiques de plusieurs milliers de lignes, des états corrompus, des secrets en clair dans le code source... Les erreurs classiques sont nombreuses et leurs conséquences peuvent être désastreuses en production.
Dans cet article, nous allons explorer en profondeur les bonnes pratiques Terraform éprouvées par la communauté et les équipes DevOps expérimentées. De l'organisation du code à la sécurité, en passant par la CI/CD et la documentation, vous disposerez d'un guide complet pour construire une infrastructure fiable, maintenable et scalable.

Organisation du Code : La Structure de Répertoires
L'organisation de vos fichiers Terraform est la fondation de tout projet IaC réussi. Une structure claire permet à n'importe quel membre de l'équipe de comprendre rapidement le projet et d'y contribuer efficacement.
Structure recommandée par environnement
La séparation par environnement est la pratique la plus courante et la plus robuste :
infrastructure/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ └── database/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── README.md
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── global/
├── iam/
└── dns/
Convention de nommage des fichiers
Au sein de chaque répertoire, adoptez une convention de fichiers standard :
- main.tf : ressources principales et appels de modules
- variables.tf : déclaration de toutes les variables d'entrée
- outputs.tf : valeurs de sortie exposées
- providers.tf : configuration des providers
- backend.tf : configuration du backend pour le state
- versions.tf : contraintes de versions Terraform et providers
- locals.tf : valeurs locales calculées
- data.tf : sources de données (data sources)
Pour les projets plus complexes, vous pouvez découper main.tf en fichiers thématiques comme networking.tf, compute.tf, storage.tf, etc.
Conventions de Nommage
Des conventions de nommage cohérentes sont essentielles pour la lisibilité et la maintenabilité du code. Voici les règles à suivre systématiquement.
Nommage des ressources Terraform
# Bonne pratique : snake_case, noms descriptifs
resource "aws_instance" "web_server" {
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = "${var.project}-${var.environment}-web-server"
}
}
# À éviter : noms génériques ou ambigus
resource "aws_instance" "instance1" {
# ...
}
Nommage des variables
# Bonne pratique : noms explicites avec description et type
variable "database_instance_class" {
description = "Classe d'instance RDS pour la base de données principale"
type = string
default = "db.t3.medium"
validation {
condition = can(regex("^db\\.", var.database_instance_class))
error_message = "La classe d'instance doit commencer par 'db.'."
}
}
# Bonne pratique : utiliser des objets pour regrouper les variables liées
variable "vpc_config" {
description = "Configuration du VPC"
type = object({
cidr_block = string
enable_dns_support = bool
enable_dns_hostnames = bool
})
default = {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}
}
Convention pour les tags des ressources cloud
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
Team = var.team_name
CostCenter = var.cost_center
CreatedAt = timestamp()
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-vpc"
})
}
Utilisation des Modules
Les modules sont le mécanisme principal de réutilisation et d'abstraction dans Terraform. Un bon module encapsule une fonctionnalité cohérente et expose une interface claire.
Principes de conception d'un module
- Responsabilité unique : chaque module gère un ensemble cohérent de ressources
- Interface claire : variables d'entrée bien documentées avec types et validations
- Sorties utiles : exposer les attributs nécessaires aux autres modules
- Valeurs par défaut sensées : fournir des defaults raisonnables pour un démarrage rapide
- Pas de provider hardcodé : laisser le module parent configurer les providers
Exemple de module bien structuré
# modules/networking/variables.tf
variable "vpc_cidr" {
description = "CIDR block pour le VPC"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Le CIDR block doit être un CIDR valide."
}
}
variable "public_subnet_cidrs" {
description = "Liste des CIDR blocks pour les subnets publics"
type = list(string)
default = []
}
variable "private_subnet_cidrs" {
description = "Liste des CIDR blocks pour les subnets privés"
type = list(string)
default = []
}
variable "availability_zones" {
description = "Liste des zones de disponibilité"
type = list(string)
}
variable "environment" {
description = "Nom de l'environnement (dev, staging, prod)"
type = string
}
# modules/networking/main.tf
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
Environment = var.environment
Type = "public"
}
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.environment}-private-subnet-${count.index + 1}"
Environment = var.environment
Type = "private"
}
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "ID du VPC créé"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "IDs des subnets publics"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs des subnets privés"
value = aws_subnet.private[*].id
}
Appel du module
# environments/prod/main.tf
module "networking" {
source = "../../modules/networking"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
availability_zones = ["eu-west-1a", "eu-west-1b"]
environment = "prod"
}
Versionnement des modules
Pour les modules partagés, utilisez un registre (Terraform Registry, Git, S3) avec des versions sémantiques :
module "networking" {
source = "git::https://github.com/mon-org/terraform-modules.git//networking?ref=v1.2.0"
# ou depuis le Terraform Registry
# source = "mon-org/networking/aws"
# version = "~> 1.2"
}
Gestion du State : Remote Backend et Locking
Le fichier state de Terraform est la pièce maîtresse de tout déploiement. Il contient la correspondance entre votre code et les ressources réelles dans le cloud. Une mauvaise gestion du state est l'une des erreurs les plus dangereuses.
Remote Backend avec S3 et DynamoDB
# backend.tf
terraform {
backend "s3" {
bucket = "mon-entreprise-terraform-state"
key = "environments/prod/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
# Activer le versionnement sur le bucket S3
# pour pouvoir récupérer un state précédent en cas de problème
}
}
Création du backend avec un script d'initialisation
# bootstrap/main.tf - À exécuter une seule fois
resource "aws_s3_bucket" "terraform_state" {
bucket = "mon-entreprise-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Bonnes pratiques pour le state
- Un state par environnement : ne jamais mélanger dev et prod dans le même state
- Chiffrement obligatoire : le state contient des données sensibles (mots de passe, clés)
- Versionnement du bucket : pour pouvoir restaurer un state corrompu
- Locking avec DynamoDB : pour éviter les modifications concurrentes
- Accès restreint : seuls les pipelines CI/CD et les administrateurs doivent avoir accès au state
Sécurité : Protéger vos Secrets et vos Accès
La sécurité en IaC est un sujet critique souvent négligé. Voici les pratiques essentielles pour sécuriser votre infrastructure Terraform.
Gestion des variables sensibles
# Marquer les variables comme sensibles
variable "database_password" {
description = "Mot de passe de la base de données"
type = string
sensitive = true
}
# Utiliser un gestionnaire de secrets
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ...
}
Ne jamais commiter de secrets
# .gitignore
*.tfvars
!example.tfvars
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
Utilisez des variables d'environnement pour passer les secrets :
# Les variables d'environnement TF_VAR_ sont automatiquement lues
export TF_VAR_database_password="mon-mot-de-passe-secret"
terraform apply
RBAC et principe du moindre privilège
# Politique IAM restrictive pour Terraform
resource "aws_iam_policy" "terraform_deployer" {
name = "terraform-deployer-policy"
description = "Politique pour le déploiement Terraform"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:*",
"rds:*",
"s3:*"
]
Resource = "*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = ["eu-west-1"]
}
}
},
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
Resource = "arn:aws:s3:::mon-entreprise-terraform-state/*"
},
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
Resource = "arn:aws:dynamodb:eu-west-1:*:table/terraform-state-lock"
}
]
})
}
CI/CD : Automatiser le Workflow Terraform
L'automatisation du workflow Terraform via un pipeline CI/CD est indispensable pour garantir la qualité et la traçabilité des changements d'infrastructure.
Pipeline GitLab CI
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_ROOT: "environments/prod"
TF_STATE_NAME: "prod"
.terraform_base:
image: hashicorp/terraform:1.7
before_script:
- cd ${TF_ROOT}
- terraform init
validate:
extends: .terraform_base
stage: validate
script:
- terraform fmt -check -recursive
- terraform validate
- tflint --init
- tflint
- checkov -d . --framework terraform
plan:
extends: .terraform_base
stage: plan
script:
- terraform plan -out=tfplan
- terraform show -json tfplan > plan.json
artifacts:
paths:
- ${TF_ROOT}/tfplan
- ${TF_ROOT}/plan.json
expire_in: 1 week
apply:
extends: .terraform_base
stage: apply
script:
- terraform apply -auto-approve tfplan
dependencies:
- plan
when: manual
only:
- main
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
working-directory: environments/prod
- name: Terraform Validate
run: terraform validate
working-directory: environments/prod
- name: Terraform Plan
run: terraform plan -no-color
working-directory: environments/prod
- name: Post Plan to PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
// Poster le plan en commentaire de la PR
Outils de Qualité : fmt, validate, tflint et checkov
Plusieurs outils complémentaires permettent de garantir la qualité de votre code Terraform.
terraform fmt
Formate automatiquement votre code selon les conventions HCL standard :
# Formater tous les fichiers récursivement
terraform fmt -recursive
# Vérifier le formatage sans modifier (utile en CI)
terraform fmt -check -recursive -diff
terraform validate
Vérifie la syntaxe et la cohérence interne de la configuration :
terraform init -backend=false
terraform validate
tflint
Linter avancé qui détecte les erreurs spécifiques aux providers et les mauvaises pratiques :
# .tflint.hcl
config {
module = true
}
plugin "aws" {
enabled = true
version = "0.30.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "terraform_naming_convention" {
enabled = true
}
rule "terraform_documented_variables" {
enabled = true
}
rule "terraform_documented_outputs" {
enabled = true
}
rule "terraform_unused_declarations" {
enabled = true
}
checkov
Analyse statique de sécurité pour détecter les mauvaises configurations :
# Installation
pip install checkov
# Analyse
checkov -d . --framework terraform
# Ignorer des règles spécifiques (avec justification)
checkov -d . --skip-check CKV_AWS_144 # Cross-region replication non nécessaire
Gestion des Versions
Verrouiller les versions de Terraform et des providers est crucial pour la reproductibilité des déploiements.
# versions.tf
terraform {
required_version = ">= 1.7.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.30"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
Le fichier .terraform.lock.hcl généré par terraform init verrouille les versions exactes des providers. Commitez ce fichier dans votre dépôt Git pour garantir que tous les membres de l'équipe et le pipeline CI/CD utilisent exactement les mêmes versions.
Tagging Systématique des Ressources
Le tagging est souvent négligé, mais il est fondamental pour la gestion des coûts, la sécurité et l'organisation des ressources cloud.
# Utiliser default_tags au niveau du provider
provider "aws" {
region = "eu-west-1"
default_tags {
tags = {
Project = "mon-projet"
Environment = var.environment
ManagedBy = "terraform"
Team = "platform"
CostCenter = "CC-12345"
}
}
}
# Les tags spécifiques à une ressource sont fusionnés avec les default_tags
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.medium"
tags = {
Name = "${var.project}-${var.environment}-web"
Role = "webserver"
}
}
Documentation du Code
Un code Terraform bien documenté réduit considérablement le temps d'onboarding et les erreurs de maintenance.
Commentaires dans le code
# Ce module crée l'infrastructure réseau principale.
# Il inclut le VPC, les subnets publics/privés, les NAT Gateways
# et les tables de routage associées.
#
# Architecture :
# - 2 AZs pour la haute disponibilité
# - Subnets publics pour les load balancers
# - Subnets privés pour les instances applicatives
# - NAT Gateways pour l'accès Internet sortant
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
# DNS support requis pour les endpoints VPC
enable_dns_support = true
enable_dns_hostnames = true
}
Documentation automatique avec terraform-docs
# Installation
brew install terraform-docs
# Générer la documentation d'un module
terraform-docs markdown table ./modules/networking > ./modules/networking/README.md
# Configuration via .terraform-docs.yml
# .terraform-docs.yml
formatter: markdown table
output:
file: README.md
mode: inject
sort:
enabled: true
by: required
Checklist des Bonnes Pratiques Terraform
Voici une checklist complète à utiliser pour chaque projet Terraform :
Organisation et Code
- Structure de répertoires claire avec séparation par environnement
- Fichiers découpés par responsabilité (main.tf, variables.tf, outputs.tf...)
- Convention de nommage snake_case cohérente
- Modules réutilisables avec interface claire
- Variables typées avec descriptions et validations
- Utilisation de
localspour les valeurs calculées - Pas de valeurs hardcodées dans les ressources
State et Backend
- Remote backend configuré (S3, GCS, Azure Blob...)
- Locking activé (DynamoDB, Cloud Storage...)
- Chiffrement du state activé
- Versionnement du bucket de state activé
- Un state par environnement
- Accès restreint au state
Sécurité
- Variables sensibles marquées avec
sensitive = true - Secrets gérés via un gestionnaire de secrets (Vault, AWS Secrets Manager...)
- Fichiers .tfvars exclus du contrôle de version
- RBAC et principe du moindre privilège pour les comptes de déploiement
- Analyse de sécurité avec checkov ou tfsec
Qualité et CI/CD
terraform fmtexécuté systématiquementterraform validatedans le pipeline- tflint configuré avec les règles du provider
- Plan automatique sur les Pull Requests
- Apply manuel (avec approbation) sur la branche principale
- Code review obligatoire pour tout changement d'infrastructure
Versionnement et Maintenance
- Version de Terraform verrouillée (
required_version) - Versions des providers verrouillées (
required_providers) - Fichier
.terraform.lock.hclcommité - Modules versionnés avec tags Git ou registre
- Documentation générée automatiquement (terraform-docs)
- Tagging systématique de toutes les ressources
Opérations
- Utiliser
prevent_destroysur les ressources critiques - Utiliser
create_before_destroyquand nécessaire - Tester les changements en dev/staging avant la production
- Garder les plans Terraform comme artefacts de déploiement
- Monitorer les drifts de configuration régulièrement
Conclusion
Adopter les bonnes pratiques Terraform n'est pas un luxe, c'est une nécessité pour toute équipe qui gère de l'infrastructure en production. La clé est de commencer par les fondamentaux — structure de répertoires, conventions de nommage, remote backend — puis d'ajouter progressivement les couches de qualité et de sécurité.
N'essayez pas d'implémenter toutes ces pratiques d'un coup. Commencez par celles qui ont le plus d'impact pour votre contexte : si vous travaillez en équipe, priorisez le remote backend et le locking ; si vous gérez des données sensibles, concentrez-vous sur la sécurité ; si vous avez des déploiements fréquents, automatisez votre pipeline CI/CD.
L'investissement dans ces bonnes pratiques vous fera gagner un temps considérable à long terme et vous évitera les incidents en production qui auraient pu être évités par une meilleure discipline de code.
Dans les prochains articles de cette série, nous explorerons des sujets avancés comme l'intégration de Terraform avec Kubernetes et l'import de ressources existantes. Restez connectés !