ℹ️ À savoir · En bref

En équipe, personne ne lance terraform apply depuis son poste : un pipeline exécute le plan à chaque pull request, publie le résultat, et n'applique qu'après relecture et approbation, avec une identité dédiée sans secret stocké (OIDC).

Le state contient les identifiants des ressources et parfois des secrets : il se protège comme un coffre (accès Entra ID, versioning, verrou, réseau privé).

Objectifs de la fiche

  • Construire un pipeline plan → relecture → approbation → apply.
  • Authentifier le pipeline sur Azure sans secret (fédération OIDC).
  • Écrire des tests avec terraform test et ajouter linters et scanners.
  • Sécuriser le state et détecter la dérive.

Le pipeline type

Contrôles automatiques, plan relu, approbation sur l'environnement protégé, puis apply du plan exact
Contrôles automatiques, plan relu, approbation sur l'environnement protégé, puis apply du plan exact
ÉtapeOutilÉchoue si…
Formatterraform fmt -check -recursiveUn fichier n'est pas au format canonique
Validationterraform init -backend=false + terraform validateErreur de syntaxe ou de référence
Linttflint (avec le plugin azurerm)Taille de VM inconnue, variable inutilisée, mauvaise pratique
Sécuritétrivy config . ou checkov -d .Stockage public, NSG ouvert sur Internet, chiffrement absent
Teststerraform testUne assertion échoue
Planterraform plan -out=tfplanErreur d'API ; le résumé est publié dans la PR
Applyterraform apply tfplanLe plan enregistré n'est plus valide (state modifié entre-temps)

ℹ️ À savoir

tfsec a été fusionné dans Trivy : pour un nouveau pipeline, utilise trivy config. Checkov reste une alternative courante.

Authentification sans secret (OIDC)

Plutôt qu'un secret de principal de service qui expire et fuit, on crée une identité managée (ou une app Entra) avec une fédération d'identité de confiance pour le dépôt et l'environnement. Le pipeline reçoit un jeton à courte durée de vie.

pipeline (exemple GitHub Actions)

# .github/workflows/terraform.yml (extrait)
permissions:
  id-token: write          # obligatoire pour obtenir le jeton OIDC
  contents: read
env:
  ARM_USE_OIDC: "true"
  ARM_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}        # identifiants publics, pas de secret
  ARM_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
  ARM_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -out=tfplan -input=false
      - uses: actions/upload-artifact@v4
        with: { name: tfplan, path: tfplan }
  apply:
    needs: plan
    environment: production    # règle de protection : approbateurs requis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - uses: actions/download-artifact@v4
        with: { name: tfplan }
      - run: terraform init
      - run: terraform apply -input=false tfplan

💡 Astuce

Azure DevOps propose la même chose avec une connexion de service par fédération d'identité de charge de travail (workload identity federation). Pour le backend azurerm, ajoute use_oidc = true et use_azuread_auth = true.

Tester avec terraform test

Depuis Terraform 1.6, les fichiers *.tftest.hcl décrivent des scénarios : chaque bloc run fait un plan ou un apply (sur des ressources temporaires détruites ensuite) et vérifie des assert.

test natif

# tests/catalog.tftest.hcl
variables {
  name          = "CAT-TEST"
  machine_count = 2
}

run "nommage_et_taille" {
  command = plan                       # aucun objet créé
  assert {
    condition     = citrix_machine_catalog.this.name == "CAT-TEST"
    error_message = "Le catalogue ne porte pas le nom attendu."
  }
}

run "refuse_trop_de_machines" {
  command = plan
  variables { machine_count = 500 }
  expect_failures = [var.machine_count]   # la validation doit rejeter
}

ℹ️ À savoir

Depuis la 1.7, mock_provider permet de tester sans appeler les vraies API, ce qui est pratique pour des providers comme Citrix ou Azure en CI.

Sécuriser le state

Accès par identité Entra, versioning, verrou par bail, réseau restreint
Accès par identité Entra, versioning, verrou par bail, réseau restreint

backend.tf

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate"
    storage_account_name = "sttfstatecontoso"
    container_name       = "tfstate"
    key                  = "prd/citrix.tfstate"
    use_azuread_auth     = true      # accès par RBAC, pas par clé de compte
    use_oidc             = true
  }
}
  • Un state par environnement (TF-02) et des droits distincts : le pipeline de recette ne peut pas écrire le state de production.
  • Sur le compte de stockage : versioning des blobs et suppression réversible (retour à un state précédent), clés d'accès partagé désactivées, accès réseau privé.
  • Le verrou est automatique (bail du blob) : en cas de pipeline interrompu, terraform force-unlock <ID> seulement après vérification.
  • Secrets : sensitive = true masque l'affichage mais pas le state ; préfère les références Key Vault, et les valeurs ephemeral (Terraform ≥ 1.10) qui ne sont jamais écrites dans le state.

Détecter la dérive

job planifié (chaque nuit)

terraform plan -detailed-exitcode -input=false
# code 0 : rien à changer
# code 1 : erreur
# code 2 : différences → modification manuelle détectée, alerter l'équipe

Pièges classiques

SymptômeCauseParade
« Saved plan is stale »Le state a changé entre le plan et l'applyRefaire le plan ; sérialiser les exécutions par environnement
Le pipeline n'obtient pas de jeton OIDCid-token: write absent ou sujet de fédération incorrectVérifier permissions et le sujet (repo, branche ou environnement)
Apply sans relecturePas de règle de protection sur l'environnementApprobateurs requis, branche protégée
Secret visible dans le stateMot de passe passé en variable classiqueKey Vault, ephemeral, compte de service Citrix (TF-04)
Tests lents et coûteuxcommand = apply systématiqueplan par défaut, apply ciblé, mock_provider

🔷 En contexte Citrix

Le provider Citrix accepte ses identifiants par variables d'environnement CITRIX_* : stocke le secret du client API Citrix Cloud dans les secrets de l'environnement protégé, jamais dans le dépôt.

Une mise à jour d'image (TF-04) devient une pull request « snapshot de novembre » : plan relu, approbation, apply dans la fenêtre de maintenance.

À retenir

  • Pipeline : fmt, validate, tflint, scan, test → plan relu → approbation → apply du plan exact.
  • OIDC : identité fédérée, aucun secret Azure stocké.
  • terraform test (1.6+) avec plan par défaut ; mock_provider (1.7+).
  • State : RBAC Entra, versioning, clés désactivées, un state par environnement.
  • Dérive : plan planifié avec -detailed-exitcode (2 = différences).

Auto-évaluation

Q1. Pourquoi appliquer un fichier tfplan plutôt que relancer terraform apply ?

→ On applique exactement ce qui a été relu et approuvé ; si l'état a changé entre-temps, Terraform refuse le plan au lieu d'appliquer autre chose.

Q2. Qu'apporte OIDC par rapport à un secret de principal de service ?

→ Pas de secret à stocker ni à renouveler : le pipeline reçoit un jeton à courte durée, valable seulement pour le dépôt et l'environnement autorisés.

Q3. sensitive = true suffit-il à protéger un mot de passe ?

→ Non : il masque l'affichage mais la valeur reste dans le state. Utiliser Key Vault, des valeurs ephemeral et un backend chiffré à accès restreint.

Q4. Que signifie le code retour 2 de terraform plan -detailed-exitcode ?

→ Le plan a trouvé des différences : quelqu'un ou quelque chose a modifié l'infrastructure hors Terraform (ou le code n'est pas appliqué).

Pour aller plus loin