Terraform guarda en un archivo de estado (terraform.tfstate) la relación entre tu código y los recursos reales que ha creado. Mientras trabajas solo, ese archivo local basta, pero en cuanto dos personas o un pipeline de CI aplican cambios, necesitas un backend remoto con bloqueo para que nadie pise el estado de otro. En este tutorial moverás el estado de un proyecto de ejemplo a un bucket S3 con bloqueo nativo, verás cómo inspeccionarlo y repararlo con los comandos terraform state, y separarás entornos con workspaces, todo desde Ubuntu 24.04.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor o equipo con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. - Una cuenta de AWS con un usuario IAM y su par de claves de acceso (
AWS_ACCESS_KEY_IDyAWS_SECRET_ACCESS_KEY) con permisos sobre S3. Si usas otro almacenamiento compatible con S3, consulta la sección dedicada más abajo. - Terraform 1.10 o superior. El bloqueo nativo en S3 (
use_lockfile) no existe en versiones anteriores.
Paso 1: Instalar Terraform y la CLI de AWS
Instala Terraform desde el repositorio oficial de HashiCorp. Primero descarga la clave del repositorio:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg
Añade el repositorio:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(. /etc/os-release && echo "$VERSION_CODENAME") main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
Instala el paquete:
sudo apt update
sudo apt install terraform
Ubuntu 24.04 ya no incluye el paquete awscli en sus repositorios, así que instala la CLI de AWS v2 con el snap oficial:
sudo snap install aws-cli --classic
Comprueba las dos herramientas:
terraform version
aws --version
Terraform v1.13.3
on linux_amd64
aws-cli/2.27.50 Python/3.13.4 Linux/6.8.0-79-generic exe/x86_64.ubuntu.24
Las versiones exactas pueden variar; lo importante es que Terraform sea 1.10 o posterior.
Paso 2: Crear un proyecto con estado local
Para ver el proceso completo, empieza con un proyecto que guarda el estado en local. Usa el proveedor random, que no crea nada en ninguna nube, así puedes practicar sin coste:
mkdir -p ~/tf-estado && cd ~/tf-estado
nano main.tf
terraform {
required_version = ">= 1.10"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
resource "random_pet" "servidor" {
length = 2
}
output "nombre" {
value = random_pet.servidor.id
}
Inicializa y aplica:
terraform init
terraform apply -auto-approve
random_pet.servidor: Creating...
random_pet.servidor: Creation complete after 0s [id=calm-falcon]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
nombre = "calm-falcon"
Ahora existe un archivo terraform.tfstate en el directorio. Es JSON y contiene todos los atributos de cada recurso:
terraform state list
random_pet.servidor
Advertenciael estado guarda en texto plano todos los atributos de los recursos, incluidas contraseñas y claves generadas o leídas por Terraform. Nunca lo subas a Git; añade
*.tfstate,*.tfstate.*y.terraform/a tu.gitignore.
Paso 3: Crear el bucket S3 para el estado
Configura las credenciales de AWS en la sesión. Sustituye los valores por los de tu usuario IAM:
export AWS_ACCESS_KEY_ID="tu_access_key"
export AWS_SECRET_ACCESS_KEY="tu_secret_key"
export AWS_REGION="eu-west-1"
Los nombres de bucket son globales en S3, así que elige uno único. En esta guía se usa tu-empresa-tfstate:
aws s3api create-bucket \
--bucket tu-empresa-tfstate \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
Activa el versionado. Es tu red de seguridad: si alguien corrompe o borra el estado, podrás recuperar la versión anterior:
aws s3api put-bucket-versioning \
--bucket tu-empresa-tfstate \
--versioning-configuration Status=Enabled
Bloquea cualquier acceso público al bucket:
aws s3api put-public-access-block \
--bucket tu-empresa-tfstate \
--public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Verifica el versionado:
aws s3api get-bucket-versioning --bucket tu-empresa-tfstate
{
"Status": "Enabled"
}
Los objetos nuevos en S3 se cifran en reposo por defecto con SSE-S3, así que no necesitas configurar nada más para el cifrado básico.
Paso 4: Configurar el backend S3 y migrar el estado
Añade un bloque backend "s3" dentro del bloque terraform de main.tf:
nano main.tf
terraform {
required_version = ">= 1.10"
backend "s3" {
bucket = "tu-empresa-tfstate"
key = "proyectos/tf-estado/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
Qué hace cada opción:
key: ruta del objeto dentro del bucket. Usa una por proyecto para poder compartir el mismo bucket.encrypt: fuerza el cifrado del objeto de estado en el servidor.use_lockfile: activa el bloqueo nativo de S3. Terraform crea un objetoterraform.tfstate.tflockjunto al estado mientras dura cada operación y lo borra al terminar. Sustituye a la antigua tabla de DynamoDB, que HashiCorp ha marcado como obsoleta.
Los bloques backend no admiten variables. Nunca pongas credenciales en ellos: Terraform las lee de las variables de entorno que exportaste en el paso anterior.
Reinicializa el proyecto indicando que quieres migrar el estado existente:
terraform init -migrate-state
Terraform detecta el cambio de backend y pregunta si debe copiar el estado local:
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Comprueba que el estado está en el bucket:
aws s3 ls s3://tu-empresa-tfstate/proyectos/tf-estado/
2026-09-25 10:12:41 871 terraform.tfstate
Y que Terraform lo lee desde allí sin detectar cambios:
terraform plan
random_pet.servidor: Refreshing state... [id=calm-falcon]
No changes. Your infrastructure matches the configuration.
Una vez verificado, borra la copia local, que ya no se usa y contiene datos sensibles:
rm terraform.tfstate terraform.tfstate.backup
Configuración parcial del backend
Si el mismo código se despliega en varias cuentas o buckets, deja fuera del código los valores que cambian y pásalos en terraform init. Crea un archivo backend.hcl:
nano backend.hcl
bucket = "tu-empresa-tfstate"
region = "eu-west-1"
En main.tf deja solo lo común:
backend "s3" {
key = "proyectos/tf-estado/terraform.tfstate"
encrypt = true
use_lockfile = true
}
E inicializa con:
terraform init -backend-config=backend.hcl
Paso 5: Comprobar el bloqueo del estado
Con use_lockfile activo, dos operaciones simultáneas sobre el mismo estado no pueden ejecutarse a la vez. Para verlo, abre una segunda terminal en el mismo directorio (con las mismas variables AWS_* exportadas) y lanza un apply sin confirmar en la primera:
terraform apply
Déjalo esperando en el prompt Enter a value:. En la segunda terminal ejecuta:
terraform plan
Error: Error acquiring the state lock
Error message: operation error S3: PutObject, https response error StatusCode: 412,
...
Lock Info:
ID: 6f1c2a9e-5b0d-4f3e-9a7c-1d2e3f4a5b6c
Path: tu-empresa-tfstate/proyectos/tf-estado/terraform.tfstate
Operation: OperationTypeApply
Who: tu_usuario@servidor
Version: 1.13.3
Created: 2026-09-25 10:20:03.123456 +0000 UTC
El bloqueo funciona. Responde no en la primera terminal para liberarlo.
Si un proceso muere a mitad (un runner de CI cancelado, una conexión SSH cortada), el bloqueo puede quedarse huérfano. Primero asegúrate de que nadie está aplicando cambios de verdad y luego libéralo con el ID que muestra el error:
terraform force-unlock 6f1c2a9e-5b0d-4f3e-9a7c-1d2e3f4a5b6c
Importanteno uses
force-unlockpara saltarte un bloqueo activo. Si otra persona está aplicando cambios, forzar el desbloqueo puede dejar un estado inconsistente.
Paso 6: Inspeccionar y reparar el estado
Nunca edites el JSON del estado a mano. Terraform incluye comandos para las operaciones habituales.
Listar recursos y ver los atributos de uno:
terraform state list
terraform state show random_pet.servidor
# random_pet.servidor:
resource "random_pet" "servidor" {
id = "calm-falcon"
length = 2
separator = "-"
}
Hacer una copia de seguridad local del estado remoto antes de una operación delicada:
terraform state pull > backup-$(date +%F).tfstate
Renombrar un recurso sin recrearlo
Si cambias el nombre de un recurso en el código, Terraform lo destruiría y crearía uno nuevo. Para evitarlo, declara un bloque moved en main.tf. Cambia el recurso y el output así:
resource "random_pet" "web" {
length = 2
}
moved {
from = random_pet.servidor
to = random_pet.web
}
output "nombre" {
value = random_pet.web.id
}
terraform plan
# random_pet.servidor has moved to random_pet.web
resource "random_pet" "web" {
id = "calm-falcon"
# (2 unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
Aplica con terraform apply. El bloque moved queda en el código como registro del cambio y funciona igual para todo el equipo y para CI, a diferencia de terraform state mv, que solo modifica el estado en el momento en que lo ejecutas.
Dejar de gestionar un recurso sin destruirlo
Para que Terraform olvide un recurso pero lo deje existiendo, borra su bloque resource (y cualquier output que lo referencie) y añade un bloque removed (Terraform 1.7 o superior). Este es el aspecto que tendría para random_pet.web; no lo apliques ahora, porque el siguiente paso sigue usando ese recurso:
removed {
from = random_pet.web
lifecycle {
destroy = false
}
}
El plan mostrará que el recurso se elimina del estado sin destruirse. Es el equivalente declarativo de terraform state rm.
Paso 7: Separar entornos con workspaces
Los workspaces permiten tener varios estados independientes con el mismo código y el mismo backend. Son útiles para entornos casi idénticos, como staging y produccion.
Crea un workspace nuevo:
terraform workspace new staging
Created and switched to workspace "staging"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
Aplica en ese workspace. Terraform crea un recurso nuevo, independiente del de default:
terraform apply -auto-approve
terraform workspace list
default
* staging
En el bucket, cada workspace distinto de default se guarda bajo el prefijo env:/:
aws s3 ls s3://tu-empresa-tfstate/ --recursive
2026-09-25 10:31:02 871 env:/staging/proyectos/tf-estado/terraform.tfstate
2026-09-25 10:25:40 871 proyectos/tf-estado/terraform.tfstate
Puedes usar el nombre del workspace dentro del código con terraform.workspace, por ejemplo para ajustar tamaños:
locals {
instancias = terraform.workspace == "produccion" ? 3 : 1
}
Vuelve al workspace por defecto con:
terraform workspace select default
Si los entornos difieren mucho (otras cuentas, otros permisos, otros proveedores), usa directorios separados con su propio key en el backend en lugar de workspaces: el aislamiento es más claro y un error en uno no afecta a los demás.
Usar un almacenamiento compatible con S3
El backend s3 también funciona con proveedores compatibles con la API de S3, como MinIO o Wasabi. Necesitas indicar el endpoint y desactivar las comprobaciones propias de AWS:
backend "s3" {
bucket = "tu-empresa-tfstate"
key = "proyectos/tf-estado/terraform.tfstate"
region = "us-east-1"
endpoints = {
s3 = "https://s3.tu_proveedor.com"
}
use_path_style = true
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
use_lockfile = true
}
El bloqueo con use_lockfile se basa en escrituras condicionales de S3. Comprueba en la documentación de tu proveedor que las soporta; si no, dos operaciones simultáneas podrían escribir el estado a la vez.
Proteger el estado
Como el estado contiene secretos en texto plano, trátalo como tal:
- Restringe el bucket con una política IAM: solo los usuarios o roles que ejecutan Terraform deben poder leerlo. Necesitan
s3:ListBucketsobre el bucket ys3:GetObject,s3:PutObjectys3:DeleteObjectsobre las claves del estado y del.tflock. - Marca las variables y outputs con datos sensibles con
sensitive = true. No los cifra en el estado, pero evita que aparezcan en la salida deplanyapplyy en los logs de CI. - Mantén el versionado activado y añade una regla de ciclo de vida que borre versiones antiguas pasados unos meses, para no acumular copias con secretos indefinidamente.
Solución de problemas
Error: Backend configuration changed: modificaste el bloque backend después del último init. Ejecuta terraform init -migrate-state si quieres mover el estado, o terraform init -reconfigure si quieres empezar a usar la nueva configuración sin migrar nada.
No valid credential sources found: Terraform no encuentra credenciales. Comprueba que AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY están exportadas en la misma sesión, o configura un perfil con aws configure y usa AWS_PROFILE.
AccessDenied al crear el .tflock: la política IAM permite escribir el estado pero no el archivo de bloqueo. Añade permisos sobre proyectos/tf-estado/terraform.tfstate.tflock.
Recuperar un estado dañado: lista las versiones con aws s3api list-object-versions --bucket tu-empresa-tfstate --prefix proyectos/tf-estado/, descarga la buena con aws s3api get-object --version-id ID_VERSION ... y súbela con terraform state push.
Conclusión
Tu proyecto ya guarda el estado en S3 con versionado, cifrado y bloqueo nativo, sabes migrarlo desde un estado local y repararlo con bloques moved y removed en lugar de editar el JSON. Como siguientes pasos, ejecuta Terraform desde un pipeline de CI con credenciales de solo ese bucket, divide proyectos grandes en varios estados más pequeños con claves distintas, y consulta outputs de otro estado con la fuente de datos terraform_remote_state cuando un proyecto dependa de otro.
