Les contrôles des trois leçons précédentes existent, mais chacun les lance quand il y pense. Résultat observé sur la plateforme : une PR sur deux échoue en CI sur terraform fmt, après huit minutes d'attente, et un terraform.tfstate local a été commité une fois, avec la liste complète des ressources et leurs identifiants. Les contrôles doivent tourner seuls, au bon endroit : les rapides avant le commit, les lents et ceux qui ont besoin du cloud dans la PR.
Le mécanisme
sequenceDiagram participant D as "Développeur" participant PC as "pre-commit (poste)" participant GH as "GitHub Actions" participant GCP as "Google Cloud (WIF)" D->>PC: git commit PC->>PC: fmt, validate, tflint, conftest verify, garde .tfstate (11 s) PC-->>D: refus si un contrôle échoue D->>GH: push + pull request GH->>GH: job verifier : fmt, validate, test, lint, checkov GH->>GCP: jeton OIDC échangé contre un jeton court (aucune clé JSON) GH->>GCP: plan dev + prod (lecture, verrou d'état) GH->>GH: conftest + checkov sur les plans GH-->>D: un commentaire de PR mis à jour : verdict + plans
Trois principes :
- Mêmes commandes partout. pre-commit et la CI appellent
make validate,make lint,make test: pas une deuxième définition des contrôles qui divergerait de la première. - Pas de secret de longue durée. La CI s'authentifie par fédération d'identité de charge de travail (Workload Identity Federation) : GitHub émet un jeton OIDC signé pour ce dépôt et cette exécution, Google l'échange contre un jeton d'accès d'une heure pour un compte de service. Aucune clé JSON à stocker ni à faire tourner.
- Le plan de la PR est lu par un humain. Il est commenté dans la PR, avec le verdict des politiques, et le même commentaire est mis à jour à chaque push au lieu d'en empiler dix.
La mise en œuvre
pre-commit
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
- repo: local
hooks:
- id: terraform-validate
name: terraform validate
entry: make validate
language: system
files: \.tf$
pass_filenames: false
- id: interdire-etat-terraform
name: aucun état ni plan Terraform commité
entry: "sh -c 'echo \"Fichier interdit dans git - $*\"; exit 1' --"
language: system
files: (\.tfstate(\.backup)?|\.tfplan)$Les hooks local utilisent les outils déjà installés par make setup, aux mêmes versions que la CI. Le dernier refuse tout fichier d'état ou de plan, même forcé par git add -f au-delà du .gitignore. Installation : pre-commit install dans le dépôt.
Le pipeline
ci/github-actions.yml est à copier en .github/workflows/terraform.yml (la plateforme de formation ne sert pas les dossiers cachés). Trois jobs :
| Job | Déclencheur | Droits | Contenu |
|---|---|---|---|
verifier | PR et push | contents: read | make setup, fmt, validate, test, lint, security |
plan | PR | + id-token: write, pull-requests: write | auth WIF, make policy, commentaire du plan |
derive | chaque matin ouvré, ou manuel | + id-token: write, issues: write | drift.sh, ticket si code 2 (leçon 5) |
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: ${{ vars.GCP_SA_PLAN }} # lecture seule + écriture du verrou d'état
- name: Plans et politiques
id: policy
run: |
make policy PROJECT="${{ vars.GCP_PROJECT }}" STATE_BUCKET="${{ vars.TF_STATE_BUCKET }}" \
TICKETING_TOKEN="${{ secrets.TICKETING_TOKEN }}" 2>&1 | tee policy.txtChaque action est épinglée par SHA de commit, la version en commentaire : un tag peut être déplacé vers un autre code par quiconque contrôle le dépôt de l'action, un SHA non. Le compte GCP_SA_PLAN n'a que des droits de lecture, plus l'écriture des objets de verrou dans le bucket d'état : la PR d'un contributeur ne peut rien appliquer. L'apply (absent de ce fichier) se fait après fusion, avec un autre compte, sur l'environnement protégé de GitHub.
concurrency: cancel-in-progress annule le plan d'un push précédent sur la même branche. Il ne doit jamais s'appliquer à un job d'apply : annuler un apply en cours laisse un verrou et un état partiel.
Résultat obtenu (2026-10-16)
pre-commit sur une copie du dépôt (git init, puis pre-commit run --all-files) :
check for merge conflicts................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
detect private key.......................................................Passed
check for added large files..............................................Passed
terraform fmt............................................................Passed
terraform validate.......................................................Passed
tflint...................................................................Passed
tests des politiques.....................................................Passed
aucun état ni plan Terraform commité.................(no files to check)Skipped11 s. Puis un terraform.tfstate ajouté avec git add -f :
aucun état ni plan Terraform commité.....................................Failed
- hook id: interdire-etat-terraform
- exit code: 1
Fichier interdit dans git - infra/envs/dev/terraform.tfstateLa chaîne complète que le pipeline enchaîne, exécutée en local avec make ci (27 s, plans réels sur le bucket d'état) :
Le fichier du pipeline passe actionlint 1.7.12 sans remarque, et chaque SHA épinglé a été vérifié contre le tag annoncé (git ls-remote). Il n'a pas été exécuté sur GitHub : ce lab n'a ni dépôt GitHub ni fournisseur WIF configurés.
Ce qui casse en production
- Le plan qui fuit un secret.
terraform showdu plan dans un commentaire public : une variable non marquéesensitivey apparaît en clair. Ici, le jeton estephemeralet le secretwrite-only: rien à fuiter. Parade :sensitive = truesur toute variable secrète, et relire un commentaire de plan avant d'ouvrir le dépôt. - Le verrou pris par la CI annulée. Un plan annulé par
cancel-in-progresspendant qu'il tenait le verrou d'état. Symptôme : la PR suivante échoue surError acquiring the state lock. Parade :-lock-timeout=60ssur les plans (fait dansmake plan) ; le verrou d'un plan interrompu se lève après vérification, comme vu au projet précédent. - Le hook contourné.
git commit --no-verifysaute pre-commit. Parade : la CI refait tous les contrôles ; pre-commit sert à gagner du temps, pas de garde-fou. - Commentaire tronqué. Un plan de prod de 4 000 lignes dépasse la limite d'un commentaire GitHub (65 536 caractères). Parade : tronquer avec un marqueur (fait à 25 000 caractères par plan) et joindre le plan complet en artefact du job.
La règle de décision
Avant le commit : ce qui prend moins de 15 s et n'a besoin d'aucun identifiant. Dans la PR : tout le reste, plan réel compris, avec un compte en lecture seule. Après fusion : l'apply, avec un autre compte et une approbation. Aucune clé JSON dans la CI : la fédération d'identité, ou rien.
Pratique
Un contributeur externe ouvre une PR qui modifie le workflow pour lancer terraform apply avec le compte de la CI. Qu'est-ce qui l'en empêche dans cette configuration ?
La défense en profondeur ici, c'est le moindre privilège du compte : même un workflow modifié ne peut pas créer ou détruire. (GitHub protège aussi les PR venant de forks : sans approbation, elles n'ont ni secrets ni jeton OIDC.) L'épinglage protège contre une action compromise, pas contre un workflow modifié ; pre-commit tourne sur le poste du contributeur.
Vous voulez que la CI échoue si un développeur oublie de lancer terraform fmt. Où placez-vous le contrôle ?
pre-commit économise un aller-retour, mais il se contourne (--no-verify) ou n'est pas installé. La CI est la seule garantie. Le job plan est plus lent et demande des identifiants : y mettre fmt retarde l'échec pour rien.
Exercice. Ajoutez au job verifier une étape qui échoue si un fichier *.tfstate ou *.tfplan est suivi par git, pour le cas où pre-commit a été contourné.
Voir la solution
- name: Aucun état ni plan versionné
run: |
if git ls-files | grep -E '\.(tfstate|tfstate\.backup|tfplan)$'; then
echo "Fichiers d'état ou de plan versionnés : à retirer (git rm --cached) et à faire tourner si un secret y figurait"
exit 1
fiÀ placer juste après actions/checkout. git ls-files liste les fichiers suivis, y compris ceux ajoutés avec -f. Vérifiez la syntaxe avec actionlint ci/github-actions.yml.
- J'installe pre-commit avec des hooks locaux qui appellent les mêmes cibles
makeque la CI. - J'authentifie une CI sur Google Cloud par fédération d'identité, sans clé JSON.
- J'épingle les actions par SHA et je sais vérifier qu'un SHA correspond au tag.
- Je commente le plan et le verdict des politiques dans la PR, avec un compte en lecture seule.