Lección 4 — Gouverner

pre-commit et pipeline CI

7 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

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

files/.pre-commit-config.yaml (extrait)
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 :

JobDéclencheurDroitsContenu
verifierPR et pushcontents: readmake setup, fmt, validate, test, lint, security
planPR+ id-token: write, pull-requests: writeauth WIF, make policy, commentaire du plan
derivechaque matin ouvré, ou manuel+ id-token: write, issues: writedrift.sh, ticket si code 2 (leçon 5)
files/ci/github-actions.yml (extrait)
      - 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.txt

Chaque 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)Skipped

11 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.tfstate

La 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) :

make ci, sortie réelle du 2026-10-16

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 show du plan dans un commentaire public : une variable non marquée sensitive y apparaît en clair. Ici, le jeton est ephemeral et le secret write-only : rien à fuiter. Parade : sensitive = true sur 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-progress pendant qu'il tenait le verrou d'état. Symptôme : la PR suivante échoue sur Error acquiring the state lock. Parade : -lock-timeout=60s sur les plans (fait dans make 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-verify saute 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 ?

Vous voulez que la CI échoue si un développeur oublie de lancer terraform fmt. Où placez-vous le contrôle ?

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 make que 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.
Siguiente