En équipe et en intégration continue
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
applydepuismaindé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 :
mainest 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.- 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.
- 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ôle | Ce qu'il attrape | Code de sortie en échec |
|---|---|---|
terraform fmt -check -recursive | un fichier mal formaté | 3 (constaté avec Terraform 1.16.1) |
terraform init -backend=false puis terraform validate | une syntaxe ou une référence invalide, un argument inconnu du fournisseur, un type faux | 1 |
| TFLint | des 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ègles | ré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, puisterraform 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é | Usage | Droits Scaleway | Où vit la clé |
|---|---|---|---|
tf-plan-preprod, tf-plan-prod | plan en demande de fusion, détection de dérive | lecture sur le projet de l'environnement | environnement GitHub plan-preprod, plan-prod |
tf-apply-preprod, tf-apply-prod | application après fusion | écriture sur le projet de l'environnement | environnement GitHub preprod, prod (protégé) |
tf-etat-... | lecture et écriture de l'état dans Object Storage | objets 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-terraformavecterraform_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=falseinstalle les fournisseurs d'après le fichier de verrouillage, sans toucher au backend ni demander d'identifiant ;terraform validatevé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 compactdonne une ligne par problème, lisible dans le journal. - Trivy en mode
config, avecseverity: HIGH,CRITICALetexit-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érencerefs/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 tfplanproduit l'affichage relu, etactions/github-scriptle 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]avecmax-parallel: 1applique la préproduction, puis la production. Si la préproduction échoue, la production ne commence pas :fail-fastvauttruepar défaut. environment: ${{ matrix.environnement }}: l'environnementprodexige 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
planetapplysur 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 (jobverifier) 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, jobverifieret jobsplanrequis, 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
mainest 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=falseetvalidate, 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.
Sources
- HashiCorp, Running Terraform in Automation (tutoriel)
- HashiCorp, Terraform : données sensibles dans l'état et les plans
- hashicorp/setup-terraform, action.yml (v4.0.1)
- opentofu/setup-opentofu, action.yml (v2.0.2)
- terraform-linters/tflint, README : jeux de règles officiels
- Trivy, documentation : analyse des erreurs de configuration et des plans Terraform
- aquasecurity/tfsec, guide de migration de tfsec vers Trivy
- OpenTofu, documentation : chiffrement de l'état et des plans
- Atlantis, documentation