ℹ️ À 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 testet ajouter linters et scanners. - Sécuriser le state et détecter la dérive.
Le pipeline type

| Étape | Outil | Échoue si… |
|---|---|---|
| Format | terraform fmt -check -recursive | Un fichier n'est pas au format canonique |
| Validation | terraform init -backend=false + terraform validate | Erreur de syntaxe ou de référence |
| Lint | tflint (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 |
| Tests | terraform test | Une assertion échoue |
| Plan | terraform plan -out=tfplan | Erreur d'API ; le résumé est publié dans la PR |
| Apply | terraform apply tfplan | Le 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

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 = truemasque l'affichage mais pas le state ; préfère les références Key Vault, et les valeursephemeral(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ôme | Cause | Parade |
|---|---|---|
| « Saved plan is stale » | Le state a changé entre le plan et l'apply | Refaire le plan ; sérialiser les exécutions par environnement |
| Le pipeline n'obtient pas de jeton OIDC | id-token: write absent ou sujet de fédération incorrect | Vérifier permissions et le sujet (repo, branche ou environnement) |
| Apply sans relecture | Pas de règle de protection sur l'environnement | Approbateurs requis, branche protégée |
| Secret visible dans le state | Mot de passe passé en variable classique | Key Vault, ephemeral, compte de service Citrix (TF-04) |
| Tests lents et coûteux | command = apply systématique | plan 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+) avecplanpar 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é).