Terraform guarda en su archivo de estado la relación entre cada recurso de tu código y el objeto real que gestiona. Cuando esa relación falla (un recurso creado a mano, un renombrado en el código, un cambio hecho fuera de Terraform), tienes que intervenir en el estado. En este tutorial practicarás las operaciones más habituales en Ubuntu 24.04: importar un recurso existente, renombrarlo sin destruirlo, dejar de gestionarlo, migrar el estado a otro backend y detectar el drift.
Para que todo sea reproducible en un solo servidor, el ejemplo usa el proveedor de Docker y una red Docker creada a mano. Las mismas órdenes y bloques funcionan con AWS, Azure, Google Cloud o cualquier otro proveedor que soporte importación.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. - Terraform 1.7 o posterior, instalado desde el repositorio oficial de HashiCorp (los bloques
removednecesitan la 1.7). - Docker Engine instalado y tu usuario en el grupo
docker, de forma quedocker psfuncione sinsudo. - Conocimientos básicos de HCL y del ciclo
init,plan,apply.
Comprueba ambas herramientas antes de empezar:
terraform version
docker network ls
Paso 1: Crear un recurso fuera de Terraform
Simula la situación típica: alguien creó un recurso a mano y ahora quieres gestionarlo con Terraform. Crea una red Docker:
docker network create red-app
Anota su ID, que es el identificador que usará la importación:
docker network inspect -f '{{.Id}}' red-app
3f9c2d7e5a41b8c0e6d1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7
Paso 2: Preparar el proyecto de Terraform
Crea el directorio del proyecto:
mkdir -p ~/tf-estado && cd ~/tf-estado
Define el proveedor en providers.tf:
nano providers.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
Inicializa el proyecto para descargar el proveedor:
terraform init
Terraform has been successfully initialized!
Paso 3: Importar el recurso con un bloque import
Desde Terraform 1.5, la forma recomendada de importar es declarar un bloque import en el código. A diferencia de la orden terraform import, se revisa en el plan antes de tocar el estado y puede quedar en el repositorio como registro.
Crea red.tf con el recurso y el bloque de importación. Sustituye el ID por el que obtuviste en el paso 1:
nano red.tf
import {
to = docker_network.app
id = "3f9c2d7e5a41b8c0e6d1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7"
}
resource "docker_network" "app" {
name = "red-app"
}
Revisa el plan:
terraform plan
docker_network.app: Preparing import... [id=3f9c2d7e5a41...]
docker_network.app: Refreshing state... [id=3f9c2d7e5a41...]
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
La línea clave es 1 to import, 0 to add, 0 to change, 0 to destroy. Si el plan muestra cambios o un reemplazo (must be replaced), tu bloque resource no coincide con el recurso real: ajusta los atributos que indica el plan hasta que desaparezcan. Nunca apliques una importación que vaya a reemplazar el recurso.
Aplica la importación:
terraform apply
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.
Comprueba que el recurso ya está en el estado:
terraform state list
terraform state show docker_network.app
docker_network.app
# docker_network.app:
resource "docker_network" "app" {
driver = "bridge"
id = "3f9c2d7e5a41..."
name = "red-app"
...
}
Una vez aplicada, puedes borrar el bloque import de red.tf; ya no hace nada.
Generar la configuración automáticamente
Si el recurso tiene muchos atributos, deja solo el bloque import (sin el bloque resource) y pide a Terraform que escriba la configuración:
terraform plan -generate-config-out=generado.tf
Terraform crea generado.tf con todos los atributos que lee del proveedor. Revísalo, elimina los valores por defecto que no aportan nada y mueve el bloque a su archivo definitivo antes de aplicar. La función es útil como punto de partida, no como código final.
La orden terraform import
La orden clásica sigue disponible y modifica el estado de inmediato, sin plan previo. Requiere que el bloque resource ya exista:
terraform import docker_network.app 3f9c2d7e5a41b8c0e6d1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7
Úsala solo para casos puntuales; el bloque import es más seguro en equipo.
Paso 4: Hacer una copia del estado antes de modificarlo
Antes de cualquier cambio manual en el estado, guarda una copia. terraform state pull funciona igual con estado local o remoto:
terraform state pull > estado-$(date +%Y%m%d-%H%M).json
ls estado-*.json
El archivo de estado puede contener datos sensibles (contraseñas generadas, claves privadas, IPs internas). No lo subas nunca a un repositorio y añade *.tfstate* y estas copias a .gitignore.
Paso 5: Renombrar un recurso con un bloque moved
Si cambias el nombre local de un recurso en el código, Terraform lo interpreta como "destruir el antiguo y crear uno nuevo". El bloque moved le indica que es el mismo objeto con otra dirección.
Edita red.tf, cambia el nombre local de app a principal y añade el bloque moved:
nano red.tf
resource "docker_network" "principal" {
name = "red-app"
}
moved {
from = docker_network.app
to = docker_network.principal
}
Revisa el plan:
terraform plan
# docker_network.app has moved to docker_network.principal
resource "docker_network" "principal" {
name = "red-app"
...
}
Plan: 0 to add, 0 to change, 0 to destroy.
Aplica con terraform apply y comprueba el nuevo nombre con terraform state list. El mismo mecanismo sirve para mover un recurso dentro de un módulo (to = module.red.docker_network.principal).
La alternativa imperativa es terraform state mv docker_network.app docker_network.principal, que cambia el estado al instante. Prefiere moved: queda en el código y el resto del equipo recibe el mismo cambio al hacer plan.
Paso 6: Dejar de gestionar un recurso sin destruirlo
A veces un recurso debe salir de Terraform pero seguir existiendo, por ejemplo porque pasa a gestionarlo otro equipo. Desde Terraform 1.7, el bloque removed lo expresa en el código.
Sustituye el contenido de red.tf por:
removed {
from = docker_network.principal
lifecycle {
destroy = false
}
}
Revisa el plan:
terraform plan
# docker_network.principal will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
Plan: 0 to add, 0 to change, 0 to destroy.
Aplica y verifica que la red sigue existiendo aunque ya no esté en el estado:
terraform apply
terraform state list
docker network ls --filter name=red-app
NETWORK ID NAME DRIVER SCOPE
3f9c2d7e5a41 red-app bridge local
El equivalente imperativo es terraform state rm docker_network.principal. Para seguir con el tutorial, vuelve a importar la red repitiendo el paso 3 con el nombre principal y borra el bloque removed.
Paso 7: Detectar y corregir el drift
El drift aparece cuando alguien cambia la infraestructura sin pasar por Terraform. Provócalo borrando la red a mano:
docker network rm red-app
Un plan en modo -refresh-only muestra qué ha cambiado fuera de Terraform sin proponer cambios en la infraestructura:
terraform plan -refresh-only
Note: Objects have changed outside of Terraform
# docker_network.principal has been deleted
- resource "docker_network" "principal" {
...
}
Ahora decides qué versión es la correcta:
- Si el cambio manual era un error, ejecuta
terraform applyy Terraform recreará la red según el código. - Si el cambio manual es el nuevo estado deseado, ejecuta
terraform apply -refresh-onlypara aceptar la realidad en el estado y después adapta el código.
En este caso, recrea la red:
terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Nota
terraform refreshestá obsoleto. Usaterraform plan -refresh-onlyyterraform apply -refresh-only, que muestran los cambios antes de escribirlos en el estado.
Para detectar drift de forma periódica, ejecuta terraform plan -detailed-exitcode en tu CI: devuelve 0 si no hay cambios, 2 si hay diferencias y 1 si hay un error.
Paso 8: Migrar el estado a un backend remoto
Un estado local en un único servidor no sirve para trabajar en equipo: no tiene bloqueo compartido ni copias automáticas. Para moverlo a un backend remoto, añade el bloque backend y ejecuta terraform init -migrate-state.
Este ejemplo usa un bucket de Amazon S3 con bloqueo nativo mediante archivo (use_lockfile, disponible desde Terraform 1.10). El bucket debe existir y tener el versionado activado, y necesitas credenciales de AWS en el entorno:
nano backend.tf
terraform {
backend "s3" {
bucket = "mi-empresa-terraform-state"
key = "tf-estado/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}
Migra el estado y responde yes cuando Terraform pregunte si quieres copiarlo:
terraform init -migrate-state
Do you want to copy existing state to the new backend?
Enter a value: yes
Successfully configured the backend "s3"!
Verifica que el estado remoto contiene los mismos recursos y que el plan no propone cambios:
terraform state list
terraform plan
Cuando lo confirmes, guarda aparte y elimina terraform.tfstate y terraform.tfstate.backup del directorio para evitar confusiones. Si en lugar de S3 usas HCP Terraform, el proceso es el mismo con un bloque cloud.
Solución de problemas
Error acquiring the state lock. Otra ejecución tiene el estado bloqueado. Espera a que termine. Si el proceso murió (un CI cancelado, una sesión SSH cortada) y estás seguro de que no hay nada en marcha, libera el bloqueo con el ID que aparece en el mensaje:
terraform force-unlock LOCK_ID
Resource already managed by Terraform al importar. La dirección ya existe en el estado. Comprueba con terraform state show si es el mismo objeto; si no lo es, elimínala con un bloque removed o terraform state rm antes de importar.
El plan de importación propone reemplazar el recurso. Algún atributo que fuerza la recreación no coincide (en redes Docker, por ejemplo, driver o ipam_config). Copia el valor real que muestra terraform plan en tu bloque resource.
Estado dañado tras una edición manual. Restaura la copia del paso 4. En un backend remoto, súbela con terraform state push estado-AAAAMMDD-HHMM.json. Terraform rechaza el push si el número de serie es anterior al actual; en ese caso comprueba bien qué copia es la correcta antes de usar -force.
Conclusión
Has importado un recurso existente con un bloque import, lo has renombrado con moved, lo has sacado de Terraform con removed sin destruirlo, has detectado y corregido el drift y has migrado el estado a un backend remoto. Como siguientes pasos, activa el versionado del bucket de estado, ejecuta terraform plan -detailed-exitcode de forma programada para vigilar el drift y reorganiza tu código en módulos usando bloques moved para no recrear nada.
