Aller au contenu principal

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

NameVersion
terraform>= 1.5.0
aws~> 5.0

Inputs

NameDescriptionTypeDefaultRequired
vpc_cidrCIDR block for VPCstringn/ayes
environmentEnvironment namestringn/ayes

Outputs

NameDescription
vpc_idID 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

  1. Configurez pre-commit avec terraform-fmt et tfsec
  2. Créez un workflow GitHub Actions complet
  3. Générez la documentation avec terraform-docs
  4. Estimez les coûts avec Infracost

← Modules | Exercices et projets →