Terraform — Intégration CI/CD

De wiki.nexiat.fr
Aller à la navigation Aller à la recherche
Fiche express
Type Intégration de Terraform dans un pipeline CI/CD
Motif type plan auto en MR (commenté) · apply manuel/gated sur branche principale
Credentials OIDC / identité fédérée, pas de clé statique
Point critique verrouillage du state (pas de plan/apply concurrents)
Voir aussi GitLab CI/CD · Terraform — Concepts fondamentaux · Terraform — Tests et validation

Faire tourner Terraform depuis un poste de travail individuel ne passe pas à l'échelle en équipe : chacun a une version différente du binaire, du state local ou d'identifiants divergents, et rien n'empêche deux personnes de lancer un apply en même temps. Cette page décrit le motif standard d'intégration dans une CI, avec un exemple concret sur GitLab CI/CD.

Motif type : plan en MR, apply gated sur la branche principale

<mermaid> flowchart TD

   MR[Merge request] -->|pipeline auto| P[terraform plan]
   P -->|commentaire du diff| REVIEW[Revue humaine]
   REVIEW -->|fusion sur main| M[Merge sur branche principale]
   M -->|pipeline auto| P2[terraform plan]
   P2 --> G{Gate manuel}
   G -->|approbation| A[terraform apply]
   A --> INFRA[(Infrastructure)]

</mermaid>

  • Sur la merge request — un job terraform plan s'exécute automatiquement à chaque push, et son résultat (le diff create/update/destroy) est posté en commentaire sur la MR — la revue porte alors autant sur le code HCL que sur son effet réel avant fusion.
  • Sur la branche principale — après fusion, un nouveau plan puis un apply manuel (déclenché explicitement, avec approbation) ou gated (environnement protégé nécessitant une validation) exécute réellement les changements. L'apply n'est jamais automatique sur la branche principale en production : un humain valide le diff final juste avant l'exécution réelle.

Exemple de pipeline GitLab CI

stages:
  - validate
  - plan
  - apply

variables:
  TF_ROOT: infra/
  TF_STATE_NAME: production

default:
  image: hashicorp/terraform:1.9
  before_script:
    - cd $TF_ROOT
    - terraform init

validate:
  stage: validate
  script:
    - terraform fmt -check -recursive
    - terraform validate

plan:
  stage: plan
  script:
    - terraform plan -out=plan.tfplan
    - terraform show -no-color plan.tfplan > plan.txt
  artifacts:
    paths:
      - $TF_ROOT/plan.tfplan
      - $TF_ROOT/plan.txt
    expire_in: 1 day
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

apply:
  stage: apply
  script:
    - terraform apply plan.tfplan
  dependencies:
    - plan
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

Points clés de cet exemple :

  • le job plan tourne à la fois sur les merge requests (pour la revue) et sur main (juste avant l'apply) — le plan exécuté juste avant l'apply doit toujours être frais, pas celui de la MR (dérive possible entre-temps) ;
  • terraform apply plan.tfplan rejoue le plan sauvegardé plutôt qu'un nouveau plan implicite, pour garantir que ce qui est appliqué est exactement ce qui a été revu ;
  • when: manual combiné à environment: production matérialise le gate : le job apparaît dans le pipeline mais attend un déclenchement humain explicite dans l'interface GitLab.

Pour poster le diff du plan en commentaire de merge request, GitLab CI/CD propose un rapport dédié (artifacts: reports: terraform, à vérifier selon version) qui affiche le plan formaté directement dans l'onglet Modifications de la MR, plutôt qu'un simple artefact téléchargeable.

Credentials cloud en CI : OIDC plutôt que clés statiques

Le pipeline doit s'authentifier auprès du provider cloud (AWS, Azure, GCP) pour exécuter plan/apply. Deux approches :

  • Clé statique en variable protégée CIAWS_ACCESS_KEY_ID, secret de service principal Azure, clé JSON GCP stockés comme variables CI masquées/protégées. Fonctionne, mais reste un secret de longue durée à faire tourner, révoquer, et qui fuit potentiellement dans les logs si mal masqué.
  • OIDC (fédération d'identité) — le runner CI présente un jeton OIDC signé par GitLab (ou GitHub) ; le cloud (rôle IAM AWS, identité fédérée Azure/GCP) vérifie ce jeton et délivre des identifiants temporaires, sans aucun secret stocké côté CI. C'est la méthode recommandée, cohérente avec l'authentification par rôle/identité déjà privilégiée sur les pages providers.
apply:
  stage: apply
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.com
  script:
    - export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/terraform-ci
    - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/token.txt
    - echo "$AWS_ID_TOKEN" > token.txt
    - terraform apply plan.tfplan
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

Variables protégées et masquées restent nécessaires même en OIDC pour tout ce qui ne relève pas de l'identité cloud (jeton de registre privé, notification Slack) : les réserver aux branches/environnements protégés (protected: true) évite qu'un pipeline sur une branche de feature quelconque n'y accède.

Verrouillage du state en pipeline

Le verrouillage du state reste indispensable en CI, où plusieurs pipelines peuvent en théorie s'exécuter en parallèle (deux merge requests plannées au même moment, un rerun manuel pendant qu'un apply est en cours). Le backend distant (S3+DynamoDB, Storage Account, GCS — voir les pages providers) pose le verrou pendant toute la durée du plan/apply : un second job qui tente d'y accéder échoue proprement plutôt que de corrompre le state.

Bonnes pratiques complémentaires côté pipeline :

  • resource_group GitLab CI (mot-clé resource_group: sur le job apply) — empêche GitLab de lancer deux instances du même job en parallèle, en file d'attente plutôt qu'en concurrence, en complément du verrou du backend ;
  • limiter le déclenchement du job apply à la seule branche principale (comme dans l'exemple ci-dessus) pour éviter les apply accidentels depuis une branche de feature ;
  • un plan sauvegardé (plan.tfplan) a une durée de vie courte : s'il est rejoué trop tard, Terraform détecte un état devenu incohérent et refuse de l'appliquer tel quel plutôt que de forcer un apply obsolète.
apply:
  stage: apply
  resource_group: production-tfstate
  script:
    - terraform apply plan.tfplan

Voir aussi