← Toutes les fiches

Fiche 20 Parcours : DevOps Lecture : 25 minutes

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.

L'infrastructure à la main, ou en code À gauche, l'infrastructure créée à la main dans la console web : la recette a été créée en 2023 par une personne partie depuis, la production a un port ouvert pour un test. Personne ne sait exactement ce qui existe. À droite, un fichier main.tf, relu et versionné dans Git, crée la recette et la production à partir du même module, avec des paramètres différents. À la main, dans la console En code, dans Git Console web des clics, sans trace Recette créée en 2023, auteur parti depuis Production un port ouvert « pour un test » Personne ne sait exactement ce qui existe, ni pourquoi la recette diffère de la production. main.tf relu, versionné Recette le même module, des paramètres Production le même module, des paramètres Chaque changement est une pull request, et on peut tout reconstruire à l'identique.
Schéma 20.1 · L'infrastructure à la main, ou en code
  1. Pas d'historique : qui a changé ce réglage, quand, et pourquoi ? Personne ne sait.
  2. Pas de relecture : un clic malheureux part directement en production, sans qu'un collègue l'ait vu.
  3. Pas de reconstruction : recréer l'infrastructure après un incident, c'est tout refaire de mémoire.
  4. 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 (…) :

exemples/20/plan.txt · illustration, non exécuté
$ 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.
  1. Le premier plan crée tout, puisque rien n'existe encore.
  2. Le second ne fait rien : l'opération est idempotente, comme un PUT HTTP (fiche 17). On peut relancer l'outil sans crainte.
  3. 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.
  4. 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 :

exemples/20/main.tf · illustration, non exécuté
# 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
}
  1. Un provider sait parler à un fournisseur : AWS, Scaleway, OVHcloud, mais aussi GitHub ou Cloudflare. Il en existe des centaines.
  2. Une ressource (resource) décrit un élément à créer : un bucket, une base, une règle de pare-feu.
  3. 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.
  4. Les références créent l'ordre : aws_s3_bucket.uploads.id dit 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 :

  1. terraform init télécharge les providers et prépare l'état.
  2. terraform plan affiche ce qui va être créé (+), modifié (~) ou détruit (-). Lis-le toujours en entier, et cherche les destructions.
  3. terraform apply exécute le plan, après confirmation.
Le cycle de Terraform : écrire, planifier, relire, appliquer Quatre étapes : écrire les fichiers .tf, lancer le plan qui montre ce qui va changer, relire ce plan dans une pull request, puis l'appliquer, ce qui modifie le cloud. Au centre, le fichier d'état, terraform.tfstate, garde ce que Terraform a créé : le plan le lit et le compare à la réalité, l'apply le met à jour. Écrire .tf modifié Plan ce qui va changer Revue pull request Apply exécute le plan Le cloud la réalité L'état (terraform.tfstate) ce que Terraform a créé, et leurs identifiants lit met à jour compare Le plan compare trois choses : le code, l'état, et ce qui existe vraiment. Rien ne change tant que personne n'a relu le plan et lancé l'apply.
Schéma 20.2 · Le cycle de Terraform : écrire, planifier, relire, appliquer

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.

Un état partagé, protégé par un verrou Alice et Bob lancent terraform apply en même temps. L'état de la production est rangé dans un bucket privé et chiffré. Alice prend le verrou la première ; Bob attend, avec le message « état verrouillé par Alice ». Sans état partagé, chacun croit être seul ; sans verrou, deux apply peuvent corrompre l'état. Alice terraform apply Bob terraform apply Bucket de l'état (privé, chiffré) production.tfstate verrou : Alice prend le verrou Bob attend : « état verrouillé par Alice ». Sans état partagé, chacun a sa copie et croit être seul. Sans verrou, deux apply simultanés peuvent corrompre l'état. Et l'état contient parfois des secrets : il doit rester privé.
Schéma 20.3 · Un état partagé, protégé par un verrou
  1. 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é.
  2. Verrouillé : un seul apply à la fois. Deux exécutions simultanées peuvent corrompre l'état.
  3. Privé et chiffré : il contient les attributs des ressources, parfois des mots de passe. Il ne va jamais dans Git.
  4. Un état par environnement : une erreur dans la recette ne doit pas pouvoir toucher la production.
exemples/20/environments.tf · illustration, non exécuté
# 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.

  1. 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).
  2. Une modification passe d'abord en recette : on applique le module modifié sur la recette, on vérifie, puis sur la production.
  3. 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 :

exemples/20/playbook.yml · illustration, non exécuté
# 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
  1. Chaque tâche est idempotente : state: present installe nginx seulement s'il manque. On relance le playbook sans risque.
  2. 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é.
  3. 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, ou des images ?

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.

Modifier un serveur en place, ou le remplacer En haut, l'approche mutable : un serveur installé en 2024 reçoit un correctif en mars, PHP 8.3 en juin, puis un réglage fait à la main. Au bout d'un an, chaque serveur est unique. En bas, l'approche immuable : la CI construit une image v1 puis une image v2, et la v2 est déployée sur de nouvelles machines, les anciennes étant détruites. Modifier en place (mutable) serveur installé en 2024 + correctif mars + PHP 8.3 juin + réglage à la main Au bout d'un an, chaque serveur est unique : un « flocon de neige » que personne n'ose recréer. Remplacer (immuable) image v1 construite par la CI image v2 construite par la CI nouvelles machines les anciennes détruites On ne modifie jamais une machine qui tourne : on la remplace. Revenir en arrière, c'est redéployer l'image précédente.
Schéma 20.4 · Modifier un serveur en place, ou le remplacer

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.

GitOps : Git comme source de vérité En haut, le modèle poussé : une pull request modifie l'infrastructure, la CI affiche le plan pour la revue, la fusion a lieu après approbation, puis la CI applique le plan relu. En bas, le modèle tiré : Argo CD, installé dans le cluster Kubernetes, lit en permanence l'état voulu dans le dépôt Git et corrige l'état réel du cluster pour qu'il corresponde. Poussé par la CI Pull request modifie infra/ CI : plan affiché pour la revue Fusion après approbation CI : apply le plan relu Tiré par le cluster (Argo CD, Flux) Dépôt Git l'état voulu Argo CD dans le cluster Kubernetes l'état réel lit corrige Dans les deux cas, Git est la source de vérité : on ne change rien à la main, et l'historique des commits dit qui a changé quoi, quand, et pourquoi.
Schéma 20.5 · GitOps : Git comme source de vérité
exemples/20/infra.yml · illustration, non exécuté
# .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
  1. 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.
  2. L'apply exécute un plan enregistré (-out=tfplan) : on applique exactement ce qu'on a relu, et rien d'autre.
  3. 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.
  4. 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.

  1. Détecte l'écart : un plan lancé chaque nuit par la CI, qui alerte s'il n'est pas vide. terraform plan -refresh-only montre seulement ce qui a changé dans la réalité.
  2. 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.
  3. Protège ce qui ne se recrée pas : prevent_destroy et la protection contre la suppression du fournisseur sur les bases et les stockages de données.
  4. 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.
Sécurité : les secrets et l'IaC
  1. 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.
  2. sensitive = true masque 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.
  3. Ajoute *.tfstate et .terraform/ au .gitignore dès le premier commit.
  4. 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.
Quand l'IaC ne vaut pas le coup

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ègeCe qui arriveLa parade
Appliquer sans lire le planUne base détruite et recréée videLire chaque ligne, et chercher les -
L'état sur le poste d'une personneLes autres créent des ressources en doubleUn état partagé, avec un verrou
L'état commité dans GitDes mots de passe dans l'historique, pour toujours.gitignore, état privé et chiffré
Un seul état pour la recette et la productionUne erreur en recette touche la productionUn état par environnement
Des modifications dans la console « juste pour cette fois »Un écart, puis un apply qui annule toutUn plan de nuit, et une procédure d'urgence
Une base sans protectionUn renommage dans le code la détruitprevent_destroy et protection du fournisseur
Des clés du cloud en dur dans le codeElles fuient avec le dépôtOIDC, ou un profil local jamais commité
Des versions de provider non fixéesUn plan différent d'un jour à l'autrerequired_providers et le fichier de verrouillage commité
Des serveurs modifiés en place depuis des annéesDes « flocons de neige » que personne n'ose recréerAnsible pour tout changement, ou des images immuables

Checklist d'un projet d'infrastructure

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.

exemples/20/exercice.tf · illustration, non exécuté
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
}
  1. 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é.
  2. 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 ?
  3. 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.
  4. 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 ?
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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