Bonnes pratiques Terraform
Objectifs du chapitre
- Appliquer les patterns recommandés
- Sécuriser les configurations
- Optimiser la structure de projet
- Intégrer Terraform dans CI/CD
1 - Organisation du code
Structure de projet
Exemple de structure
terraform-infra/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ └── ...
│ └── production/
│ └── ...
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ └── database/
├── .github/
│ └── workflows/
│ ├── terraform-plan.yml
│ └── terraform-apply.yml
├── .gitignore
├── .terraform-version
└── README.md
2 - Nommage
Conventions
# ✅ Ressources: snake_case descriptif
resource "aws_instance" "web_server_production" {}
resource "aws_security_group" "web_inbound_rules" {}
# ✅ Variables: snake_case avec préfixe si besoin
variable "vpc_cidr_block" {}
variable "instance_type" {}
# ✅ Modules: kebab-case pour les dossiers
module "web-cluster" {
source = "./modules/web-cluster"
}
# ❌ À éviter
resource "aws_instance" "Instance1" {}
resource "aws_instance" "i" {}
variable "VpcCidr" {}
Tags standards
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
Owner = var.team_name
CostCenter = var.cost_center
}
}
resource "aws_instance" "web" {
# ...
tags = merge(local.common_tags, {
Name = "${var.project_name}-web-${var.environment}"
Role = "webserver"
})
}
3 - Sécurité
Ne jamais hardcoder les secrets
# ❌ DANGER
provider "aws" {
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
# ✅ Variables d'environnement
provider "aws" {
# Utilise AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY
}
# ✅ Profil AWS CLI
provider "aws" {
profile = "production"
}
# ✅ IAM Role (EC2/ECS)
provider "aws" {
# Utilise le role de l'instance
}
Variables sensibles
variable "db_password" {
description = "Database password"
type = string
sensitive = true
# Pas de default pour les secrets!
}
output "connection_string" {
value = "postgresql://user:${var.db_password}@${aws_db_instance.main.endpoint}/db"
sensitive = true
}
Backend sécurisé
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "prod/terraform.tfstate"
region = "eu-west-1"
encrypt = true # Chiffrement au repos
dynamodb_table = "terraform-locks" # Locking
# KMS pour chiffrement renforcé
kms_key_id = "arn:aws:kms:eu-west-1:123456789:key/xxx"
}
}
Scan de sécurité
# tfsec - Scan de sécurité
tfsec .
# Checkov - Policy as Code
checkov -d .
# Terrascan
terrascan scan
4 - Validation et tests
Validation syntaxique
# Valider la syntaxe
terraform validate
# Formater le code
terraform fmt
# Vérifier le formatage
terraform fmt -check -recursive
Pre-commit hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_docs
- id: terraform_tfsec
# Installation
pip install pre-commit
pre-commit install
Tests avec Terratest
// test/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVpcModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"vpc_cidr": "10.0.0.0/16",
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
5 - CI/CD avec GitHub Actions
Workflow Terraform
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: "1.6.0"
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Upload Plan
uses: actions/upload-artifact@v3
with:
name: tfplan
path: tfplan
# Apply uniquement sur main
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Workflow avec environnements
6 - Gestion des coûts
Infracost
# Installation
brew install infracost
# Configuration
infracost auth login
# Estimation des coûts
infracost breakdown --path .
# Dans CI/CD
infracost diff --path . --compare-to infracost-base.json
Output exemple
Project: terraform-project
Name Monthly Qty Unit Monthly Cost
aws_instance.web
├─ Instance usage (Linux/UNIX, on-demand, t3.large) 730 hours $60.74
└─ root_block_device
└─ Storage (general purpose SSD, gp3) 20 GB $1.60
aws_db_instance.main
├─ Database instance (on-demand, db.t3.medium) 730 hours $29.93
└─ Storage (general purpose SSD, gp2) 100 GB $11.50
OVERALL TOTAL $103.77
7 - Documentation
terraform-docs
# Générer la documentation
terraform-docs markdown table . > README.md
# Configuration
# .terraform-docs.yml
formatter: markdown table
sections:
show:
- requirements
- providers
- inputs
- outputs
output:
file: README.md
mode: inject
Template README
# Module VPC
Creates a VPC with public and private subnets.
## Usage
```hcl
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "production"
}
Requirements
| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
| aws | ~> 5.0 |
Inputs
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| vpc_cidr | CIDR block for VPC | string | n/a | yes |
| environment | Environment name | string | n/a | yes |
Outputs
| Name | Description |
|---|---|
| vpc_id | ID of the VPC |
---
## 8 - Patterns avancés
### DRY avec locals
```hcl
locals {
az_count = length(data.aws_availability_zones.available.names)
subnet_configs = {
for idx in range(local.az_count) : "subnet-${idx}" => {
az = data.aws_availability_zones.available.names[idx]
cidr = cidrsubnet(var.vpc_cidr, 8, idx)
is_public = idx < 2
}
}
}
resource "aws_subnet" "main" {
for_each = local.subnet_configs
vpc_id = aws_vpc.main.id
availability_zone = each.value.az
cidr_block = each.value.cidr
map_public_ip_on_launch = each.value.is_public
}
Feature flags
variable "enable_monitoring" {
type = bool
default = false
}
variable "enable_backup" {
type = bool
default = true
}
resource "aws_cloudwatch_metric_alarm" "cpu" {
count = var.enable_monitoring ? 1 : 0
# ...
}
resource "aws_backup_plan" "main" {
count = var.enable_backup ? 1 : 0
# ...
}
Zero-downtime deployments
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true
}
}
resource "aws_launch_template" "web" {
# ...
lifecycle {
create_before_destroy = true
}
}
9 - Checklist production
Avant le premier déploiement
☐ Backend distant configuré (S3/GCS/Azure)
☐ State chiffré et versionné
☐ Locking activé (DynamoDB)
☐ Credentials via variables d'environnement
☐ Secrets jamais dans le code
☐ .gitignore configuré
☐ Versions fixées (Terraform et providers)
☐ Pre-commit hooks installés
Avant chaque apply
☐ terraform fmt vérifié
☐ terraform validate passé
☐ tfsec/checkov sans erreur critique
☐ terraform plan revu
☐ Changements attendus confirmés
☐ Coûts estimés vérifiés
Après le déploiement
☐ Outputs vérifiés
☐ Ressources accessibles
☐ Monitoring opérationnel
☐ Alertes configurées
☐ Documentation mise à jour
10 - Anti-patterns à éviter
Ce qu'il ne faut pas faire
# ❌ Hardcoder les valeurs
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
}
# ✅ Utiliser des variables et data sources
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
}
# ❌ Provisioners (sauf exception)
resource "aws_instance" "web" {
provisioner "remote-exec" {
inline = ["apt install nginx"]
}
}
# ✅ Utiliser user_data ou Ansible
resource "aws_instance" "web" {
user_data = file("${path.module}/scripts/init.sh")
}
# ❌ Ignorer le state
# Ne jamais manipuler manuellement terraform.tfstate
# ✅ Utiliser les commandes state
terraform state mv ...
terraform import ...
Résumé
Points clés
- Séparez les environnements en dossiers distincts
- Ne jamais hardcoder de secrets
- Utilisez un backend distant chiffré
- Automatisez avec CI/CD
- Documentez avec terraform-docs
- Testez avec pre-commit et tfsec
Exercices pratiques
- Configurez pre-commit avec terraform-fmt et tfsec
- Créez un workflow GitHub Actions complet
- Générez la documentation avec terraform-docs
- Estimez les coûts avec Infracost