Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

14.2 — Infrastructure as Code

🎯 Objectif : ne plus jamais provisionner d’infra à la main. Une console cloud te trahit en six mois (qui a créé quoi ? quels droits ? quelle config ?). L’IaC met l’infra dans Git — versionnée, relue, reproductible.

À l'issue de cet axe, tu sauras :

  • Comprendre le rôle d'un état (state) et pourquoi il doit être distant
  • Écrire un module Terraform / OpenTofu réutilisable et le tester
  • Structurer un repo IaC pour gérer dev / staging / prod
  • Choisir entre Terraform, OpenTofu et Pulumi selon le contexte
  • Sécuriser secrets et accès cloud avec OIDC + Vault / SOPS

Confirmé 12 min prérequis : axes 4 et 8 lus

Ton infra est du code : VPC, base de données, CDN, domaine, DNS, IAM. Sans IaC :

  • Personne ne sait ce qui tourne en prod (« qui a ouvert ce port 22 ? »).
  • Reproduire l’env staging prend 3 jours et reste différent de prod.
  • Migrer de cloud est un projet à 6 mois.
  • Disaster recovery : au revoir.

Avec IaC :

  • terraform plan te dit exactement ce qui changera.
  • git diff montre la diff d’infra comme la diff de code.
  • destroy d’un environnement = 1 commande.
  • Migration cloud = nouveau provider, mêmes ressources logiques.

OutilLangageForceQuand l’utiliser
TerraformHCL (DSL)Standard de fait, écosystème énormeProjets multi-cloud, équipe ops
OpenTofuHCL — fork de TerraformOSS, gouverné par la Linux FoundationPréférer en 2026 si licence importe
PulumiTS / Python / Go / .NETVrai code (boucles, types, tests)Équipes dev qui veulent rester dans leur langage
AWS CDKTS / PythonGénère CloudFormation, AWS-onlyProjet 100 % AWS
AnsibleYAMLConfiguration de serveurs (post-provisioning)Bootstrapping VPS, configs OS
CrossplaneYAML K8sGérer l’infra depuis KubernetesPlateformes internes

Une intégration vers un cloud / service (AWS, GCP, Cloudflare, GitHub, Datadog, etc.).

terraform {
required_version = ">= 1.7"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.80" }
}
backend "s3" {
bucket = "myorg-tfstate"
key = "prod/terraform.tfstate"
region = "eu-west-3"
use_lockfile = true # OpenTofu 1.10+ : verrou natif sans DynamoDB
}
}
provider "aws" {
region = "eu-west-3"
}

Une ressource concrète à créer / gérer :

resource "aws_s3_bucket" "uploads" {
bucket = "myapp-uploads-prod"
tags = { Project = "myapp", Env = "prod" }
}
resource "aws_s3_bucket_public_access_block" "uploads" {
bucket = aws_s3_bucket.uploads.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

Paramètre injecté à l’exécution :

variable "env" {
type = string
description = "dev | staging | prod"
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env doit être dev, staging ou prod"
}
}
Fenêtre de terminal
terraform apply -var="env=prod"
# ou via fichier
terraform apply -var-file="prod.tfvars"

Valeur exposée vers d’autres modules / utilisateurs :

output "uploads_bucket_arn" {
value = aws_s3_bucket.uploads.arn
}

Lire un truc qui existe déjà :

data "aws_route53_zone" "main" {
name = "example.com"
}

Du code réutilisable :

module "uploads" {
source = "./modules/s3-bucket"
name = "myapp-uploads-prod"
env = var.env
}

┌─────────────┐
│ terraform │
│ init │ télécharge providers + config backend
└─────┬───────┘
┌─────────────┐
│ terraform │
│ plan │ diff entre code et état réel
└─────┬───────┘
┌─────────────┐
│ terraform │
│ apply │ applique les changements
└─────────────┘
CommandeQuand l’utiliser
initÀ chaque clone, à chaque ajout de provider
planAvant d’appliquer — toujours lire le diff
applyUne fois le plan validé
destroyDétruire toutes les ressources gérées par ce state
fmtAuto-formatter (en pre-commit)
validateCohérence syntaxique
stateManipuler le state (avec précaution)

Terraform stocke l’état (terraform.tfstate) — un mapping entre ton code et les ressources réelles. Sans état, il ne peut pas savoir ce qu’il a déjà créé.

# ❌ Local (par défaut, pour un projet perso seul)
# terraform.tfstate écrit dans le dossier courant.
# ✅ Distant (équipe, CI)
backend "s3" {
bucket = "myorg-tfstate"
key = "prod/terraform.tfstate"
region = "eu-west-3"
use_lockfile = true
}

Toujours distant en équipe. Sans backend distant :

  • Le state finit committé en clair (avec des secrets dedans !).
  • Deux dev qui appliquent en même temps corrompent l’état.

Le backend doit verrouiller l’état pendant un apply pour éviter les races. Backends qui supportent : S3 + lockfile (OpenTofu 1.10+), Terraform Cloud, Azure Storage, GCS, Postgres.

Un mot de passe RDS apparaîtra en clair dans terraform.tfstate. Conséquences :

  • Chiffrement at rest sur le bucket (S3 SSE, GCS-managed key).
  • Accès restreint au bucket de state (un IAM role dédié).
  • Jamais committé.

infra/
├── modules/
│ ├── s3-bucket/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── postgres/
│ └── ecs-service/
├── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── backend.tf # state path: dev/terraform.tfstate
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
└── README.md

Chaque env a son propre state isolé. Tu n’écrases pas la prod en testant dev.

Un seul state, plusieurs environnements via terraform workspace. Plus simple à mettre en place, mais plus risqué : un bug dans le code peut affecter tous les envs.

En 2026, le pattern « par environnement » domine en entreprise. Workspaces réservés aux mini-projets perso.


modules/s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.name
tags = merge(var.tags, { Module = "s3-bucket" })
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration { status = var.versioning ? "Enabled" : "Disabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}
modules/s3-bucket/variables.tf
variable "name" { type = string }
variable "tags" { type = map(string), default = {} }
variable "versioning" { type = bool, default = true }
# Utilisation
module "user_uploads" {
source = "../../modules/s3-bucket"
name = "myapp-uploads-${var.env}"
versioning = var.env == "prod"
tags = { Env = var.env }
}

Bonnes pratiques modules :

RèglePourquoi
Un module = un sous-système cohérentS3+IAM ensemble OK ; S3+RDS+VPC = trop
Pas plus de 5-6 ressources par module simpleAu-delà, splitter
Versionner avec git tag (source = "git::...?ref=v1.2.0")Reproductibilité
Documenter inputs / outputs dans README.mdterraform-docs génère ça automatiquement
Tester avec terratest ou tflintRégressions évitées
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
# ...
}

Utile pour les briques très standard. Pour le métier, écris tes propres modules — les modules publics deviennent vite des contraintes.


Pulumi te laisse écrire ton infra en TypeScript / Python / Go / .NET :

import * as aws from '@pulumi/aws';
const bucket = new aws.s3.Bucket('uploads', {
versioning: { enabled: true },
});
// Boucle, condition, fonction — tout est possible.
['fr', 'en', 'ar'].forEach((locale) => {
new aws.s3.Bucket(`assets-${locale}`, { /*...*/ });
});
export const bucketArn = bucket.arn;

Avantages :

  • IDE complet : autocomplete, refactor, types.
  • Tests : vitest / pytest sur la définition d’infra.
  • Boucles natives (vs for_each HCL parfois cryptique).

Inconvénient : écosystème moins large que Terraform Registry, moins d’exemples copy-pastable.


Pipeline standard :

.github/workflows/iac.yml
on:
pull_request:
paths: ['infra/**']
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
with: { tofu_version: '1.10.x' }
- uses: aws-actions/configure-aws-credentials@v4 # OIDC
with:
role-to-assume: arn:aws:iam::123:role/tfplan
aws-region: eu-west-3
- run: tofu init && tofu plan -no-color | tee plan.txt
- uses: actions/upload-artifact@v4
with: { name: plan, path: plan.txt }
- name: Comment plan on PR
uses: marocchino/sticky-pull-request-comment@v2
with: { path: plan.txt }

L’apply se fait après merge sur main, dans un environnement protégé (required reviewers).


Quelqu’un a modifié l’infra à la main via la console AWS ? Le drift est l’écart entre ton code et la réalité.

Fenêtre de terminal
tofu plan
# Si tu vois des changements alors que tu n'as pas modifié le code → drift.

Outils dédiés : driftctl, Spacelift, Atlantis, Terraform Cloud — détection programmée et alerte.

Politique saine : 0 toléré sur la prod. Les modifs manuelles sont autorisées en debug, mais doivent être rapatriées dans le code dans la journée.


RisqueParade
Secrets dans tfstateBackend chiffré + accès IAM restreint
Secrets dans tfvars committésJamais. Utiliser TF_VAR_* env, Vault, SOPS, AWS Secrets Manager
Provider cloud non pinnedversion = "~> 5.80" (auto-update mineur, pas majeur)
Apply non revuRequired reviewer sur l’env protégé
Module tiers compromisPin sur SHA / tag, scan avec tfsec / checkov
Élévation IAM accidentelleLinting policy (opa, conftest, checkov)

Outils SAST IaC :

  • tfsec / trivy config — scan Terraform / OpenTofu.
  • checkov — multi-IaC (Terraform, K8s, CloudFormation).
  • kics — alternative Checkmarx.

  • Cliquer dans la console « pour aller plus vite » → drift permanent.
  • Un seul state pour tous les envs → un bug en dev casse la prod.
  • Modules à 200 ressources → blast radius énorme à chaque modification.
  • terraform apply direct sur main sans review.
  • Pas de tests sur les modules réutilisés à 10 endroits.
  • Hardcoder des IDs cloud dans le code applicatif au lieu d’output IaC.

Tu es seul sur un projet, tu utilises Terraform avec backend local. Tu changes d'ordi et tu perds le terraform.tfstate. Que se passe-t-il ?
Sur un projet en équipe, deux dev font `terraform apply` en même temps sans verrou. Conséquence ?
Pour gérer dev / staging / prod en 2026 sur un nouveau projet d'équipe, lequel choisir ?
Tu choisis entre Terraform et OpenTofu sur un nouveau projet 2026. Quelle est la bonne raison de préférer OpenTofu ?


Suite : 14.3 — Hébergement pour choisir entre PaaS, IaaS et VPS.