🔒 Bloqueo del estado¶
1. Cómo bloquea el backend azurerm¶
El proceso de bloqueo sigue una secuencia estricta para garantizar que nadie más modifique el estado mientras se evalúan o aplican cambios.
terraform apply
│
├─ 1. Lock() → PUT lease (duración infinita) sobre tfstate/app/lab.tfstate
│ → PUT metadata terraformlockid = base64({ID, Operation, Who, Version, Created, Path})
│ si el lease ya existe → "Error acquiring the state lock" + Lock Info del metadato
├─ 2. GET estado → lee el blob (con el lease-id)
├─ 3. plan → "Do you want to perform these actions?" ◄── el lock sigue tomado mientras piensas
├─ 4. apply → cambios en Azure
├─ 5. PUT estado → escribe el blob (serial + 1; snapshot si snapshot = true)
└─ 6. Unlock() → DELETE metadata, release lease
Aquí tienes una clasificación clara de qué comandos interactúan con el bloqueo:
| Toman el bloqueo | No lo toman (solo leen) |
|---|---|
plan, apply, destroy, refresh, import, state mv/rm/push/replace-provider, init -migrate-state, test (runs con apply) |
state list, state show, state pull, output, show, validate, fmt, graph |
⚠️ El lease manual del original bloquea a Terraform, no a los demás. Los scripts
apply_lock.shcreaban un lease sobre el blob y después lanzabanterraform apply. Terraform intenta su propio lease, lo encuentra ocupado y falla: state blob is already locked, y sin Lock Info porque nadie escribió el metadato. Además, ninguno de esos comandos existe tal como estaban:az storage blob lease createeslease acquire,lease listno existe (se consulta conblob show) y--lease-duration 3600es inválido: Azure solo admite 15-60 segundos o -1 (infinito). El bloqueo correcto es no hacer nada: el backend lo gestiona.
2. Anatomía de un bloqueo¶
Cuando Terraform no puede adquirir el bloqueo, devuelve un mensaje detallado. Leer este mensaje es clave para saber cómo actuar.
│ Error: Error acquiring the state lock
│
│ Error message: state blob is already locked
│ Lock Info:
│ ID: 1f6c3a2e-9b1d-4c0e-8a7f-2d5e6b8c9a01 ◄── lo único que acepta force-unlock
│ Path: tfstate/app/lab.tfstate ◄── qué estado (contenedor/key)
│ Operation: OperationTypeApply ◄── plan, apply, destroy, migration…
│ Who: ana@portatil-ana ◄── usuario@host del proceso: a quién preguntar
│ Version: 1.9.5
│ Created: 2026-09-10 13:04:11.418 +0000 UTC ◄── hace 2 minutos: espera. Hace 2 días: huérfano
│ Info:
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
| Lo que dice Lock Info | Decisión |
|---|---|
Created reciente, Who es una persona o el pipeline |
Esperar. terraform plan -lock-timeout=5m reintenta solo. Si es una persona, pregúntale: puede estar delante de "Do you want to perform these actions?" |
Who eres tú en este mismo host, y no tienes ningún terraform abierto |
Un proceso murió sin liberar (Ctrl+C dos veces, terminal cerrada, agente cancelado). terraform force-unlock <ID> |
Created de hace horas y Who es un runner de CI (runner@fv-az123) |
Job cancelado o runner destruido. Comprueba en la plataforma que no hay ejecuciones activas y force-unlock |
| Sin Lock Info: solo already locked o LeaseAlreadyPresent | Alguien creó el lease fuera de Terraform (los scripts del original). az storage blob lease break |
🔷
-lock=falseno es una solución. Desactiva la comprobación: el comando se ejecuta como si nadie más estuviera trabajando. Tiene un único uso legítimo: unplande solo lectura en la PR de CI, donde no se escribe estado y no se quiere esperar alapplyde otra rama. Enapply,destroyo cualquier comandostatees la causa directa del estado corrupto que la página del original decía querer evitar.
3. Laboratorio en Topaz: el backend local también bloquea¶
El backend local bloquea con un flock del sistema operativo y escribe el mismo Lock Info en un archivo .<nombre>.lock.info junto al estado. Misma interfaz, mismo mensaje, mismo force-unlock. Retoma el proyecto de la página 7:
cd ~/tf-estado/app && mkdir -p estados
cat > backend.tf <<'EOF'
terraform { backend "local" { path = "estados/app-lab.tfstate" } }
EOF
terraform init -reconfigure && terraform apply -auto-approve # 4 to add (grupo, vnet, 2 subredes; main.tf de la [página 7](index.md#pagina-7))
# ─── 1. Provocar el bloqueo ─────────────────────────────────────────────────────
# Terminal A:
terraform apply # se queda en "Do you want to perform these actions?": NO respondas
# Terminal B:
cd ~/tf-estado/app
ls -a estados/ # app-lab.tfstate .app-lab.tfstate.lock.info ◄── existe mientras A tiene el lock
jq . estados/.app-lab.tfstate.lock.info # {"ID": "…", "Operation": "OperationTypeApply", "Who": "root@terraform00", "Created": "…"}
terraform plan
# Error: Error acquiring the state lock
# Error message: resource temporarily unavailable ◄── el flock; en azurerm diría "state blob is already locked"
# Lock Info: ID … Who: root@terraform00 …
terraform state list # funciona: los comandos de lectura no bloquean
terraform plan -lock=false # funciona: y por eso es peligroso en apply
# ─── 2. Esperar en vez de fallar ────────────────────────────────────────────────
# Terminal B:
terraform plan -lock-timeout=2m # "Acquiring state lock. This may take a few moments…" y reintenta
# Terminal A: responde no
# Terminal B continúa solo en cuanto A libera. Es lo que usará el pipeline (8.5)
# ─── 3. Un proceso colgado (no muerto): simula un agente congelado ──────────────
# Terminal A:
terraform apply & # en segundo plano; se para a esperar la confirmación
kill -STOP %1 # congela el proceso: tiene el lock y no lo soltará
# Terminal B:
terraform plan # bloqueado, Lock Info con Who = tú
ps -ef | grep '[t]erraform apply' # el proceso EXISTE: no es huérfano. force-unlock sería un error
# Terminal A:
kill -CONT %1 && fg # descongela; responde no
# ─── 4. force-unlock: solo con el ID ────────────────────────────────────────────
# Terminal A: terraform apply (deja la pregunta abierta)
# Terminal B:
ID=$(jq -r .ID estados/.app-lab.tfstate.lock.info)
terraform force-unlock "$ID" # pide confirmación: "yes". El archivo .lock.info desaparece
terraform force-unlock 00000000-0000-0000-0000-000000000000 # ID equivocado: "lock ID does not match": la salvaguarda
# Terminal A: responde no → Terraform intenta liberar un lock que ya no es suyo y avisa: "Failed to unlock state"
# En azurerm el efecto es idéntico: el lease se rompe y el metadato se borra. La operación de A, si siguiera,
# fallaría al escribir porque su lease-id ya no es válido: el estado no se corrompe, pero A termina con error.
# ─── 5. Lo que el local NO reproduce: el bloqueo huérfano ───────────────────────
terraform apply & sleep 3; kill -9 %1 # muerte súbita
terraform plan # FUNCIONA: el SO liberó el flock al morir el proceso
# En azurerm, el lease infinito sobrevive al proceso: es exactamente el caso que requiere force-unlock (8.4)
terraform destroy -auto-approve
4. Azure real: ver, romper y no crear leases¶
En un entorno real con Azure Storage, las herramientas de CLI te permiten inspeccionar y gestionar el estado del lease directamente.
# Con el backend azurerm de la página 7 (use_azuread_auth = true, claves desactivadas en la cuenta)
ST=sttfstateXXXXXXXX; BLOB=app/lab.tfstate
# Ver el bloqueo desde fuera mientras otro apply espera confirmación
az storage blob show --account-name $ST -c tfstate -n $BLOB --auth-mode login \
--query "{lease:properties.lease.status, duracion:properties.lease.duration, lock:metadata.terraformlockid}" -o table
# lease = locked, duracion = infinite, lock = eyJJRCI6…
az storage blob show --account-name $ST -c tfstate -n $BLOB --auth-mode login \
--query metadata.terraformlockid -o tsv | base64 -d | jq . # el Lock Info completo: ID, Who, Created
# Bloqueo huérfano (agente cancelado, portátil apagado). Orden: confirmar → force-unlock → si falla, romper el lease
terraform force-unlock <ID> # 1º: usa el backend, limpia lease y metadato
az storage blob lease break --account-name $ST -c tfstate -b $BLOB --auth-mode login # 2º: si force-unlock no puede
az storage blob metadata update --account-name $ST -c tfstate -n $BLOB --auth-mode login --metadata "" # y quita el metadato
# El experimento del original, para ver por qué está mal
az storage blob lease acquire --account-name $ST -c tfstate -b $BLOB --auth-mode login --lease-duration 60
terraform plan # Error acquiring the state lock: state blob is already locked (sin Lock Info)
sleep 60 && terraform plan # el lease de 60 s caducó: vuelve a funcionar
# Con --lease-duration -1 (infinito), como hacían los scripts, solo lo arregla "lease break"
# Quién bloqueó qué y cuándo: los logs de diagnóstico de la página 7 registran las operaciones de lease
# KQL: StorageBlobLogs | where OperationName == "LeaseBlob" and ObjectKey endswith "lab.tfstate"
# | project TimeGenerated, RequesterUpn, StatusCode, LeaseAction = tostring(parse_json(Uri))
A continuación, una comparativa para entender qué protege cada mecanismo:
| Mecanismo | Protege contra | No protege contra |
|---|---|---|
| Lease del blob (bloqueo del estado) | Dos operaciones de escritura simultáneas sobre el mismo estado | Borrar el blob, la cuenta o el grupo; aplicar un plan obsoleto; dos proyectos que gestionan el mismo recurso |
azurerm_management_lock CanNotDelete (ARM) |
Borrar la cuenta, el contenedor o el grupo del estado | Escrituras concurrentes: los locks de ARM no actúan sobre el plano de datos. Un ReadOnly sobre la cuenta tampoco impide modificar blobs |
serial del estado |
Escribir una versión más antigua (state push) o aplicar un tfplan guardado sobre un estado que cambió (Saved plan is stale) |
Nada en tiempo real: se comprueba al escribir |
| Versionado + soft delete | Recuperar tras cualquiera de los fallos anteriores | Que ocurran |
🔷 El lock de ARM del original no es state locking. Crear un
CanNotDeletesobre el contenedortfstatees buena idea (la página 7 lo hace sobre el grupo), pero por otro motivo: impide que alguien borre el contenedor con todos los estados dentro. No tiene ningún efecto sobre dosapplyconcurrentes; de eso se ocupa el lease. Son capas complementarias, no alternativas.
5. Bloqueo en CI/CD¶
Un pipeline no necesita scripts de lease: el backend bloquea igual que en tu terminal. Lo que sí necesita son tres cosas que el original no tenía: serializar los jobs que tocan el mismo estado, esperar en vez de fallar cuando otro job lo tiene, y un camino para liberar un bloqueo huérfano cuando un runner se cancela a mitad de apply.
# .github/workflows/terraform.yml (fragmentos; el resto como en la página 6: OIDC, fmt, validate, test)
concurrency:
group: tfstate-app-${{ github.event.inputs.entorno || 'lab' }} # un grupo por estado: los jobs se encolan, no compiten
cancel-in-progress: false # NUNCA cancelar un apply a medias: deja el lock huérfano
jobs:
plan:
steps:
- run: terraform plan -input=false -lock=false -out=tfplan # PR: solo lectura, no esperar al apply de otra rama
aplicar:
environment: lab
steps:
- run: terraform apply -input=false -lock-timeout=10m tfplan # espera hasta 10 min al lock; "stale plan" si el estado cambió
# Liberación manual de un bloqueo huérfano: workflow_dispatch con el ID, deja rastro de quién lo ejecutó
force-unlock:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.lock_id != ''
environment: lab # con revisores: dos personas para romper un lock
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform force-unlock -force ${{ github.event.inputs.lock_id }} # -force: sin prompt interactivo
# azure-pipelines.yml (fragmento equivalente)
stages:
- stage: aplicar
lockBehavior: sequential # las ejecuciones del stage se encolan en orden
jobs:
- deployment: apply
environment: lab # con "Exclusive lock" y aprobaciones en el environment
strategy:
runOnce:
deploy:
steps:
- task: TerraformTaskV4@4
inputs:
command: apply
commandOptions: '-input=false -lock-timeout=10m tfplan'
environmentServiceNameAzureRM: 'sc-tf-lab' # service connection con Workload Identity Federation (OIDC): sin secreto
| Decisión | Motivo |
|---|---|
concurrency / lockBehavior por estado |
Evita el conflicto antes de que exista: el segundo job ni siquiera arranca hasta que el primero termina. El lease queda como red de seguridad |
cancel-in-progress: false |
Cancelar un apply mata el proceso: recursos a medio crear y lease huérfano. Cancelar un plan de PR sí es aceptable |
-lock-timeout=10m en apply |
Una persona aplicando desde su portátil no debe hacer fallar el pipeline: que espere |
-lock=false solo en el plan de PR |
No escribe estado; un plan sobre un estado que está cambiando solo produce un plan que habrá que repetir |
Job force-unlock con aprobación |
Romper un lock es una acción auditable: queda quién lo pidió, quién lo aprobó y qué ID se liberó. Mejor que un az storage blob lease break desde un portátil |
Sin secrets.AZURE_CREDENTIALS ni claves de cuenta |
OIDC (página 6) y use_azuread_auth. Los scripts del original necesitaban la clave para el lease: con shared_access_key_enabled = false ni siquiera funcionarían |
6. Lo que el bloqueo no resuelve¶
El bloqueo del estado es fundamental, pero no es una solución mágica para todos los problemas de concurrencia o gestión.
| Problema | Por qué el lock no lo evita | Qué lo evita |
|---|---|---|
| Dos personas aplican en secuencia con código distinto: la segunda deshace lo de la primera | No hay concurrencia; cada apply es correcto respecto a su código |
Un solo origen de verdad (rama main) y aplicar solo desde el pipeline |
Aplicar un tfplan de hace una hora |
El lock se toma al aplicar, no al planificar | El serial: Saved plan is stale. Volver a planificar |
| Dos proyectos (dos estados) gestionan el mismo recurso | Cada estado tiene su propio lease; se pisan en Azure, no en el blob | Un recurso, un estado. Compartir por data sources, no duplicando resource |
| Cambios a mano en el portal mientras nadie aplica | No pasan por Terraform | plan -refresh-only programado (página 6), RBAC que no dé Contributor a personas en producción |
Un apply interrumpido deja recursos creados y no registrados |
El lock protege el archivo, no la transacción con Azure | Terraform escribe el estado tras cada recurso; lo que falte se adopta con import o se borra a mano |
7. Errores comunes¶
⚠️ Solución de problemas
Mensaje o síntoma Causa y solución Error acquiring the state lock con Lock Info reciente Otra operación en curso. No es un error: es el bloqueo funcionando. Espera o usa -lock-timeout=5m; pregunta aWhoantes de tocar nadaLock Info con Createdde hace horas o díasBloqueo huérfano: proceso muerto, runner cancelado, portátil apagado. Confirma que no existe el proceso y terraform force-unlock <ID>state blob is already locked sin Lock Info Alguien creó el lease fuera de Terraform (scripts del original, o una herramienta externa). force-unlockno puede:az storage blob lease break … --auth-mode loginFailed to unlock state: … lock ID does not match El ID no es el del lock actual: copiado mal o de un error antiguo. Vuelve a lanzar planpara obtener el ID vigente. Nunca respondas con-lock=falseFailed to unlock state al terminar un applyque sí funcionóAlguien hizo force-unlockde tu lock mientras trabajabas. El estado se escribió; comprueba conplany habla con quien lo rompió: el ID identifica al proceso, no a la personaError: Saved plan is stale al aplicar un tfplanEl serial del estado cambió desde que se generó el plan. No es un problema de lock: vuelve a plan -outy revisa la diferenciaLeaseIdMissing / LeaseIdMismatchWithBlobOperation (412) Un proceso intenta escribir el blob con un lease que ya no es suyo (le rompieron el lock). El estado no se corrompe; el proceso termina con error. Relanza cuando el lock esté libre InvalidHeaderValue al hacer lease acquire --lease-duration 3600Azure solo admite 15-60 segundos o -1. Y recuerda: no deberías crear leases a mano sobre el blob del estado az storage blob lease list: 'list' is not in the 'az storage blob lease' command groupNo existe. El estado del lease se consulta con az storage blob show --query properties.leaseAuthorizationPermissionMismatch al romper un lease Romper un lease es una operación de escritura: necesitas Storage Blob Data Contributor; Reader no basta. Ni Contributor de ARM: no toca el plano de datos En Topaz, kill -9delapplyy el siguienteplanfunciona sinforce-unlockComportamiento correcto del backend local: el sistema operativo libera el flock al morir el proceso. En azurermel lease sobrevive: no extrapolesEl pipeline falla con Error acquiring the state lock cada vez que alguien aplica desde su portátil Falta -lock-timeouten elapplydel job. Y a medio plazo: nadie aplica desde el portátil en entornos con pipelineCancelar un job de GitHub Actions deja el lock tomado cancel-in-progress: trueen el grupo de concurrencia, o cancelación manual. Ponlo enfalseparaapplyy usa el jobforce-unlockcon aprobación para limpiarUn ReadOnly de ARM sobre la cuenta y Terraform sigue escribiendo el estado Los locks de ARM no gobiernan el plano de datos. Para impedir escrituras en el blob: quitar el rol de datos, no poner locks. Para evitar borrados: CanNotDelete
8. Autoevaluación¶
- ¿Es cierto que Azure Blob Storage no tiene bloqueo nativo para Terraform?
No. El backend
azurermtoma un lease infinito del blob al empezar cada operación de escritura, guarda el Lock Info en el metadatoterraformlockidy lo libera al terminar. No hace falta ningún script. - ¿Qué ocurre si creas un lease a mano antes de
terraform apply, como hacía el original? Terraform intenta su propio lease, lo encuentra ocupado y falla con state blob is already locked, sin Lock Info. Has bloqueado a Terraform, no a los demás. - ¿Qué comandos toman el bloqueo y cuáles no?
Lo toman los que pueden escribir:
plan,apply,destroy,import,state mv/rm/push,init -migrate-state. No lo toman los de solo lectura:state list/show/pull,output,validate. - ¿Qué tres campos del Lock Info deciden qué hacer?
Created(reciente: espera; antiguo: huérfano),Who(a quién preguntar, o si el proceso existe) eID(lo único que aceptaforce-unlock). - ¿Por qué
force-unlockexige el ID? Para garantizar que liberas ese lock y no uno que otra persona acaba de tomar. Con un ID equivocado falla con lock ID does not match: es la salvaguarda. - ¿Cuándo es legítimo
-lock=false? Solo en unplande solo lectura en la PR de CI. Nunca enapply,destroyni comandosstate: es la causa directa de estados corruptos. - ¿Qué diferencia hay entre el bloqueo del estado y un
CanNotDeletede ARM sobre el contenedor? El lease impide escrituras concurrentes en el blob; el lock de ARM impide borrar el contenedor o la cuenta. El de ARM no actúa sobre el plano de datos, así que no evita dosapplya la vez. Son complementarios. - ¿Qué reproduce el backend local de Topaz y qué no?
Reproduce el mensaje, el Lock Info (archivo
.lock.info),-lock-timeoutyforce-unlock. No reproduce el bloqueo huérfano: el sistema operativo libera el flock al morir el proceso, mientras que el lease de Azure sobrevive. - ¿Qué tres medidas sustituyen a los scripts de lease en un pipeline?
concurrency(olockBehavior: sequential) por estado para encolar jobs,-lock-timeouten elapplypara esperar en vez de fallar, y un job manual deforce-unlockcon aprobación para bloqueos huérfanos. - ¿Por qué
cancel-in-progress: falseen el job de apply? Cancelar mata el proceso a mitad: recursos a medio crear y un lease huérfano. El plan de PR sí se puede cancelar porque no escribe estado. - ¿Qué protege contra aplicar un tfplan obsoleto, si el lock no lo hace?
El serial del estado: si cambió desde que se generó el plan, Terraform rechaza el
applycon Saved plan is stale.
9. Referencias¶
- Bloqueo del estado,
terraform force-unlocky opciones-locky-lock-timeout - Backend
azurermy backendlocal - Operación Lease Blob (REST),
az storage blob leasey concurrencia en Blob Storage - Bloqueos de Azure Resource Manager (y por qué no afectan al plano de datos)
- Monitorizar Blob Storage y tabla
StorageBlobLogs - Concurrencia en GitHub Actions, environments con revisores y exclusive lock en Azure Pipelines
- Service connection con Workload Identity Federation
- Azure Local Emulator (Topaz)