L'infrastructure as code
Ce qu'on peut relire, versionner et reconstruire, plutôt que ce dont on se souvient.
La fiche 19 a décrit ce qui compose une infrastructure : un réseau, des machines, une base, un stockage. Reste à savoir comment on la crée. Cliquer dans la console d'un fournisseur marche le premier jour. Six mois plus tard, personne ne sait plus ce qui existe, ni pourquoi. L'infrastructure as code (IaC) décrit l'infrastructure dans des fichiers texte, versionnés dans Git, relus comme du code et appliqués par un outil.
Aucune commande Terraform, Ansible ou cloud de cette fiche n'a été exécutée : ce sont des illustrations, relues et comparées à la documentation officielle.
1.Le problème : tout à la main
La boutique a grandi. Quelqu'un a créé la base de production dans la console web, il y a deux ans. Un autre a ouvert un port « pour un test », sans le refermer. La recette a été montée plus tard, à la main elle aussi, avec des réglages un peu différents. Et un jour, il faut recréer la production dans une autre région, ou simplement comprendre pourquoi un bug n'apparaît qu'en production.
- Pas d'historique : qui a changé ce réglage, quand, et pourquoi ? Personne ne sait.
- Pas de relecture : un clic malheureux part directement en production, sans qu'un collègue l'ait vu.
- Pas de reconstruction : recréer l'infrastructure après un incident, c'est tout refaire de mémoire.
- Des environnements qui divergent : la recette ne ressemble plus vraiment à la production (fiche 18).
Avec l'IaC, l'infrastructure suit le même chemin que le code : une modification, une pull request, une relecture, une fusion, puis l'outil applique.
2.Décrire l'état voulu, pas les étapes
Un script qui enchaîne « crée une machine, puis ouvre le port 443 » est impératif : il décrit des étapes. Lancé deux fois, il crée deux machines. La plupart des outils d'IaC sont déclaratifs : tu décris l'état voulu (« il existe une machine, avec le port 443 ouvert »), et l'outil calcule lui-même ce qu'il faut faire pour y arriver. Voici quatre plans successifs de Terraform, sur l'infrastructure de la section 3. La sortie est raccourcie, aux endroits marqués (…) :
$ terraform plan # 1. Le premier plan : rien n'existe encore
Terraform will perform the following actions:
# aws_db_instance.main will be created
+ resource "aws_db_instance" "main" {
+ engine = "postgres"
+ identifier = "boutique-production"
+ instance_class = "db.t4g.small"
+ multi_az = true
(…)
}
# aws_s3_bucket.uploads will be created
+ resource "aws_s3_bucket" "uploads" {
+ bucket = "boutique-production-uploads"
(…)
}
# aws_s3_bucket_public_access_block.uploads will be created
# aws_s3_bucket_versioning.uploads will be created
(…)
Plan: 4 to add, 0 to change, 0 to destroy.
$ terraform plan # 2. Relancé juste après l'apply
No changes. Your infrastructure matches the configuration.
$ terraform plan # 3. Quelqu'un a suspendu le versionnage à la main
# aws_s3_bucket_versioning.uploads will be updated in-place
~ resource "aws_s3_bucket_versioning" "uploads" {
id = "boutique-production-uploads"
~ versioning_configuration {
~ status = "Suspended" -> "Enabled"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
$ terraform plan # 4. La base a été retirée du code par erreur
│ Error: Instance cannot be destroyed
│
│ Resource aws_db_instance.main has lifecycle.prevent_destroy set, but the plan
│ calls for this resource to be destroyed.- Le premier plan crée tout, puisque rien n'existe encore.
- Le second ne fait rien : l'opération est idempotente, comme un
PUTHTTP (fiche 17). On peut relancer l'outil sans crainte. - Une modification à la main est détectée et sera annulée au prochain passage : le code fait autorité. C'est l'écart (drift), détaillé en section 8.
- Une suppression dangereuse est bloquée, parce que la ressource est marquée comme protégée : le plan entier échoue, rien n'est appliqué.
3.Terraform et OpenTofu
Terraform est l'outil d'IaC le plus répandu. En 2023, son éditeur, HashiCorp, a changé sa licence pour une licence qui n'est plus open source. Une partie de la communauté a alors créé OpenTofu, un dérivé open source hébergé par la Linux Foundation. Les deux restent très proches : ce qui suit vaut pour l'un et l'autre. Ils décrivent l'infrastructure dans un langage appelé HCL :
# modules/boutique/main.tf : le stockage des fichiers et la base de données
# (le réseau est omis pour rester court)
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
variable "environment" {
type = string
description = "staging ou production"
}
resource "aws_s3_bucket" "uploads" {
bucket = "boutique-${var.environment}-uploads"
}
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
}
resource "aws_s3_bucket_versioning" "uploads" {
bucket = aws_s3_bucket.uploads.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_db_instance" "main" {
identifier = "boutique-${var.environment}"
engine = "postgres"
engine_version = "17"
instance_class = "db.t4g.small"
allocated_storage = 20
db_name = "boutique"
username = "boutique"
manage_master_user_password = true # mot de passe créé et gardé par AWS, jamais dans le code
multi_az = var.environment == "production"
storage_encrypted = true
backup_retention_period = 14
deletion_protection = true
final_snapshot_identifier = "boutique-${var.environment}-final"
lifecycle {
prevent_destroy = true # Terraform refuse tout plan qui détruirait la base
}
}
output "uploads_bucket" {
value = aws_s3_bucket.uploads.bucket
}- Un provider sait parler à un fournisseur : AWS, Scaleway, OVHcloud, mais aussi GitHub ou Cloudflare. Il en existe des centaines.
- Une ressource (
resource) décrit un élément à créer : un bucket, une base, une règle de pare-feu. - Une variable rend le code réutilisable, ici pour la recette et la production. Un output expose une valeur utile après la création, comme le nom du bucket.
- Les références créent l'ordre :
aws_s3_bucket.uploads.iddit que le blocage de l'accès public dépend du bucket. Terraform crée donc le bucket en premier, sans qu'on écrive d'ordre.
Remarque les protections : le mot de passe de la base est créé et gardé par AWS, jamais écrit dans le code, et prevent_destroy fait échouer tout plan qui détruirait la base. Le travail suit toujours le même cycle, résumé par le schéma 20.2 :
terraform inittélécharge les providers et prépare l'état.terraform planaffiche ce qui va être créé (+), modifié (~) ou détruit (-). Lis-le toujours en entier, et cherche les destructions.terraform applyexécute le plan, après confirmation.
4.L'état : la mémoire de Terraform
Pour savoir que le bucket boutique-production-uploads du code correspond à tel bucket réel chez AWS, Terraform tient un registre : l'état (state), un fichier JSON qui associe chaque ressource du code à l'objet réel et à ses attributs. C'est le fichier le plus important, et le plus délicat, d'un projet Terraform.
- Partagé : rangé dans un stockage commun (un bucket), jamais sur le poste d'une personne. Sinon, chacun a sa propre version de la réalité.
- Verrouillé : un seul
applyà la fois. Deux exécutions simultanées peuvent corrompre l'état. - Privé et chiffré : il contient les attributs des ressources, parfois des mots de passe. Il ne va jamais dans Git.
- Un état par environnement : une erreur dans la recette ne doit pas pouvoir toucher la production.
# environments/production/main.tf
terraform {
backend "s3" {
bucket = "boutique-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-3"
encrypt = true
use_lockfile = true # un verrou : deux « apply » ne se croisent pas (Terraform 1.10+)
}
}
provider "aws" {
region = "eu-west-3"
}
module "boutique" {
source = "../../modules/boutique"
environment = "production"
}
# environments/staging/main.tf est identique, avec « staging » à la place de « production » :
# le même module, donc la même infrastructure, avec un autre état.Pour une ressource créée à la main avant l'arrivée de Terraform, inutile de la détruire : un bloc import l'ajoute à l'état, puis on écrit le code qui la décrit, jusqu'à ce que le plan n'affiche plus aucun changement.
5.Modules et environnements
Le fichier de la section 3 est un module : un ensemble de ressources réutilisable, avec ses variables d'entrée. Chaque environnement l'appelle avec ses propres valeurs, comme une fonction avec ses arguments. Dans le fichier de la section 4, la production appelle le module avec environment = "production" ; la recette fait de même avec "staging", dans un autre dossier et avec un autre état.
- La recette et la production ont la même forme, puisqu'elles sortent du même module. Les différences sont visibles, en paramètres : ici, une base de secours seulement en production (
multi_az). - Une modification passe d'abord en recette : on applique le module modifié sur la recette, on vérifie, puis sur la production.
- N'en fais pas trop : un module qui accepte trente variables pour couvrir tous les cas devient plus difficile à lire que les ressources qu'il cache. Comme pour les abstractions de la fiche 6, extrais un module quand le deuxième usage arrive.
6.Configurer les machines, ou les remplacer
Terraform crée les ressources. Mais une machine virtuelle créée doit encore être configurée : paquets, fichiers de configuration, services. C'est le rôle d'Ansible, qui se connecte aux machines en SSH, sans rien installer dessus, et applique un playbook :
# playbook.yml : configurer les serveurs web (le dépôt de paquets PHP est supposé déjà ajouté)
- name: Serveur web de la boutique
hosts: web
become: true
tasks:
- name: Installer nginx et PHP-FPM
ansible.builtin.apt:
name: [nginx, php8.4-fpm]
state: present
update_cache: true
- name: Déposer la configuration du site
ansible.builtin.template:
src: templates/boutique.conf.j2
dest: /etc/nginx/sites-enabled/boutique.conf
mode: '0644'
notify: Recharger nginx
- name: Lancer nginx, aussi au démarrage de la machine
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Recharger nginx # seulement si la configuration a changé
ansible.builtin.service:
name: nginx
state: reloaded- Chaque tâche est idempotente :
state: presentinstalle nginx seulement s'il manque. On relance le playbook sans risque. - Les handlers ne s'exécutent que si une tâche a changé quelque chose : nginx n'est rechargé que si sa configuration a réellement changé.
- L'inventaire, un fichier à part, liste les machines du groupe
web.
Une autre approche évite presque tout ce travail : ne jamais modifier une machine qui tourne, mais la remplacer. C'est l'infrastructure immuable, et c'est exactement ce que font les images Docker construites par la CI (fiches 12 et 18). Le schéma 20.4 compare les deux approches.
Ansible reste utile pour des machines qui vivent longtemps : un serveur unique comme celui de la fiche 19, des machines physiques, ou la préparation d'une image de machine virtuelle.
Avec des conteneurs et des services gérés, il n'y a presque plus rien à configurer à la main : l'image contient l'application, Terraform crée le reste. Plus de configuration en place, moins de serveurs uniques.
7.GitOps : Git comme source de vérité
Le GitOps pousse la logique jusqu'au bout : l'état voulu de l'infrastructure est dans Git, et seule une fusion dans la branche principale peut le changer. Plus personne ne lance terraform apply depuis son poste.
# .github/workflows/infra.yml : plan sur chaque pull request, apply après la fusion
name: Infrastructure
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
permissions:
contents: read
id-token: write # OIDC : un jeton AWS de quelques minutes, aucune clé stockée
defaults:
run:
working-directory: infra/environments/production
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-plan # lecture seule
aws-region: eu-west-3
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform plan -input=false # le plan s'affiche dans le journal du job
apply:
if: github.event_name == 'push'
runs-on: ubuntu-24.04
environment: production # une personne approuve avant
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-apply
aws-region: eu-west-3
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform plan -input=false -out=tfplan
- run: terraform apply -input=false tfplan # exactement le plan affiché juste avant- Le plan est affiché sur la pull request, et la relecture porte sur lui autant que sur le code. Certaines équipes le publient en commentaire avec une action dédiée.
- L'apply exécute un plan enregistré (
-out=tfplan) : on applique exactement ce qu'on a relu, et rien d'autre. - OIDC au lieu de clés : le pipeline obtient d'AWS un jeton de quelques minutes (fiche 18). Le rôle de plan peut seulement lire ; seul celui de l'apply peut modifier.
- Pour Kubernetes, des outils comme Argo CD ou Flux tournent dans le cluster, lisent le dépôt en continu et corrigent le cluster pour qu'il lui ressemble. On parle de modèle « tiré ».
8.L'écart et la discipline
L'IaC ne vaut que si tout le monde la respecte. Une modification faite dans la console crée un écart entre le code et la réalité. Au prochain apply, deux issues seulement, toutes deux mauvaises : soit l'outil annule la modification sans prévenir, soit quelqu'un a peur de lancer l'apply, et l'IaC est abandonnée.
- Détecte l'écart : un plan lancé chaque nuit par la CI, qui alerte s'il n'est pas vide.
terraform plan -refresh-onlymontre seulement ce qui a changé dans la réalité. - Prévois une procédure d'urgence : en plein incident, une modification à la main peut être nécessaire. Elle est notée, puis reportée dans le code le jour même.
- Protège ce qui ne se recrée pas :
prevent_destroyet la protection contre la suppression du fournisseur sur les bases et les stockages de données. - Analyse le code avant de l'appliquer : des outils comme Trivy ou Checkov signalent un bucket public, un port ouvert à tout Internet ou une base non chiffrée, dans la pull request.
- Aucun secret dans les fichiers
.tf, ni dans les fichiers de variables commités. Laisse le fournisseur créer et garder les mots de passe, ou lis-les depuis un gestionnaire de secrets. sensitive = truemasque une valeur dans l'affichage du plan, mais elle reste en clair dans l'état. D'où un état privé, chiffré, et réservé à peu de personnes. OpenTofu sait aussi chiffrer l'état lui-même.- Ajoute
*.tfstateet.terraform/au.gitignoredès le premier commit. - Les identifiants du pipeline donnent la main sur toute l'infrastructure : rôles séparés pour le plan et l'apply, et approbation obligatoire avant la production.
Terraform demande un apprentissage, et une modification prend plus de temps qu'un clic. Pour un seul serveur, ou une application sur un PaaS qui gère presque tout, quelques notes bien tenues peuvent suffire. L'IaC devient rentable dès qu'il y a plusieurs environnements, plusieurs personnes, ou une infrastructure qu'on doit pouvoir reconstruire.
Pièges fréquents
| Piège | Ce qui arrive | La parade |
|---|---|---|
| Appliquer sans lire le plan | Une base détruite et recréée vide | Lire chaque ligne, et chercher les - |
| L'état sur le poste d'une personne | Les autres créent des ressources en double | Un état partagé, avec un verrou |
| L'état commité dans Git | Des mots de passe dans l'historique, pour toujours | .gitignore, état privé et chiffré |
| Un seul état pour la recette et la production | Une erreur en recette touche la production | Un état par environnement |
| Des modifications dans la console « juste pour cette fois » | Un écart, puis un apply qui annule tout | Un plan de nuit, et une procédure d'urgence |
| Une base sans protection | Un renommage dans le code la détruit | prevent_destroy et protection du fournisseur |
| Des clés du cloud en dur dans le code | Elles fuient avec le dépôt | OIDC, ou un profil local jamais commité |
| Des versions de provider non fixées | Un plan différent d'un jour à l'autre | required_providers et le fichier de verrouillage commité |
| Des serveurs modifiés en place depuis des années | Des « flocons de neige » que personne n'ose recréer | Ansible pour tout changement, ou des images immuables |
Checklist d'un projet d'infrastructure
- Toute l'infrastructure est décrite en code, et une modification passe par une pull request.
- Le plan est relu avant chaque apply, et l'apply exécute exactement ce plan.
- L'état est partagé, verrouillé, chiffré, séparé par environnement, et absent de Git.
- Les bases et les stockages de données sont protégés contre la destruction.
- Aucun secret n'est écrit dans le code ; le pipeline utilise OIDC et des rôles séparés.
- Les versions des outils et des providers sont fixées.
- Un plan automatique détecte l'écart entre le code et la réalité.
- Un analyseur signale les erreurs de sécurité dans chaque pull request.
Exercice : relire une infrastructure
Un collègue a écrit ce fichier pour mettre la boutique dans le cloud. Ne l'applique surtout pas : lis-le comme une pull request. Les clés et le mot de passe sont fictifs.
provider "aws" {
region = "eu-west-3"
access_key = "AKIAEXEMPLEFICTIF000"
secret_key = "cle-fictive-pour-l-exercice"
}
resource "aws_s3_bucket" "uploads" {
bucket = "boutique-uploads"
}
resource "aws_s3_bucket_acl" "uploads" {
bucket = aws_s3_bucket.uploads.id
acl = "public-read"
}
resource "aws_security_group" "server" {
name = "boutique-server"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_db_instance" "main" {
identifier = "boutique"
engine = "postgres"
instance_class = "db.t4g.small"
allocated_storage = 20
username = "admin"
password = "changeme123"
publicly_accessible = true
storage_encrypted = false
skip_final_snapshot = true
}- Trouve au moins huit problèmes. Pour chacun, explique ce qui peut arriver, et classe-les : fuite de données, perte de données, accès non autorisé, ou fiabilité.
- Ce dossier contient aussi un fichier
terraform.tfstate, commité. Pourquoi est-ce grave, même une fois le mot de passe retiré du code ? Que faut-il faire, et dans quel ordre ? - Réécris la base et le bucket en t'appuyant sur le module de la section 3, et ajoute une règle de pare-feu qui n'ouvre PostgreSQL qu'aux serveurs de l'application.
- Quelqu'un supprime le bucket à la main dans la console. Écris, au format de la section 2, le plan que Terraform affichera ensuite. Quelles ressources seront recréées, et pourquoi pas seulement le bucket ? Que sont devenus les fichiers qu'il contenait ?
- Question de recul : l'équipe ne compte que toi, et la boutique tourne sur un seul serveur. Que mettrais-tu en code dès maintenant, et que laisserais-tu pour plus tard ?
Conseils
- Commence par ce qui ne bouge plus. Importe d'abord les ressources stables (le réseau, le stockage), puis le reste, une ressource à la fois, jusqu'à un plan vide.
- Lis les plans des autres dans les pull requests, même si tu n'es pas l'auteur : c'est la meilleure façon d'apprendre ce que fait chaque ressource.
- Garde la documentation du provider ouverte. Chaque ressource y décrit ses attributs et ses pièges, comme les réglages qui forcent la destruction puis la recréation d'une ressource.
- Teste la reconstruction. Recrée de temps en temps un environnement complet à partir du code, comme on teste une restauration de sauvegarde (fiche 13). C'est la seule preuve que le code décrit vraiment tout.
Documentation officielle
- Terraform, dont l'état et l'import de ressources existantes (en anglais)
- OpenTofu, dont le chiffrement de l'état (en anglais)
- Le provider AWS, la référence de chaque ressource (en anglais)
- Ansible (en anglais)
- Les principes OpenGitOps, Argo CD et Flux (en anglais)
- OIDC dans GitHub Actions