Aller au contenu
En équipe et en intégration continue

En équipe et en intégration continue

À la fin, vous saurez

  • Décrire le flux de travail d'une équipe sur une infrastructure en code, de la branche à l'application
  • Automatiser les vérifications qui ne demandent aucun identifiant : formatage, validation, lint, analyse de configuration
  • Publier le plan dans la demande de fusion, et appliquer après fusion derrière un environnement protégé
  • Séparer l'identité qui planifie de celle qui applique, et celle du backend de celle du fournisseur
  • Empêcher deux applications simultanées, et détecter la dérive chaque jour
  • Expliquer pourquoi un plan enregistré est aussi sensible qu'un état

Prérequis

Testé avec checkout 7.0.1 setup-opentofu 2.0.2 setup-terraform 4.0.1 setup-tflint 6.3.2 terraform 1.16.1 trivy-action 0.36.0 , vérifié le 5 octobre 2026

Pourquoi

Les leçons précédentes ont été écrites du point de vue d'une seule personne, à son poste, qui lance plan puis apply. Dans l'équipe de Signalements, ils sont quatre, plus un pipeline. Sans règles communes, les incidents typiques arrivent vite :

  • Deux applications concurrentes. Camille applique une modification du répartiteur pendant que Dominique applique un changement de type d'instance. Le verrou d'état (leçon 7) empêche la corruption, mais l'un des deux a appliqué un plan calculé sur un état qui n'existe plus.
  • L'apply depuis une branche. Une modification jamais fusionnée est appliquée « pour tester » en production. Le dépôt dit une chose, l'infrastructure une autre, et le prochain apply depuis main défait le test sans prévenir.
  • Le plan que personne n'a lu. La revue de code porte sur le diff du .tf, qui ajoute trois lignes. Le plan, lui, annonçait le remplacement de la base, à cause d'un argument qui force la recréation.
  • Les clés sur les postes. Chaque membre de l'équipe a une clé d'API capable de tout détruire dans le projet de production, pour pouvoir lancer apply.

L'intégration continue appliquée à l'infrastructure répond à ces quatre problèmes à la fois : les vérifications tournent sur chaque demande de fusion, le plan est publié dans la demande de fusion et relu comme le code, et seul le pipeline, après fusion, applique, avec la seule clé qui écrit. C'est la transposition à Terraform des principes du cours CI/CD : les principes.

Les concepts

Le flux de travail

    flowchart LR
  B["branche"] --> PR["demande de fusion"]
  PR --> V["vérifier<br/>fmt, validate,<br/>TFLint, Trivy"]
  PR --> P["plan par environnement<br/>(lecture seule)<br/>commenté dans la PR"]
  V --> R["revue du code<br/>et du plan"]
  P --> R
  R --> M["fusion dans main"]
  M --> A1["appliquer preprod"]
  A1 --> A2["appliquer prod<br/>(approbation)"]
  N["chaque jour"] --> D["plan de dérive<br/>-detailed-exitcode"]
  

Trois règles en découlent :

  1. main est la vérité. Ce qui est appliqué est ce qui est fusionné, et rien d'autre. Personne n'applique depuis une branche ni depuis son poste, sauf procédure de secours tracée.
  2. On relit le plan, pas seulement le code. Le diff du code dit ce que l'on veut ; le plan dit ce qui va se passer, compte tenu de l'état réel.
  3. Les identités sont séparées par usage. Planifier demande de lire ; appliquer demande d'écrire ; détecter la dérive demande de lire. Trois usages, deux niveaux de droits.

Ce qui se vérifie sans identifiant

Une partie des contrôles ne demande ni clé d'API ni accès à l'état, et peut tourner sur n'importe quelle demande de fusion, y compris venue d'une bifurcation :

ContrôleCe qu'il attrapeCode de sortie en échec
terraform fmt -check -recursiveun fichier mal formaté3 (constaté avec Terraform 1.16.1)
terraform init -backend=false puis terraform validateune syntaxe ou une référence invalide, un argument inconnu du fournisseur, un type faux1
TFLintdes erreurs que validate laisse passer (variables déclarées et inutilisées, conventions de nommage, versions de fournisseur non contraintes)non nul
Trivy (trivy config)des erreurs de configuration de sécurité connues, d'après un catalogue de règlesréglable par exit-code

TFLint est un analyseur statique de configurations Terraform. Son jeu de règles pour le langage Terraform lui-même est intégré, et active par défaut un préréglage « recommandé ». Les autres jeux de règles officiels visent AWS, Azure et Google Cloud ; au 5 octobre 2026, il n'existe pas de jeu de règles TFLint publié pour le fournisseur Scaleway. Pour Signalements, TFLint vérifie donc le langage, pas les types d'instances.

Trivy, l'analyseur d'Aqua Security, a absorbé tfsec : Aqua a racheté tfsec en 2021, puis fusionné son analyseur dans Trivy, et le dépôt de tfsec publie un guide de migration vers trivy config. Trivy analyse les fichiers .tf, et aussi les plans enregistrés, ce qui lui donne les valeurs résolues. Son catalogue de règles porte surtout sur les grands fournisseurs américains : sur une configuration Scaleway, attendez-vous à peu de résultats spécifiques. Il reste utile pour les règles génériques et pour les autres fichiers du dépôt (Dockerfile, manifestes Kubernetes). Checkov, de Palo Alto Networks, joue le même rôle avec son propre catalogue.

Plan dans la demande de fusion, application après fusion

Le plan d'une demande de fusion s'exécute avec des identifiants de lecture : il interroge l'API pour rafraîchir l'état, et n'écrit rien. Il est publié en commentaire, un par environnement, pour que la revue porte sur les actions réelles.

L'application a lieu après la fusion, sur main, dans un job rattaché à un environnement protégé de GitHub (GitHub Actions, leçon 9) : relecteurs requis pour la production, secrets d'écriture disponibles seulement dans ce job, et seulement après approbation.

Reste une question : appliquer le plan relu dans la demande de fusion, ou recalculer un plan après la fusion ?

  • Appliquer le plan enregistré (terraform plan -out=tfplan, puis terraform apply tfplan) garantit que l'on applique exactement ce qui a été lu. Mais entre la relecture et la fusion, d'autres demandes ont pu être fusionnées et appliquées : l'état a changé, et Terraform refuse alors le plan (Saved plan is stale). Le tutoriel de HashiCorp sur l'automatisation ajoute que le plan et l'application sur des machines différentes exigent de conserver le répertoire de travail, les mêmes fournisseurs, le même système et la même architecture.
  • Recalculer le plan après la fusion, dans le job d'application, est plus simple et toujours à jour. Le risque : le plan appliqué peut différer de celui qui a été relu, si quelque chose a changé entre-temps.

Le workflow de cette leçon recalcule, et réduit l'écart par deux précautions : l'application est sérialisée et passe d'abord par la préproduction, et la branche main exige que les demandes soient à jour avant fusion, ce qui force un nouveau plan en demande de fusion après chaque fusion concurrente. Les outils dédiés (plus bas) appliquent, eux, le plan relu.

Des identités par usage

Scaleway ne propose pas de fédération d'identité pour les pipelines : pas d'équivalent de l'OIDC de GitHub vers AWS ou Google Cloud (cours cloud, leçon 7 ; GitHub Actions, leçon 10). Le pipeline utilise donc des clés d'API, rangées en secrets d'environnement, et l'on compense par leur nombre et leur périmètre :

IdentitéUsageDroits ScalewayOù vit la clé
tf-plan-preprod, tf-plan-prodplan en demande de fusion, détection de dérivelecture sur le projet de l'environnementenvironnement GitHub plan-preprod, plan-prod
tf-apply-preprod, tf-apply-prodapplication après fusionécriture sur le projet de l'environnementenvironnement GitHub preprod, prod (protégé)
tf-etat-...lecture et écriture de l'état dans Object Storageobjets du bucket d'étatà côté de chacune des précédentes

La séparation entre l'identité du backend et celle du fournisseur vient de la leçon 7 : le backend lit AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY, le fournisseur SCW_ACCESS_KEY et SCW_SECRET_KEY. Le plan en lecture seule a quand même besoin de lire l'état, qui contient toute la carte de l'infrastructure : ce n'est pas une clé anodine.

Les jeux de permissions exacts d'une identité « lecture seule qui sait faire un plan » dépendent des ressources : un plan relit chaque ressource par son API. Partez d'un jeu de lecture sur tous les produits du projet, lancez un plan, et ajoutez ce qui manque d'après les erreurs 403 ; Audit Trail (Scaleway en pratique, leçon 15) montre les appels refusés.

Sérialiser

Le verrou d'état empêche deux écritures simultanées de l'état ; il ne range pas les jobs dans un ordre. Dans GitHub Actions, la clé concurrency fait attendre un job tant qu'un autre du même groupe tourne ; avec cancel-in-progress: false, aucun n'est annulé. Un groupe par environnement (terraform-prod) suffit : deux fusions rapprochées s'appliquent l'une après l'autre.

En pratique

Préparer le dépôt

Deux fichiers s'ajoutent au dépôt signalements-iac. La configuration de TFLint, qui active explicitement le jeu de règles intégré :

# .tflint.hcl
plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

Et le workflow, .github/workflows/infrastructure.yml, détaillé job par job ensuite :

name: Infrastructure

on:
  pull_request:
    paths: ["*.tf", "*.tftpl", "*.hcl", "environnements/**", ".github/workflows/infrastructure.yml"]
  push:
    branches: [main]
    paths: ["*.tf", "*.tftpl", "*.hcl", "environnements/**", ".github/workflows/infrastructure.yml"]
  schedule:
    - cron: "17 6 * * 1-5"        # dérive : chaque jour ouvré à 6 h 17 UTC
  workflow_dispatch:

permissions:
  contents: read

env:
  TF_IN_AUTOMATION: "1"
  TF_INPUT: "0"
  TERRAFORM_VERSION: "1.16.1"

jobs:
  verifier:
    if: github.event_name != 'schedule'
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
          terraform_wrapper: false
      - run: terraform fmt -check -recursive -diff
      - run: terraform init -backend=false
      - run: terraform validate
      - uses: terraform-linters/setup-tflint@6ffdbaa3be476b3431f7275c26966ef32ff60337 # v6.3.2
      - run: tflint --init && tflint --format compact
      - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          scan-type: config
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: "1"

  plan:
    needs: verifier
    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        environnement: [preprod, prod]
    environment: plan-${{ matrix.environnement }}
    permissions:
      contents: read
      pull-requests: write
    env:
      SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
      SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
      AWS_ACCESS_KEY_ID: ${{ secrets.ETAT_ACCESS_KEY }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.ETAT_SECRET_KEY }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
          terraform_wrapper: false
      - run: terraform init -backend-config=backend-${{ matrix.environnement }}.hcl
      - name: Plan (sans verrou, lecture seule)
        run: |
          terraform plan -lock=false -no-color \
            -var-file="environnements/${{ matrix.environnement }}.tfvars" \
            -out=tfplan > plan.txt
          terraform show -no-color tfplan > affichage.txt
      - name: Commenter la demande de fusion
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
        env:
          ENVIRONNEMENT: ${{ matrix.environnement }}
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('affichage.txt', 'utf8');
            const extrait = plan.length > 60000 ? plan.slice(0, 60000) + '\n[plan tronqué]' : plan;
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `### Plan ${process.env.ENVIRONNEMENT}\n\n\`\`\`text\n${extrait}\n\`\`\``,
            });

  appliquer:
    needs: verifier
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-24.04
    strategy:
      max-parallel: 1
      matrix:
        environnement: [preprod, prod]
    environment: ${{ matrix.environnement }}
    concurrency:
      group: terraform-${{ matrix.environnement }}
      cancel-in-progress: false
    env:
      SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
      SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
      AWS_ACCESS_KEY_ID: ${{ secrets.ETAT_ACCESS_KEY }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.ETAT_SECRET_KEY }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
          terraform_wrapper: false
      - run: terraform init -backend-config=backend-${{ matrix.environnement }}.hcl
      - run: |
          terraform plan -no-color \
            -var-file="environnements/${{ matrix.environnement }}.tfvars" -out=tfplan
          terraform apply -no-color tfplan

  derive:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        environnement: [preprod, prod]
    environment: plan-${{ matrix.environnement }}
    env:
      SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
      SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
      AWS_ACCESS_KEY_ID: ${{ secrets.ETAT_ACCESS_KEY }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.ETAT_SECRET_KEY }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
          terraform_wrapper: false
      - run: terraform init -backend-config=backend-${{ matrix.environnement }}.hcl
      - name: Détecter la dérive
        run: |
          set +e
          terraform plan -lock=false -no-color -detailed-exitcode \
            -var-file="environnements/${{ matrix.environnement }}.tfvars"
          code=$?
          if [ "$code" -eq 2 ]; then
            echo "::error::Dérive détectée en ${{ matrix.environnement }}"
          fi
          exit "$code"

Le fichier a été vérifié par un analyseur YAML. Les actions sont épinglées par empreinte de commit, avec la version en commentaire (GitHub Actions, leçon 11) ; les empreintes ci-dessus correspondent aux dernières versions publiées au 5 octobre 2026, lues par l'API de GitHub. Faites-les tenir à jour par Dependabot ou Renovate plutôt qu'à la main.

Le job verifier

Il tourne sur chaque demande de fusion et chaque poussée sur main, sans aucun secret :

  • setup-terraform avec terraform_wrapper: false. Par défaut, l'action installe un enrobage autour du binaire, qui capture sa sortie dans des sorties d'étape (stdout, stderr, exitcode). Pratique pour réutiliser la sortie d'un plan, il modifie le comportement de la commande (sa sortie standard est interceptée) et surprend dans les redirections : on le désactive et l'on redirige soi-même.
  • terraform fmt -check -recursive -diff échoue si un fichier n'est pas formaté, et montre la correction. Sur un fichier volontairement mal indenté :
$ terraform fmt -check -diff
zone.tf
--- old/zone.tf
+++ new/zone.tf
@@ -1,4 +1,4 @@
 variable "zone" {
-type=string
-  default    = "fr-par-1"
+  type    = string
+  default = "fr-par-1"
 }
$ echo $?
3
  • terraform init -backend=false installe les fournisseurs d'après le fichier de verrouillage, sans toucher au backend ni demander d'identifiant ; terraform validate vérifie ensuite la configuration contre leur schéma.
  • TFLint télécharge les jeux de règles déclarés (--init) puis analyse le répertoire ; --format compact donne une ligne par problème, lisible dans le journal.
  • Trivy en mode config, avec severity: HIGH,CRITICAL et exit-code: "1" : le job échoue sur une erreur grave, et seulement sur celles-là. Commencez avec un seuil élevé, qui ne fait pas échouer pour des broutilles, et descendez-le une fois le dépôt propre.

TF_IN_AUTOMATION et TF_INPUT sont posés pour tout le workflow : le premier, selon le tutoriel de HashiCorp, ajuste la sortie pour ne pas suggérer de commandes à taper ; le second interdit toute question interactive, qui bloquerait le job jusqu'à son délai maximal.

Le job plan

Il ne tourne que pour une demande de fusion venue du dépôt lui-même (head.repo.full_name == github.repository) : une demande venue d'une bifurcation ne reçoit de toute façon pas les secrets avec l'événement pull_request, et son plan échouerait. Pour chaque environnement de la matrice :

  • environment: plan-${{ matrix.environnement }} donne accès aux clés de lecture de l'environnement. Ces environnements de plan n'ont pas de relecteur requis, et ne peuvent pas avoir de règle de branche limitée à main : depuis décembre 2025, pour une demande de fusion, GitHub compare la règle à la référence refs/pull/<n>/merge (GitHub Actions, leçon 9).
  • terraform init -backend-config=backend-${{ matrix.environnement }}.hcl : la configuration partielle de la leçon 7, un fichier par environnement.
  • terraform plan -lock=false : le plan d'une demande de fusion ne prend pas le verrou, pour ne jamais bloquer une application. Il n'écrit pas l'état ; au pire, il lit un état en cours de modification, et le commentaire sera périmé de quelques minutes.
  • terraform show -no-color tfplan produit l'affichage relu, et actions/github-script le publie en commentaire, tronqué à 60 000 caractères pour rester sous la limite de taille d'un commentaire de GitHub. Les valeurs marquées sensibles y apparaissent comme (sensitive value), et les arguments en écriture seule n'y sont pas du tout.

Warning

Une demande de fusion modifie aussi le workflow. Quelqu'un qui peut ouvrir une demande de fusion depuis une branche du dépôt peut donc écrire un workflow qui lit les secrets de l'environnement de plan et les envoie ailleurs. C'est pourquoi les clés de plan sont des clés de lecture, limitées au projet de l'environnement, et pourquoi n'importe qui ne doit pas pouvoir pousser de branche sur le dépôt d'infrastructure. N'utilisez jamais pull_request_target pour exécuter le code d'une demande de fusion avec des secrets (GitHub Actions, leçon 11).

Le job appliquer

Il ne tourne que sur une poussée dans main, c'est-à-dire après une fusion :

  • La matrice [preprod, prod] avec max-parallel: 1 applique la préproduction, puis la production. Si la préproduction échoue, la production ne commence pas : fail-fast vaut true par défaut.
  • environment: ${{ matrix.environnement }} : l'environnement prod exige l'approbation d'une personne de l'équipe, différente de celle qui a fusionné ; ses secrets (clé d'écriture) ne sont injectés qu'après.
  • concurrency: terraform-${{ matrix.environnement }}, sans annulation : deux fusions rapprochées s'appliquent l'une après l'autre, chacune avec un plan recalculé sur l'état laissé par la précédente.
  • Plan puis application du plan enregistré, dans la même étape. Le plan enregistré n'est pas une formalité : s'il s'était écoulé un instant de trop et que l'état avait changé entre les deux commandes, Terraform refuserait d'appliquer.

Ce refus se démontre localement, avec le fournisseur local. Un plan est enregistré, puis une autre application modifie l'état, puis on tente d'appliquer le premier plan :

$ terraform plan -var version_signalements=1.3.0 -out=tfplan
...
Saved the plan to: tfplan
$ terraform apply -auto-approve -var version_signalements=1.2.1
$ terraform apply tfplan

Error: Saved plan is stale

The given plan file can no longer be applied because the state was changed by
another operation after the plan was created.

Le job derive

Planifié chaque jour ouvré à 6 h 17 UTC (une minute choisie hors des heures rondes, où les planifications de GitHub sont les plus retardées), et déclenchable à la main, il lance pour chaque environnement le plan de dérive de la leçon 11 : -lock=false, -detailed-exitcode, identités de lecture. Un code 2 fait échouer le job avec une annotation d'erreur ; la notification d'échec de GitHub prévient l'équipe. Un code 1 signale une panne de la surveillance elle-même, à traiter aussi.

Avec OpenTofu

Le même workflow fonctionne avec OpenTofu en remplaçant l'action d'installation et la commande :

      - uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
        with:
          tofu_version: ${{ env.TOFU_VERSION }}
          tofu_wrapper: false
      - run: tofu init -backend-config=backend-${{ matrix.environnement }}.hcl

(avec une variable TOFU_VERSION déclarée dans env, comme TERRAFORM_VERSION). L'action setup-opentofu expose les entrées tofu_version, tofu_version_file et tofu_wrapper, symétriques de celles de setup-terraform. Fixez la version, comme pour Terraform : une mise à jour de l'outil doit être une demande de fusion, pas une surprise.

Les outils dédiés

Un workflow GitHub Actions suffit à une équipe de quelques personnes. Au-delà, des outils spécialisés reprennent le même flux avec plus de garanties :

  • Atlantis, logiciel libre auto-hébergé, exécute plan et apply sur commentaire dans la demande de fusion (atlantis plan, atlantis apply), verrouille chaque répertoire tant que la demande est ouverte, et applique le plan relu avant la fusion.
  • HCP Terraform (HashiCorp), Spacelift, env0, Scalr : des services qui exécutent Terraform ou OpenTofu, gardent l'état, appliquent des politiques et détectent la dérive. À évaluer avec la grille du cours Cloud souverain : l'outil qui exécute vos plans voit tout votre état.

Sous le capot

Ce que contient un plan enregistré. Un fichier de plan est une archive ZIP. Celle du test précédent contient :

$ unzip -l tfplan
Archive:  tfplan
  Length      Date    Time    Name
---------  ---------- -----   ----
     1216  2026-10-05 12:56   tfplan
     1637  2026-10-05 12:56   tfstate
     1601  2026-10-05 12:56   tfstate-prev
      317  2026-10-05 12:56   tfconfig/m-/main.tf
       41  2026-10-05 12:56   tfconfig/modules.json
     1257  2026-10-05 12:56   .terraform.lock.hcl
---------                     -------
     6069                     6 files

Le plan proprement dit (tfplan), mais aussi une copie de l'état avant et après rafraîchissement (tfstate, tfstate-prev), la configuration (tfconfig/) et le fichier de verrouillage des fournisseurs. C'est ce qui permet à apply de vérifier que rien n'a bougé (l'état de référence) et d'appliquer sans relire la configuration. C'est aussi pourquoi la documentation de HashiCorp traite les plans comme les états : ils contiennent les valeurs sensibles, y compris celles marquées sensitive.

Pourquoi un artefact de plan est un risque. Téléverser le plan comme artefact de GitHub Actions, pour l'appliquer dans un autre job, met une copie de l'état à disposition de toute personne qui peut lire les artefacts du dépôt. Si vous choisissez ce modèle, réduisez la conservation au minimum (retention-days: 1), restreignez la lecture du dépôt, et, avec OpenTofu, chiffrez le plan : OpenTofu chiffre au repos l'état et les plans, avec une clé dérivée d'une phrase de passe (PBKDF2) ou fournie par un gestionnaire de clés. Sa documentation prévient qu'un état ou un plan chiffré devient irrécupérable sans la clé.

Ce que fait concurrency. GitHub garde au plus un job en cours et un job en attente par groupe. Un troisième job du même groupe, arrivé pendant qu'un deuxième attend, remplace celui qui attend, qui est annulé même avec cancel-in-progress: false. Pour l'application d'infrastructure, c'est acceptable : le job qui reste appliquera main dans son dernier état, qui inclut les changements du job annulé.

Pièges courants

Le plan vert, l'apply rouge. Le plan réussit avec la clé de lecture, l'apply échoue avec 403 sur une ressource : la clé d'écriture n'a pas le jeu de permissions d'un produit ajouté récemment. Testez chaque nouvelle ressource jusqu'à l'apply en préproduction avant la production.

L'enrobage de setup-terraform. Actif par défaut, il intercepte la sortie et le code de retour de chaque commande terraform pour les exposer en sorties d'étape. Les redirections et les tests de code de sortie écrits pour un poste ne se comportent alors plus tout à fait de la même façon. terraform_wrapper: false rend à la commande son comportement ordinaire.

-lock=false à l'apply. Désactiver le verrou est acceptable pour un plan en lecture ; à l'apply, c'est supprimer la seule protection contre deux écritures simultanées.

La version de Terraform qui flotte. terraform_version: latest fait changer l'outil sans demande de fusion ; un état écrit par une version plus récente peut ne plus être lisible par une plus ancienne. Fixez la version dans le workflow et dans required_version.

Le commentaire de plan qui fuit. Un output non marqué sensitive, une valeur dans un local affiché par une ressource : tout ce qui apparaît dans terraform show finit dans un commentaire, visible de tous ceux qui lisent le dépôt.

Les fichiers de variables oubliés par le filtre paths. Une modification de environnements/prod.tfvars qui ne déclenche pas le workflow s'appliquera, plus tard, avec la modification suivante. Le filtre doit couvrir tout ce qui change le plan : .tf, modèles, fichiers de variables et de backend.

L'application en dehors du pipeline. Une personne qui applique depuis son poste « parce que le pipeline est lent » crée exactement la dérive que le job quotidien signalera le lendemain. Retirez les droits d'écriture des postes.

Sécurité

  • Le pipeline est la cible. La clé d'écriture de production peut tout détruire dans le projet : elle ne vit que dans l'environnement protégé prod, n'est injectée qu'après approbation, et expire (cours cloud, leçon 7). Faites-la tourner à date fixe, et à chaque départ d'une personne qui l'a manipulée.
  • L'état et le plan sont des secrets. Le bucket d'état a ses propres droits ; les plans enregistrés ne sortent pas du job qui les crée, ou sont chiffrés. Les arguments en écriture seule (leçon 10) réduisent ce qu'ils contiennent.
  • Les demandes de fusion depuis une bifurcation ne reçoivent pas les secrets avec pull_request, et c'est voulu. Les vérifications sans identifiant (job verifier) leur suffisent ; le plan sera produit après reprise de la branche dans le dépôt par une personne de l'équipe.
  • La chaîne d'approvisionnement du pipeline. Les actions sont épinglées par empreinte, les fournisseurs Terraform par le fichier de verrouillage, qui contient leurs sommes de contrôle (leçon 4). Une mise à jour de l'un ou de l'autre passe par une demande de fusion.
  • Les analyseurs ne remplacent pas la revue. Trivy et TFLint attrapent des erreurs connues ; une règle de groupe de sécurité ouverte sur le port de la base, propre à votre architecture, ne se voit qu'à la lecture du plan.

En production

  • Protéger main : demande de fusion obligatoire, job verifier et jobs plan requis, branche à jour avant fusion, au moins une approbation, et pas de contournement par les administrateurs.
  • Un dépôt, plusieurs états. Quand l'infrastructure grossit, on la découpe (réseau, données, applications) en configurations séparées avec leurs propres états, pour réduire la durée des plans et le rayon d'impact d'une erreur. Le workflow devient une matrice de répertoires : c'est le moment où Atlantis ou un service dédié deviennent rentables.
  • Mesurer. La durée du plan, le nombre de demandes de fusion d'infrastructure par semaine, les jours avec dérive et le temps de résorption disent si le flux tient.
  • Chez Lyneko, le site que vous lisez se déploie par un pipeline qui pousse une image et écrit son étiquette dans Git, qu'Argo CD applique : la même séparation entre ce qui vérifie, ce qui décide et ce qui écrit, appliquée à l'application plutôt qu'à l'infrastructure (GitOps avec Argo CD).

Exercices

1. Lire un échec (niveau 200). Le job verifier échoue à l'étape terraform fmt -check -recursive -diff avec le code 3. Que s'est-il passé, et comment l'éviter ?

Solution

Au moins un fichier n'est pas au format canonique ; la sortie montre le diff de la correction. On lance terraform fmt -recursive en local et l'on pousse le résultat. Pour ne plus y penser : formatage à l'enregistrement dans l'éditeur, ou un crochet pre-commit qui lance terraform fmt.

2. Appliquer le plan relu (niveau 200). Modifiez le workflow pour que la production applique exactement le plan produit après la fusion et approuvé, plutôt qu'un plan recalculé après l'approbation. Quelles précautions prenez-vous ?

Solution

Un job plan-prod sur main (environnement de plan, sans approbation) produit tfplan et terraform show, publie l'affichage dans le résumé du job ($GITHUB_STEP_SUMMARY), et téléverse le plan comme artefact avec retention-days: 1. Un job appliquer-prod, qui en dépend (needs), rattaché à l'environnement protégé prod, télécharge l'artefact, refait terraform init avec le même fichier de verrouillage et la même version de Terraform, puis terraform apply tfplan. Les relecteurs approuvent en ayant lu le résumé. Précautions : le plan contient une copie de l'état, donc conservation minimale et lecture du dépôt restreinte (ou chiffrement avec OpenTofu) ; même système et même architecture pour les deux jobs ; et si l'état a bougé entre-temps, Terraform refuse (Saved plan is stale), ce qui est le comportement voulu.

3. Les droits du plan (niveau 200). Pourquoi le job plan a-t-il besoin d'accéder à l'état, et que pourrait faire une personne malveillante avec la clé correspondante ?

Solution

Le plan compare la configuration à l'état, qu'il lit dans le backend, puis rafraîchit chaque ressource par l'API. La clé du backend lui donne donc la lecture de l'état entier : la carte complète de l'infrastructure (adresses, identifiants, configuration) et toutes les valeurs que l'état contient, y compris sensibles, sauf les arguments en écriture seule. Si cette clé avait aussi l'écriture sur le bucket, elle pourrait corrompre ou remplacer l'état. D'où une clé de backend limitée au bucket d'état, en lecture pour le plan si possible, et des valeurs secrètes gardées hors de l'état.

4. Concevoir la protection de main (niveau 200). Listez les réglages de protection de branche et d'environnement que vous activez pour ce dépôt, et ce que chacun empêche.

Solution

Branche main : demande de fusion obligatoire (pas de poussée directe qui contournerait la revue) ; vérifications requises verifier et plan (pas de fusion d'un code invalide ou d'un plan non produit) ; branche à jour avant fusion (le plan relu tient compte des fusions précédentes) ; une approbation au moins, pas par l'auteur ; pas de contournement par les administrateurs. Environnement prod : relecteurs requis, auto-approbation interdite (une personne ne déploie pas seule en production) ; branche autorisée main seulement (aucun autre déclencheur ne reçoit la clé d'écriture). Environnement preprod : branche main seulement, sans relecteur, pour aller vite.

Récapitulatif

  • main est la vérité : on vérifie et on planifie en demande de fusion, on applique après fusion, depuis le pipeline seulement.
  • Sans identifiant : fmt -check (code 3 en échec), init -backend=false et validate, TFLint (jeu de règles du langage, pas de règles Scaleway), Trivy (trivy config, héritier de tfsec).
  • Le plan de chaque environnement est publié dans la demande de fusion, avec des clés de lecture et -lock=false ; on relit le plan, pas seulement le code.
  • L'application passe par un environnement protégé, sérialisée par concurrency, préproduction d'abord ; un plan enregistré devenu périmé est refusé.
  • Sans fédération chez Scaleway : des clés par usage (plan, apply, état), dans des secrets d'environnement, limitées au projet, qui expirent.
  • Un plan enregistré contient l'état et la configuration : il se protège comme l'état ; OpenTofu sait chiffrer les deux.
  • La dérive se détecte chaque jour avec -detailed-exitcode.
  • Au-delà de quelques personnes : Atlantis ou un service dédié, choisis en connaissance de ce qu'ils voient.

Pour aller plus loin

  • Le tutoriel de HashiCorp Running Terraform in Automation, pour les détails du plan et de l'application sur des machines différentes.
  • La documentation d'Atlantis, pour le flux par commentaires et le verrouillage par demande de fusion.
  • La documentation du chiffrement de l'état et des plans d'OpenTofu.
  • Le cours Terraform avancé : modules, tests, état, qui ajoute à ce pipeline les tests (terraform test) et les modules versionnés.
Voir ma constellation →

Sources