Infrastructure
Terraform sur Azure : diagnostiquer un apply partiel avant de relancer la production
Un runbook de production pour reconstruire un apply Terraform Azure interrompu, comparer configuration, state et ressources réelles, puis décider relance, import ciblé, rollback ou réparation d’état.
Un pipeline Terraform échoue au milieu d’un apply Azure. Le job est rouge, mais plusieurs opérations ont déjà réussi : une identité a été créée, une règle réseau a changé et une attribution de rôle apparaît dans Azure. La réaction la plus tentante consiste à relancer le pipeline. Elle peut terminer le déploiement, mais aussi recréer une ressource, masquer une dérive, heurter un verrou encore actif ou appliquer un plan différent de celui qui a été approuvé.
Un apply Terraform n’est pas une transaction globale. Azure traite des opérations distinctes et Terraform enregistre progressivement les résultats dans son state. Après une interruption, trois réalités peuvent diverger : la configuration Git, le state distant et les ressources réellement présentes dans Azure. Ce runbook vise une décision précise : reprendre avec un plan propre, importer un objet déjà créé, corriger une incohérence d’état sous contrôle, ou rollbacker le changement partiel sans toucher aux ressources saines.
Geler le contexte exact de l’exécution
Avant toute relance, identifiez l’exécution qui a produit l’état courant. Conservez le commit, le workspace, le backend, la clé de state, les variables, la version de Terraform, le lockfile fournisseur, l’identité CI, le plan approuvé et le journal d’apply. Une relance depuis un autre commit ou un autre backend ne rejoue pas la même opération.
incident: inc-20260812-021
pipeline_run: platform-prod-1842
commit: "<git-sha>"
terraform_version: "<version>"
workspace: prod
backend_key: platform/prod.tfstate
plan_artifact: tfplan-prod-1842
execution_identity: "<federated-principal-id>"
started_at_utc: "<timestamp>"
failed_at_utc: "<timestamp>"
failed_address: "<terraform-resource-address>"
preserve:
- apply log and provider errors
- saved plan and plan JSON
- backend and workspace selection
- state lineage and serial
- Azure activity operations in the same window
- application and platform health evidence Suspendez les pipelines automatiques sur le même state. Vérifiez qu’aucun apply, job de drift ou poste opérateur n’écrit encore. Un verrou résiduel se traite seulement après avoir prouvé que son propriétaire n’est plus actif ; le supprimer par réflexe peut autoriser deux écrivains concurrents.
Capturer le state avant de rafraîchir
Commencez par une copie brute du state distant. Ne lancez pas immédiatement un plan qui rafraîchit les objets : la comparaison avant/après est utile pour comprendre ce que Terraform connaissait au moment de la panne.
set -euo pipefail
terraform version
terraform workspace show
terraform providers lock -platform=linux_amd64
terraform state pull > state-before.json
jq '{lineage, serial, terraform_version, resources: [.resources[].type]}' state-before.json > state-before-summary.json
terraform show -json > state-view-before.json
terraform state list > state-addresses-before.txt Le lineage doit correspondre au state attendu et le serial doit rester stable pendant le gel. Une variation indique qu’un autre processus a écrit. Arrêtez alors l’analyse locale et identifiez cette exécution avant de poursuivre.
Ne publiez pas ces fichiers dans un ticket non protégé : le state peut contenir des identifiants, des chaînes de connexion ou des valeurs sensibles. Stockez les preuves dans l’espace d’incident prévu pour les secrets et limitez leur durée de conservation.
Reconstruire ce qu’Azure a réellement accepté
Le dernier message du pipeline indique souvent la première erreur visible, pas la dernière opération réussie. L’Activity Log permet de retrouver les écritures ARM, leur statut, l’identité et le correlationId. Azure Resource Graph complète la vue avec l’état actuel des ressources ciblées.
SUBSCRIPTION_ID="<subscription-id>"
START_UTC="<apply-start-utc>"
END_UTC="<apply-end-utc>"
az account set --subscription "$SUBSCRIPTION_ID"
az monitor activity-log list --start-time "$START_UTC" --end-time "$END_UTC" --status Succeeded --query "[].{time:eventTimestamp,operation:operationName.value,resource:resourceId,caller:caller,correlationId:correlationId}" --output json > azure-writes-succeeded.json
az monitor activity-log list --start-time "$START_UTC" --end-time "$END_UTC" --status Failed --output json > azure-writes-failed.json Filtrez ensuite sur le groupe de ressources et les types concernés. Une réponse ARM Succeeded prouve qu’Azure a accepté l’opération, pas que Terraform l’a écrite dans le state ni que l’application fonctionne. À l’inverse, une ressource absente du state mais visible dans Azure est un candidat à qualifier, pas une invitation automatique à l’importer.
Comparer configuration, state et réalité
Travaillez avec une matrice par adresse Terraform. Elle évite de réduire l’incident à « le state est cassé » alors que l’écart peut venir d’un mauvais workspace, d’une création Azure réussie avant timeout, d’une policy ou d’une ressource modifiée hors IaC.
Pour chaque adresse touchee
Configuration
ressource declaree dans le commit execute
adresse stable ou modifiee par moved block
arguments et dependances attendus
State distant
adresse presente ou absente
ID Azure enregistre
attributs connus apres le dernier refresh
lineage et serial coherents
Azure reel
ressource presente ou absente
proprietes et identite proprietaire
operation de creation ou mise a jour dans l'Activity Log
impact observable sur le service
Classification
les trois vues convergent
Azure cree, state absent
state present, Azure absent
Azure et state presents, proprietes divergentes
mauvais backend ou mauvais workspace
ownership IaC ambigu Cette matrice doit être construite d’abord sur les ressources annoncées comme modifiées dans le plan sauvegardé, puis sur leurs dépendances. Évitez une recherche illimitée sur toute la souscription : elle dilue la causalité et ralentit la remise en service.
Utiliser le refresh-only comme preuve
Une fois le state initial conservé et les écrivains exclus, produisez un plan refresh-only. Il montre comment Terraform alignerait son état sur les objets distants sans proposer les changements de configuration habituels.
terraform plan -refresh-only -out=tfrefresh
terraform show -json tfrefresh > tfrefresh.json
jq -r '
.resource_changes[]?
| select(.change.actions != ["no-op"])
| [.address, (.change.actions | join(","))]
| @tsv
' tfrefresh.json > refresh-differences.tsv N’appliquez pas ce plan par automatisme. Il sert d’abord à exposer les écarts. Si le refresh propose de retirer du state un objet critique parce qu’Azure ne le retourne plus, vérifiez l’abonnement, le tenant, les permissions et l’ID avant toute écriture.
Produisez ensuite un plan normal depuis le commit exact de l’exécution. S’il veut recréer une ressource déjà présente, supprimer un objet sain ou toucher un périmètre non prévu, la relance est bloquée.
Classer le point de rupture
Un apply partiel se traite différemment selon le moment où l’écart est apparu.
Echec avant appel Azure
Aucun write correspondant dans l'Activity Log
State et Azure restent coherents
Corriger l'entree, la permission ou le provider puis regenerer un plan
Azure a cree l'objet, Terraform ne l'a pas enregistre
Operation ARM reussie, objet visible, adresse absente du state
Verifier l'ownership et envisager un import cible
State mis a jour, pipeline termine en erreur
Adresse et ID coherents, operation Azure reussie
Le prochain plan peut etre vide pour cette ressource
Diagnostiquer la ressource suivante au lieu de recreer la precedente
Dependance ou policy refusee
Ressources precedentes saines, operation suivante refusee
Corriger la cause, puis revoir le plan complet
Backend ou workspace incorrect
Lineage, serial ou inventaire inattendu
Ne rien importer et restaurer le bon contexte avant toute action Cette classification empêche un faux rollback : détruire une ressource correctement créée simplement parce que le job global est rouge. Elle empêche aussi la fausse reprise qui consiste à ignorer une ressource réelle non gouvernée par le state.
Réparer l’ownership avant le state
L’import est approprié quand la ressource existe, correspond à la configuration approuvée et doit être gérée à cette adresse. Prévisualisez toujours le résultat, utilisez un import block versionné quand le workflow le permet, puis retirez-le seulement selon la convention du dépôt.
import {
to = azurerm_resource_group.example
id = "/subscriptions/<subscription-id>/resourceGroups/<resource-group>"
} Après l’import, un plan complet doit expliquer chaque différence. Un import qui produit immédiatement une mise à jour ou un remplacement important révèle que la configuration ne décrit pas l’objet existant.
Réservez terraform state rm, state mv et le remplacement direct du state aux cas où l’ownership est formellement établi, la sauvegarde vérifiée et la revue faite par une seconde personne. Retirer une adresse du state ne supprime pas la ressource Azure ; cela retire seulement sa gouvernance Terraform. Pousser manuellement un state modifié est une opération de dernier recours, avec contrôle du lineage, du serial et possibilité de restauration.
Décider relance, import, rollback ou arrêt
La décision doit tenir dans un dossier de changement relisible.
rerun:
when:
- no active writer remains
- backend, workspace, commit and provider lock match
- state and Azure ownership are coherent
- a newly reviewed full plan contains only expected actions
targeted_import:
when:
- Azure creation succeeded before the interruption
- the resource matches approved configuration
- its Terraform address and owner are unambiguous
- the post-import plan is reviewed
rollback:
when:
- a completed partial change causes proven impact
- the reverse plan is smaller and understood
- application and infrastructure validation are ready
stop_and_escalate:
when:
- state lineage or serial changed unexpectedly
- another writer cannot be excluded
- import would seize an ambiguously owned resource
- the recovery plan deletes or replaces an unrelated resource -target n’est pas un mécanisme de déploiement normal. Il peut servir à une récupération exceptionnelle et documentée quand une dépendance précise doit être réparée, mais il doit être suivi d’un plan complet. Sans cela, l’équipe peut déclarer la reprise alors que le reste du graphe demeure divergent.
Valider la convergence et prévoir le rollback
Après la correction, exécutez un plan complet avec le même backend et les mêmes variables. Le résultat attendu n’est pas forcément un plan vide : il doit correspondre exactement à la décision approuvée. Appliquez ensuite sous verrou, avec un seul écrivain et le journal conservé.
Validez les ressources, puis le service qui les consomme : identité effective, route ou règle attendue, accès à la dépendance, logs, métriques et probe avec identifiant de corrélation. Contrôlez aussi que le state distant a avancé d’un serial attendu et qu’un nouveau plan ne révèle aucun changement résiduel inattendu.
Le rollback d’un apply partiel doit lui-même être planifié. Revenir au commit précédent peut proposer la suppression des ressources créées pendant l’exécution interrompue. Relisez ce plan comme un changement de production, protégez les données et conservez les objets sains quand leur suppression augmenterait l’impact.
Conclusion
Un pipeline Terraform rouge ne signifie ni que rien n’a changé, ni que tout doit être annulé. Après un apply partiel, la source de vérité est une comparaison contrôlée entre le commit exécuté, le state distant et les opérations réellement acceptées par Azure.
La reprise devient sûre quand le backend est identifié, les écrivains sont gelés, chaque ressource a un owner clair et le nouveau plan est plus précis que l’incident. Relancez quand les trois vues convergent, importez quand Azure a créé un objet gouvernable, rollbackez quand un changement partiel cause un impact prouvé, et arrêtez toute intervention sur le state dès que le lineage, le serial ou l’ownership restent ambigus.