🔐 Estado remoto en Azure Storage¶
1. Qué contiene el estado y por qué sacarlo del disco¶
{
"version": 4, # formato del archivo (no cambia desde 0.12)
"terraform_version": "1.9.5", # quién lo escribió: una versión más nueva lo hace ilegible para las antiguas
"serial": 17, # se incrementa en cada escritura: detecta pisar un estado más reciente
"lineage": "5f1c…-…", # identidad del estado desde su creación: impide mezclar dos proyectos
"outputs": { … },
"resources": [
{
"module": "module.red", # dirección completa: module.red.azurerm_subnet.this["backend"]
"mode": "managed", # managed = resource; data = data source
"type": "azurerm_subnet",
"name": "this",
"instances": [ { "index_key": "backend", "attributes": { "id": "/subscriptions/…", … },
"dependencies": [ "module.red.azurerm_virtual_network.this" ] } ]
}
]
}
⚠️ El estado contiene secretos en claro. La contraseña de SQL de la página 4, la clave de la cuenta de almacenamiento de la página 5 (
primary_access_key) y cualquierrandom_passwordestán enattributestal cual.sensitive = truesolo los oculta en la consola. Quien pueda leer el estado puede leer todos los secretos de la infraestructura: por eso el contenedor del estado se protege como un Key Vault, y por eso*.tfstate*está en el.gitignoredesde la página 1.
| Necesidad | Estado local | Backend azurerm |
|---|---|---|
| Varias personas y un pipeline | ❌ Cada uno tiene su copia; la segunda persona destruye lo de la primera | ✅ Una sola copia; bloqueo por lease del blob durante cada operación |
| Perderlo o corromperlo | ⚠️ Solo terraform.tfstate.backup (la versión anterior) |
✅ Versionado de blobs + soft delete + snapshot = true. No es "automático": lo configuras tú en 7.2 |
| Quién lo lee o cambia | ❌ Quien tenga el disco | ✅ RBAC de datos sobre el contenedor; logs de diagnóstico con identidad y hora |
| Secretos | ❌ En claro en un portátil, a un git add de distancia |
⚠️ En claro también, pero cifrados en reposo, sin acceso público y con permisos explícitos |
| En Topaz | ✅ | ❌ plano de datos. Se practica la mecánica con backend "local" (7.7) |
2. Bootstrap: la cuenta que guarda el estado¶
El huevo y la gallina: la cuenta del estado se crea con Terraform, pero ese Terraform aún no tiene dónde guardar su estado. La respuesta estándar es un proyecto bootstrap pequeño, separado, cuyo estado sí es local (y se guarda en un lugar seguro o se migra a su propia cuenta después). Cambia una vez al año; el resto de proyectos solo lo referencian.
mkdir -p ~/tf-estado/{bootstrap,app} && cd ~/tf-estado/bootstrap
cp ~/tf-st/providers.tf . # data_plane_available = false y storage_use_azuread = true
printf 'terraform.tfstate*\n.terraform/\n*.tfvars\n*.json\n' > ../.gitignore
# bootstrap/main.tf
variable "replicacion" { type = string, default = "LRS" } # en Azure real: ZRS o GZRS. El estado es lo único que no puedes regenerar
variable "asignar_rbac" { type = bool, default = false } # Microsoft.Authorization: solo Azure real
variable "bloquear" { type = bool, default = false } # Microsoft.Authorization/locks: solo Azure real
variable "principal_id" { type = string, default = null } # Object ID de tu usuario o del grupo "tf-operadores"
locals { tags = { proyecto = "tfstate", gestion = "terraform", criticidad = "alta" } }
resource "azurerm_resource_group" "estado" {
name = "rg-tfstate-001"
location = "eastus"
tags = local.tags
lifecycle { ignore_changes = [tags] }
}
resource "random_string" "sufijo" {
length = 8
upper = false
special = false
}
resource "azurerm_storage_account" "estado" {
name = "sttfstate${random_string.sufijo.result}" # 17 caracteres, único global
resource_group_name = azurerm_resource_group.estado.name
location = azurerm_resource_group.estado.location
account_kind = "StorageV2"
account_tier = "Standard"
account_replication_type = var.replicacion
access_tier = "Hot" # el estado se lee en cada plan: nunca Cool
min_tls_version = "TLS1_2"
https_traffic_only_enabled = true
allow_nested_items_to_be_public = false
shared_access_key_enabled = false # SIN claves: el backend usa use_azuread_auth. Quien no tenga rol, no lee el estado
public_network_access_enabled = true # false + private endpoint cuando los agentes de CI estén en tu red
blob_properties {
versioning_enabled = true # cada apply deja una versión anterior recuperable
delete_retention_policy { days = 30 } # un rm del blob se deshace durante 30 días
container_delete_retention_policy { days = 30 }
change_feed_enabled = true # registro de quién escribió qué versión y cuándo
}
tags = local.tags
}
resource "azurerm_storage_container" "tfstate" {
name = "tfstate"
storage_account_id = azurerm_storage_account.estado.id # ARM: funciona en Topaz ([página 5](index.md#pagina-5))
container_access_type = "private"
}
# Solo Azure real: quién puede leer y escribir el estado. Ámbito: el contenedor, no la cuenta ni el grupo
resource "azurerm_role_assignment" "operadores" {
count = var.asignar_rbac && var.principal_id != null ? 1 : 0
scope = azurerm_storage_container.tfstate.id
role_definition_name = "Storage Blob Data Contributor"
principal_id = var.principal_id
}
# Solo Azure real: nadie borra la cuenta del estado por accidente
resource "azurerm_management_lock" "estado" {
count = var.bloquear ? 1 : 0
name = "no-borrar-estado"
scope = azurerm_resource_group.estado.id
lock_level = "CanNotDelete"
}
# bootstrap/outputs.tf
output "cuenta" { value = azurerm_storage_account.estado.name }
output "contenedor" { value = azurerm_storage_container.tfstate.name }
output "bloque_backend" {
description = "Pégalo en el providers.tf de cada proyecto (Azure real)"
value = <<-EOT
backend "azurerm" {
resource_group_name = "${azurerm_resource_group.estado.name}"
storage_account_name = "${azurerm_storage_account.estado.name}"
container_name = "${azurerm_storage_container.tfstate.name}"
key = "<proyecto>/<entorno>.tfstate"
use_azuread_auth = true
}
EOT
}
terraform init && terraform validate
terraform apply -auto-approve # Plan: 4 to add (grupo, sufijo, cuenta, contenedor). Rol y lock: count = 0
terraform output bloque_backend # el texto que usarán los demás proyectos
az storage account show -g rg-tfstate-001 -n "$(terraform output -raw cuenta)" \
--query "{claves:allowSharedKeyAccess, publico:allowBlobPublicAccess, tls:minimumTlsVersion}" -o table
# claves = False: sin rol de datos, nadie (ni tú) puede leer el estado
# NO destruyas el bootstrap todavía: la sección 7.8 lo reutiliza. Su propio estado queda local en este directorio.
🔷 Dónde vive el estado del bootstrap. Tres opciones, de más simple a más ordenada: (1) local, copiado a un lugar seguro (Key Vault como secreto, o el propio contenedor
tfstatesubido a mano una vez); (2) migrado a la cuenta que él mismo creó, coninit -migrate-statetras el primerapply: es legítimo y habitual; (3) una segunda cuenta de bootstrap en otra región. Lo que no debe pasar es que el estado del bootstrap esté en un portátil sin copia: si se pierde, la cuenta sigue existiendo pero habrá queimportarla.
3. Operaciones sobre el estado¶
Todas funcionan igual con estado local y remoto: la familia terraform state y los bloques declarativos moved, import y removed, que dejan el cambio en el código y en el historial de Git en lugar de en la terminal de una persona.
| Comando o bloque | Qué hace | Toca Azure |
|---|---|---|
state list / state show <dir> |
Lista direcciones; muestra los atributos guardados de una (incluidos secretos) | No |
state pull > copia.json |
Descarga el estado tal cual. Es la copia de seguridad manual antes de cualquier cirugía | No |
state push copia.json |
Sube un estado. Rechaza serial menor o lineage distinto; -force lo salta (última opción) |
No |
moved { from = A to = B } |
Renombrar un recurso o módulo sin destruirlo. Sustituye a state mv; se borra del código tras aplicarlo |
No |
import { to = A id = "/subscriptions/…" } |
Adoptar un recurso creado a mano. plan -generate-config-out=gen.tf escribe el bloque por ti |
Lee |
removed { from = A lifecycle { destroy = false } } |
Olvidar un recurso sin borrarlo en Azure (pasa a otro proyecto o a gestión manual). Sustituye a state rm |
No |
plan -refresh-only / apply -refresh-only |
Detectar drift (cambios hechos fuera de Terraform) y aceptar la realidad en el estado sin tocar recursos | Lee |
apply -replace=<dir> |
Recrear un recurso concreto (sustituye al antiguo taint) |
Sí |
⚠️ Nunca edites el
tfstatea mano. El original subía el archivo conaz storage blob uploady después ejecutabainit -migrate-state: eso produce dos estados con distintolineagey Terraform pregunta cuál sobrescribir. La secuencia es siemprestate pull(copia), cambio conmoved/removed/state mv, yplanhasta ver No changes. Si unstate pushexige-force, algo has entendido mal: para antes.
4. Bloqueos: cómo funcionan de verdad¶
El original afirmaba que "Azure Storage no tiene locks nativos" y proponía crear un lease a mano. Es al revés: el backend azurerm toma un lease infinito sobre el blob del estado al empezar cada plan, apply o comando state, escribe quién lo tiene en los metadatos (terraformlockid) y lo libera al terminar. Si tú creas el lease con az storage blob lease create, lo que consigues es que Terraform no pueda escribir.
Terminal A │ Terminal B
──────────────────────────────────────────────┼──────────────────────────────────────────────
terraform apply │
→ adquiere lease del blob app/lab.tfstate │
→ escribe metadata terraformlockid │ terraform plan
→ "Do you want to perform these actions?" │ Error: Error acquiring the state lock
(espera tu respuesta: el lock sigue tomado) │ Lock Info:
│ ID: 8f2c…-… ◄── lo que pide force-unlock
│ Path: tfstate/app/lab.tfstate
│ Operation: OperationTypeApply
│ Who: ana@portatil-ana
│ Created: 2026-09-10 13:04:11 UTC
yes │
→ aplica, escribe el estado, libera lease │ terraform plan -lock-timeout=5m ◄── mejor que fallar: espera
│ (reintenta hasta 5 min y continúa)
| Situación | Qué hacer |
|---|---|
| Otra persona o el pipeline está aplicando | Esperar. -lock-timeout=10m en CI para que el job reintente en vez de fallar. Mirar Who, no adivinar |
| El proceso que tenía el lock murió (Ctrl+C, agente cancelado, portátil apagado) | Comprobar que no hay ningún terraform vivo (Who dice dónde). Luego terraform force-unlock <ID>: usa el ID para asegurarse de que liberas ese lock |
force-unlock falla o el ID se ha perdido |
az storage blob lease break --blob-name app/lab.tfstate -c tfstate --account-name … --auth-mode login y borrar el metadato terraformlockid |
| Ver el lock desde fuera de Terraform | az storage blob show … --query "{lease:properties.lease.status, lock:metadata.terraformlockid}"; el valor es el Lock Info en base64 |
Tentación: -lock=false |
Solo para plan de lectura en CI de PR, nunca para apply. Es la forma más rápida de corromper un estado compartido |
5. Configurar el backend¶
# providers.tf del proyecto de aplicación (Azure real). El bloque backend NO admite variables ni interpolación.
terraform {
required_version = ">= 1.7.0"
required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" } }
backend "azurerm" {
resource_group_name = "rg-tfstate-001"
storage_account_name = "sttfstateXXXXXXXX" # salida del bootstrap
container_name = "tfstate"
key = "app/lab.tfstate" # <proyecto>/<entorno>: nunca "terraform.tfstate" para todo
use_azuread_auth = true # Entra ID; la cuenta tiene las claves desactivadas
snapshot = true # snapshot del blob antes de cada escritura (además del versionado)
# use_oidc = true # en GitHub Actions / Azure DevOps (página 6)
# use_msi = true # en una VM o agente con identidad gestionada
}
}
# Configuración parcial: el mismo código para lab, pre y pro. En el .tf solo queda el tipo:
terraform { backend "azurerm" {} }
# backends/lab.hcl
resource_group_name = "rg-tfstate-001"
storage_account_name = "sttfstateXXXXXXXX"
container_name = "tfstate"
key = "app/lab.tfstate"
use_azuread_auth = true
terraform init -backend-config=backends/lab.hcl # o -backend-config="key=app/pro.tfstate" suelto
terraform init -backend-config=backends/pro.hcl -reconfigure # cambiar de entorno: -reconfigure, NO -migrate-state
| Decisión | Recomendación |
|---|---|
| Un estado por… | Proyecto y entorno. Un estado gigante hace lento cada plan y convierte cualquier error en un incidente global. Los proyectos se enlazan con data "terraform_remote_state" o, mejor, con data sources por nombre |
Entornos: key distinta o workspaces |
key + configuración parcial. Los workspaces (terraform workspace new pro) guardan el blob como app.tfstateenv:pro, comparten backend y credenciales, y es fácil aplicar en pro creyendo estar en lab |
| Replicación de la cuenta | ZRS como mínimo; GZRS si la región cae y debes seguir operando. El estado es el único artefacto que no puedes regenerar con un apply |
access_key / sas_token |
No. Existen por compatibilidad. Con shared_access_key_enabled = false en la cuenta, ni siquiera funcionan |
| Red | Producción: public_network_access_enabled = false + private endpoint, y los agentes de CI dentro de la VNet. Mientras tanto, reglas de firewall con las IPs de los agentes |
6. Quién accede y cómo¶
| Quién | Cómo se autentica | Rol y ámbito |
|---|---|---|
| Personas del equipo | az login + use_azuread_auth |
Storage Blob Data Contributor sobre el contenedor tfstate, asignado a un grupo de Entra ID (tf-operadores), no persona a persona |
| Revisores, auditoría | Igual | Storage Blob Data Reader: pueden plan y state list, no apply |
| GitHub Actions / Azure DevOps | OIDC: use_oidc = true o ARM_USE_OIDC=true (página 6) |
Contributor sobre el contenedor, con key restringida por condición ABAC si hay varios equipos en la misma cuenta |
| Agente en una VM de Azure | use_msi = true: identidad gestionada de la VM (identity { type = "SystemAssigned" } en azurerm_linux_virtual_machine) |
Igual que el pipeline. No hay ningún secreto en la VM |
| Cualquiera con la clave de cuenta | Imposible: shared_access_key_enabled = false |
La clave no se puede auditar ni caduca; el original la usaba incluso para crear el contenedor |
🔷 Contributor en el grupo no sirve para nada aquí. El original asignaba Contributor sobre
rg-terraform-backend"para acceder al backend". Contributor es un rol de ARM: permite crear y borrar la cuenta, pero no leer un blob. Leer y escribir el estado exige un rol de datos. Al revés también: quien solo tiene Storage Blob Data Contributor puede usar el estado pero no puede tocar la cuenta, que es justo lo que quieres para un pipeline.
7. Laboratorio en Topaz: la mecánica con backend local¶
El backend local con path explícito se comporta como cualquier otro: init -migrate-state hace las mismas preguntas, los bloqueos producen el mismo mensaje y los comandos state son idénticos. Practicas aquí la secuencia exacta que repetirás en 7.8 con azurerm.
cd ~/tf-estado/app && cp ~/tf-st/providers.tf .
# app/main.tf: recursos de red (Microsoft.Network, ✅ Topaz)
cat > main.tf <<'EOF'
resource "azurerm_resource_group" "app" {
name = "rg-app-lab-001"
location = "eastus"
lifecycle { ignore_changes = [tags] }
}
resource "azurerm_virtual_network" "app" {
name = "vnet-app-lab"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
address_space = ["10.70.0.0/16"]
}
resource "azurerm_subnet" "app" {
for_each = { web = 1, datos = 2 }
name = "snet-${each.key}"
resource_group_name = azurerm_resource_group.app.name
virtual_network_name = azurerm_virtual_network.app.name
address_prefixes = [cidrsubnet("10.70.0.0/16", 8, each.value)]
}
EOF
terraform init && terraform apply -auto-approve # 4 to add; estado en ./terraform.tfstate
ls terraform.tfstate* # terraform.tfstate (y .backup tras el segundo apply)
# ─── 1. Anatomía ───────────────────────────────────────────────────────────────
terraform state pull | jq '{version, terraform_version, serial, lineage, recursos: [.resources[] | .type + "." + .name]}'
terraform state show 'azurerm_subnet.app["datos"]' # los atributos guardados; con comillas por los corchetes
# ─── 2. Migración (misma mecánica que hacia azurerm) ────────────────────────────
mkdir -p estados
cat > backend.tf <<'EOF'
terraform { backend "local" { path = "estados/app-lab.tfstate" } }
EOF
terraform init -migrate-state
# Do you want to copy existing state to the new backend? → yes
# Successfully configured the backend "local"!
terraform plan # No changes: la migración fue limpia
ls estados/ # app-lab.tfstate
rm terraform.tfstate terraform.tfstate.backup # ya no se usan (Terraform los deja como reliquia)
# ─── 3. Bloqueo ────────────────────────────────────────────────────────────────
terraform apply # NO respondas todavía: tiene el lock
# En OTRA terminal:
# cd ~/tf-estado/app && terraform plan
# Error: Error acquiring the state lock ... Lock Info: ID, Operation, Who, Created
# terraform plan -lock-timeout=2m # espera; responde "no" en la primera y verás cómo continúa
# terraform force-unlock <ID> # solo si la primera terminal hubiera muerto
# ─── 4. Refactor sin destruir: moved ───────────────────────────────────────────
sed -i 's/"azurerm_virtual_network" "app"/"azurerm_virtual_network" "principal"/; s/azurerm_virtual_network.app.name/azurerm_virtual_network.principal.name/' main.tf
terraform plan # 1 to add, 1 to destroy: renombrar = recrear. MAL
cat >> main.tf <<'EOF'
moved {
from = azurerm_virtual_network.app
to = azurerm_virtual_network.principal
}
EOF
terraform plan # "azurerm_virtual_network.app has moved to …principal" 0 to add
terraform apply -auto-approve # solo actualiza el estado; el bloque moved se puede borrar después
# ─── 5. Drift: alguien toca Azure a mano ───────────────────────────────────────
az network vnet subnet delete -g rg-app-lab-001 --vnet-name vnet-app-lab -n snet-datos
terraform plan -refresh-only # "azurerm_subnet.app["datos"] has been deleted": solo informa
terraform plan # 1 to add: lo recrearía
terraform apply -auto-approve # lo recrea. (apply -refresh-only lo habría olvidado en vez de recrearlo)
# ─── 6. Olvidar sin borrar: removed ────────────────────────────────────────────
cat >> main.tf <<'EOF'
removed {
from = azurerm_subnet.app["web"]
lifecycle { destroy = false }
}
EOF
terraform apply -auto-approve # "will no longer be managed" ; sigue existiendo en Azure
az network vnet subnet show -g rg-app-lab-001 --vnet-name vnet-app-lab -n snet-web --query name -o tsv
# ─── 7. Adoptar: import ────────────────────────────────────────────────────────
# Borra el bloque removed y devuelve la subred al estado:
sed -i '/^removed {/,/^}/d' main.tf
cat > import.tf <<EOF
import {
to = azurerm_subnet.app["web"]
id = "$(az network vnet subnet show -g rg-app-lab-001 --vnet-name vnet-app-lab -n snet-web --query id -o tsv)"
}
EOF
terraform plan # "1 to import"
terraform apply -auto-approve && rm import.tf
# ─── 8. Copia y restauración manual ────────────────────────────────────────────
terraform state pull > copia-$(date +%F).json
terraform state push copia-$(date +%F).json # sube tal cual: mismo lineage, serial >= actual
terraform plan # No changes
# ─── 9. Volver a local implícito y limpiar ─────────────────────────────────────
rm backend.tf && terraform init -migrate-state # el estado vuelve a ./terraform.tfstate
terraform destroy -auto-approve
🔷 Lo que cambia con
azurerm: casi nada. Los pasos 1 a 9 son los mismos. Las diferencias: el lock es un lease en el blob (y se puede ver y romper conaz), elstate pullde seguridad se vuelve opcional porque el versionado ya guarda cada escritura, yinit -migrate-statepide además que estés autenticado con un rol de datos. Por eso el bootstrap tiene las claves desactivadas: el primer error que verás en Azure real si falta el rol es un 403, no un estado corrupto.
8. Azure real: activar el backend, restaurar y auditar¶
# Bootstrap: quitar metadata_host y resource_provider_registrations, subscription_id real, data_plane_available = true
cd ~/tf-estado/bootstrap && az login
GRUPO=$(az ad group create --display-name tf-operadores --mail-nickname tf-operadores --query id -o tsv)
az ad group member add -g "$GRUPO" --member-id "$(az ad signed-in-user show --query id -o tsv)"
terraform init -reconfigure
terraform apply -auto-approve -var replicacion=ZRS -var asignar_rbac=true -var principal_id="$GRUPO" -var bloquear=true
terraform output bloque_backend # copia el bloque (o escribe backends/lab.hcl)
ST=$(terraform output -raw cuenta)
# App: activar el backend y migrar
cd ../app && terraform output bloque_backend -state=../bootstrap/terraform.tfstate -raw > backend.tf 2>/dev/null || true
# (o pega el bloque a mano dentro de terraform { … } y cambia key por "app/lab.tfstate")
terraform init -migrate-state # copia el estado local al blob. El rol tarda 1-5 min: si 403, espera
terraform plan # No changes
rm -f terraform.tfstate terraform.tfstate.backup # el local ya es una reliquia con secretos: bórralo
az storage blob list --account-name "$ST" -c tfstate --auth-mode login -o table # app/lab.tfstate
# Ver el lock mientras otro apply está esperando confirmación
az storage blob show --account-name "$ST" -c tfstate -n app/lab.tfstate --auth-mode login \
--query "{lease:properties.lease.status, lock:metadata.terraformlockid}" -o table
echo "<valor de lock>" | base64 -d | jq . # ID, Who, Created
# Restaurar la versión anterior del estado (versionado del bootstrap)
az storage blob list --account-name "$ST" -c tfstate --prefix app/lab.tfstate --include v --auth-mode login \
--query "[].{version:versionId, actual:isCurrentVersion, bytes:properties.contentLength}" -o table
az storage blob download --account-name "$ST" -c tfstate -n app/lab.tfstate --auth-mode login \
--version-id "<versionId anterior>" -f anterior.json
jq '.serial' anterior.json # debe ser menor que el actual: sube el serial antes de push
terraform state push anterior.json # rechaza serial menor: edita .serial al actual+1, o usa -force con cuidado
# Quién ha leído o escrito el estado: logs de diagnóstico del servicio Blob a Log Analytics (Microsoft.Insights)
az monitor diagnostic-settings create --name audit-tfstate \
--resource "$(az storage account show -g rg-tfstate-001 -n $ST --query id -o tsv)/blobServices/default" \
--workspace "<id del workspace>" --logs '[{"category":"StorageRead","enabled":true},{"category":"StorageWrite","enabled":true}]'
# KQL: StorageBlobLogs | where ObjectKey endswith "lab.tfstate" | project TimeGenerated, OperationName, RequesterUpn, StatusCode
# Destruir en orden: primero la app, luego devolver el bootstrap a local, luego quitar el lock y destruir
terraform destroy -auto-approve
cd ../bootstrap && terraform apply -auto-approve -var asignar_rbac=true -var principal_id="$GRUPO" -var bloquear=false
terraform destroy -auto-approve
| Capa de protección | Contra qué | Cómo se recupera |
|---|---|---|
| Lease del backend | Dos escrituras simultáneas | No hay nada que recuperar: la segunda operación espera o falla antes de tocar el blob |
serial y lineage |
Subir un estado viejo o de otro proyecto | state push lo rechaza; solo -force lo salta |
Versionado + snapshot = true |
Un apply que dejó el estado mal |
az storage blob download --version-id + state push (ajustando serial) |
| Soft delete (30 días) | Borrar el blob o el contenedor | az storage blob undelete / az storage container restore |
Bloqueo CanNotDelete |
Borrar la cuenta o el grupo | No hace falta: la operación falla con ScopeLocked |
| ZRS / GZRS | Caída de un centro de datos o de la región | Transparente (ZRS); failover de cuenta o lectura en la secundaria (RA-GZRS) |
| Logs de diagnóstico | No saber quién hizo qué | Consulta KQL en Log Analytics: identidad, operación, hora, código de estado |
9. Errores comunes¶
⚠️ Solución de problemas
Mensaje o síntoma Causa y solución no such host / could not resolve en initcon backendazurermen TopazEl backend usa el plano de datos del blob, que el emulador no tiene. En Topaz, backend local(7.7); el bloqueazurermsolo en Azure realBackend initialization required / Backend configuration changed Has tocado el bloque backend. Si quieres mover el estado:init -migrate-state. Si solo cambias de entorno con configuración parcial:init -reconfigure. Confundirlos copia el estado delabencima del deproError acquiring the state lock … LeaseAlreadyPresent Otra operación en curso o un lease huérfano. Lee WhoyCreated; si el proceso no existe,force-unlock <ID>; si tú creaste el lease a mano siguiendo el original,az storage blob lease breakFailed to unlock state: … lock ID does not match Has pasado un ID de otro lock (o inventado). Copia el ID exacto del mensaje de error; nunca uses -lock=falsecomo atajoAuthorizationPermissionMismatch (403) en initoplanTienes rol de ARM pero no de datos. Necesitas Storage Blob Data Contributor (o Reader para solo plan) sobre el contenedor; tras asignarlo, espera hasta 5 minutosKeyBasedAuthenticationNotPermitted La cuenta tiene shared_access_key_enabled = falsey faltause_azuread_auth = trueen el backend (o--auth-mode loginenaz). Es la cuenta haciendo su trabajoVariables not allowed en el bloque backendEl backend se evalúa antes que las variables. Usa configuración parcial: backend "azurerm" {}+-backend-config=backends/lab.hclError: state snapshot was created by Terraform v1.10.x, which is newer than current v1.9.x Alguien aplicó con una versión más nueva. Fija required_versiony la misma versión en CI y portátiles (.terraform-versioncon tenv/tfenv)state push: serial … is lower than the current / lineage mismatchIntentas subir una versión antigua o de otro proyecto. Si es una restauración deliberada, edita serialal actual + 1; si el linaje no coincide, es otro estado: paraTras la migración, planquiere crear todo de nuevoRespondiste no a "copy existing state" o la keyapunta a un blob distinto. Vuelve al backend anterior con-reconfigure, compruebastate listy repite la migraciónEl planpropone destruir y recrear tras renombrar un recurso o móduloRenombrar cambia la dirección en el estado. Añade un bloque moved(ostate mv) antes de aplicarResource already managed by Terraform al importar Ya está en el estado con otra dirección. state list | grepy usamoveden vez deimportScopeLocked al destruir el bootstrap El bloqueo CanNotDelete. Primeroapply -var bloquear=false, despuésdestroyAparece terraform.tfstateengit statusFalta el .gitignoreo está en otro directorio. Si ya se ha subido, contiene secretos: rota las contraseñas y claves que incluya, no basta con borrar el commit
10. Autoevaluación¶
- ¿Para qué sirven
serialylineage?serialcrece en cada escritura y evita pisar un estado más reciente;lineageidentifica el estado desde su creación y impide mezclar dos proyectos.state pushcomprueba ambos. - ¿Por qué el backend
azurermno funciona en Topaz y qué se practica en su lugar? Lee y escribe el blob por el plano de datos, que el emulador no implementa. Se practica la misma mecánica (migración, bloqueo,state,moved,import,removed) con el backendlocalypathexplícito. - ¿Qué pasa si creas un lease a mano sobre el blob del estado, como proponía el original? Terraform no puede escribir: el backend toma el lease él mismo en cada operación y lo libera al terminar. El lease manual es un bloqueo contra Terraform, no a su favor.
- ¿Cómo sabes quién tiene el lock?
El mensaje de error incluye
ID,Who,OperationyCreated. Desde fuera, el metadatoterraformlockiddel blob (base64).state pullno muestra nada de eso. - ¿Cuándo usas
init -migrate-statey cuándoinit -reconfigure?-migrate-statecopia el estado del backend anterior al nuevo (local → azurerm).-reconfiguredescarta el anterior sin copiar: es el correcto al cambiar de entorno con configuración parcial. - ¿Por qué el contenedor del estado se protege como un Key Vault?
El
tfstatecontiene en claro todas las contraseñas, claves y cadenas de conexión de la infraestructura.sensitive = truesolo oculta la consola. - ¿Por qué Contributor sobre el grupo de recursos no permite usar el backend?
Es un rol de ARM: crea y borra la cuenta, pero no lee blobs. Leer y escribir el estado exige un rol de datos: Storage Blob Data Contributor (o Reader para solo
plan). - ¿Qué diferencia hay entre
apply -refresh-onlyyapplytras un drift?-refresh-onlyacepta la realidad en el estado (si borraron una subred, la olvida).applynormal la recrea para que Azure vuelva a coincidir con el código. - ¿Cómo restauras el estado tras un
applyque lo dejó mal? Descargas la versión anterior del blob con--version-id(versionado del bootstrap), ajustasserialal actual + 1 y hacesstate push. Después,planhasta ver No changes. - ¿Por qué una
keypor proyecto y entorno en vez deterraform.tfstatepara todo? Un estado gigante hace lento cadaplan, convierte cualquier error en incidente global y obliga a dar acceso a todo a quien solo necesita una parte. - ¿Qué tres cosas hacen que el bootstrap sea seguro sin claves?
shared_access_key_enabled = false(solo Entra ID), Storage Blob Data Contributor asignado a un grupo sobre el contenedor, y versionado + soft delete + bloqueoCanNotDeletepara recuperar y no perder.
11. Referencias¶
- El estado de Terraform, datos sensibles en el estado y bloqueo del estado
- Backend
azurerm, configuración parcial y backendlocal - Comandos
terraform state,force-unlockyinit -migrate-state/-reconfigure - Bloque
moved, bloqueimport, bloqueremovedy modo-refresh-only - Workspaces y
terraform_remote_state - Almacenar el estado en Azure Storage (Microsoft Learn)
- Versionado de blobs, soft delete, change feed y leases de blob
- Desactivar la autorización por clave compartida y roles RBAC de datos
- Monitorizar Blob Storage y tabla
StorageBlobLogs - Identidades gestionadas y OIDC desde GitHub Actions
- Azure Local Emulator (Topaz)