Automation
Terraform sur Azure : contenir des applies concurrents avant la divergence du state
Un runbook de production pour identifier le writer qui détient le lease Azure Blob, arrêter les pipelines concurrents, récupérer un lock obsolète et valider le state avant un nouvel apply.
Deux pipelines Terraform démarrent sur le même environnement Azure. Le premier applique un changement réseau ; le second attend le lock du state distant, puis un opérateur envisage force-unlock pour débloquer la livraison. Si le premier writer est encore actif, retirer son lock ne résout pas la file d’attente. Cela autorise deux processus à décider depuis des vues différentes de la même infrastructure.
Le cas d’usage est un backend azurerm stocké dans un blob Azure Storage, partagé entre détection de drift planifiée, plans de pull request et applies de production. Ce runbook identifie le state exact, prouve si un writer est actif, contient les jobs concurrents, ne récupère qu’un lock obsolète et valide la convergence avant un nouvel apply. L’objectif n’est pas de libérer rapidement le lock, mais de rétablir un writer unique et autoritatif.
Figer le périmètre du state
Commencez par l’identité du backend, pas par le nom affiché du pipeline. Deux jobs peuvent sembler liés tout en utilisant des clés ou workspaces différents. À l’inverse, deux dépôts distincts peuvent entrer en collision sur le même blob.
incident: inc-20260826-terraform-lock
environment: production
backend:
storage_account: sttfstateprodweu
container: tfstate
key: platform/prod.tfstate
workspace: default
contenders:
- pipeline: platform-apply-1849
commit: <git-sha>
operation: apply
started_utc: <timestamp>
- pipeline: nightly-drift-921
commit: <git-sha>
operation: plan
started_utc: <timestamp>
stop_conditions:
- un autre writer ne peut pas être exclu
- le serial du state change pendant le diagnostic
- la clé backend ou le workspace reste ambigu
- des écritures Azure continuent après l’arrêt des jobs Suspendez tous les jobs planifiés ou manuels qui ciblent ce state. Désactivez temporairement les retries automatiques. N’utilisez pas -lock=false : le lock est précisément le contrôle qui empêche un second writer d’entrer dans l’incident.
Conserver le diagnostic du lock
L’erreur Terraform expose normalement un Lock ID ainsi que le chemin, l’opération, le propriétaire, la version et l’heure de création. Conservez l’erreur complète avec le run du pipeline. Le Lock ID n’est pas un simple jeton de confirmation : terraform force-unlock l’utilise pour cibler le lock signalé par Terraform.
Preuves à conserver
Lock ID et chemin du backend
Opération : plan, apply ou autre écriture du state
Propriétaire ou identité du runner
Version Terraform
Heure de création du lock
IDs et statuts courants des runs de pipeline
Dernier timestamp de log de chaque runner
Commit, workspace et configuration backend
Fenêtre Azure Activity Log de l’identité d’apply Ne concluez pas qu’un lock est obsolète à partir de son seul âge. Un apply long, une opération provider lente ou un runner qui ne transmet plus ses logs peut rester actif. L’âge est un indice ; la présence réelle du writer porte la décision.
Prouver si le writer est encore actif
Corrélez trois vues : le control plane CI, le processus du runner et les opérations Azure. Un job marqué comme annulé peut encore avoir un processus enfant. Un runner disparu peut avoir envoyé une requête Azure juste avant de perdre sa connectivité.
SUBSCRIPTION_ID="<subscription-id>"
START_UTC="<lock-created-utc>"
END_UTC="<now-utc>"
az account set --subscription "$SUBSCRIPTION_ID"
az monitor activity-log list --start-time "$START_UTC" --end-time "$END_UTC" --query "[].{time:eventTimestamp,status:status.value,operation:operationName.value,resource:resourceId,caller:caller,correlationId:correlationId}" --output json > operations-azure-pendant-lock.json Filtrez le résultat sur l’identité d’exécution et les scopes attendus. Des écritures récentes ne prouvent pas que Terraform possède encore un processus, mais elles interdisent un unlock précipité tant que le job et l’opération associés ne sont pas compris.
Classez l’incident avant d’agir :
- Writer actif : le processus tourne ou les opérations Azure continuent. Attendez ou arrêtez proprement ce writer.
- Lock Terraform obsolète : le processus propriétaire a disparu, aucun retry n’est actif et le serial reste stable. Un
force-unlockcontrôlé est possible. - Mauvais contexte d’exécution : le chemin, le workspace ou le tenant ne correspond pas à la cible. Corrigez le contexte sans retirer de lock.
- Échec d’accès au backend : l’authentification, le DNS ou la joignabilité Storage échoue avant l’acquisition. Réparez ce chemin au lieu de traiter une fausse contention.
Lire le lease Azure Blob comme preuve complémentaire
Le backend azurerm s’appuie sur les capacités natives de verrouillage d’Azure Blob Storage. Les propriétés du blob confirment que l’objet de state attendu porte un lease actif et donnent sa dernière modification. Elles n’identifient pas, à elles seules, le processus Terraform vivant.
ACCOUNT="sttfstateprodweu"
CONTAINER="tfstate"
BLOB="platform/prod.tfstate"
az storage blob show --auth-mode login --account-name "$ACCOUNT" --container-name "$CONTAINER" --name "$BLOB" --query "{etag:properties.etag,lastModified:properties.lastModified,leaseStatus:properties.lease.status,leaseState:properties.lease.state,leaseDuration:properties.lease.duration}" --output json Utilisez si possible une identité en lecture seule pour cette inspection. Casser directement le lease contourne le workflow de lock Terraform et peut retirer la protection d’un writer valide. Réservez l’opération Azure de rupture de lease à une procédure exceptionnelle réunissant les responsables Storage et Terraform, pas au chemin normal de déverrouillage.
Arrêter les concurrents avant de retirer un lock obsolète
Annulez d’abord les jobs en attente, puis terminez le runner ou le processus propriétaire via la plateforme CI. Vérifiez qu’aucun automatisme ne peut le relancer. Observez une fenêtre de silence courte et explicite : aucun heartbeat runner, aucune écriture Azure par l’identité d’exécution et aucune modification du state.
Capturez les métadonnées du state sans diffuser son contenu.
set -euo pipefail
umask 077
terraform version
terraform workspace show
terraform state pull > state-before-unlock.json
jq '{lineage,serial,terraform_version}' state-before-unlock.json > state-before-unlock-metadata.json
sha256sum state-before-unlock.json > state-before-unlock.sha256 Le state peut contenir des valeurs sensibles. Stockez cette copie uniquement dans l’emplacement de preuve approuvé, limitez les accès et supprimez la copie de travail selon la procédure d’incident.
Si le propriétaire a disparu, si l’identité du backend est exacte et si le serial reste stable, utilisez le Lock ID fourni par Terraform.
LOCK_ID="<terraform-lock-id>"
terraform force-unlock "$LOCK_ID" La commande retire le lock ; elle n’annule aucune ressource Azure et ne répare pas le state. Si l’ID est refusé, arrêtez-vous et renouvelez les preuves. Ne substituez pas un autre lock et ne cassez pas le lease pour forcer le passage.
Reprendre par un plan propre
N’autorisez qu’un seul job de diagnostic. Utilisez le même commit, la même version Terraform, le même lockfile provider, les mêmes variables, le même workspace et la même configuration backend que le run interrompu. Acquérez le lock normal avec une attente bornée, puis produisez un nouveau plan.
set -euo pipefail
terraform init -input=false
terraform workspace show
terraform plan -input=false -lock-timeout=5m -out=recovery.tfplan
terraform show -json recovery.tfplan > recovery.tfplan.json
jq -r '
.resource_changes[]?
| select(.change.actions != ["no-op"])
| [.address, (.change.actions | join(","))]
| @tsv
' recovery.tfplan.json > recovery-actions.tsv Un plan vide ou strictement attendu soutient la reprise. Un plan qui supprime, remplace ou importe des ressources sans rapport signifie que l’incident dépasse le lock obsolète. Comparez configuration, state distant et ressources Azure réelles avant tout apply.
Décider attente, unlock, récupération ou arrêt
Utilisez une décision qu’un autre opérateur peut contester :
ATTENDRE
Le writer propriétaire reste actif ou son opération Azure n’est pas résolue.
FORCE-UNLOCK
Le propriétaire est arrêté, les retries sont désactivés, le backend exact
est prouvé, le serial reste stable et le Lock ID Terraform est conservé.
RÉCUPÉRER LE STATE
Un writer s’est arrêté après avoir modifié Azure ou le state et le nouveau
plan n’est pas propre. Reconstruire configuration, state et ressources réelles.
ARRÊTER ET ESCALADER
Deux writers ont pu se chevaucher, lineage ou serial est inattendu,
l’ownership est ambigu ou la reprise propose une destruction hors périmètre. Si deux writers ont réellement opéré en parallèle, restaurer une ancienne version du blob n’est pas un rollback automatique. Cette action peut effacer les bindings écrits par une opération valide. Conservez les versions, comparez serial et lineage, puis passez par une revue dédiée de récupération du state.
Prévenir la prochaine collision
Le locking est la dernière ligne de défense, pas l’ordonnanceur des pipelines. Sérialisez les opérations sur l’identité du state : compte Storage, conteneur, clé et workspace. Plans, détections de drift et applies capables d’acquérir le même lock appartiennent au même groupe de concurrence.
Gardez un seul writer pour les applies de production, rendez la file visible, annulez les plans dépassés et interdisez les retries aveugles après interruption. Affichez l’identité du backend dans les logs sans imprimer de credentials. Ne séparez les clés de state que lorsque les stacks possèdent réellement des responsabilités et cycles de vie distincts, pas pour contourner une attente.
Après la reprise, vérifiez qu’un apply relu se termine, que le serial avance comme prévu, qu’un plan suivant ne contient aucune action inexpliquée et que le service Azure concerné passe sa probe opérationnelle. C’est le point de validation. Un lock libéré ne prouve rien sur la santé de l’infrastructure.
Conclusion
Un incident de lock Terraform est d’abord un incident de concurrence. La séquence sûre consiste à figer le state Azure Blob exact, arrêter les concurrents, prouver la présence du writer, conserver les métadonnées, ne retirer qu’un lock Terraform obsolète, puis reprendre avec un plan propre et unique.
Attendez tant qu’un writer est actif. Exécutez force-unlock lorsque son absence et la stabilité du state sont prouvées. Passez en récupération du state si Azure, la configuration et le state ne convergent plus. Le résultat utile n’est pas un backend déverrouillé, mais un système à writer unique restauré, avec un plan relu et un chemin de retour explicite.