Introducción

Hasta ahora hemos trabajado con el estado de Terraform en su forma más simple: un archivo local en el directorio de trabajo. Sin embargo, en el mundo real, los proyectos de infraestructura son trabajo de equipo. Varios ingenieros necesitan colaborar, los pipelines de CI/CD necesitan acceso al estado, y la infraestructura de producción no puede depender de un archivo en el portátil de alguien.

La solución es el estado remoto: almacenar el archivo de estado en un sistema compartido, accesible y seguro. En esta lección aprenderás a configurar los backends remotos más populares y a sacar el máximo partido del estado remoto.


¿Por Qué Usar Estado Remoto?

El estado local presenta limitaciones críticas en entornos reales:

Problemas del estado local en equipos

  1. Sin colaboración: El archivo terraform.tfstate está en el disco de un desarrollador. El resto del equipo no puede ver el estado actual de la infraestructura.

  2. Sin locking: Si dos personas ejecutan terraform apply al mismo tiempo, ambas leerán el mismo estado, calcularán los cambios de forma independiente y escribirán resultados inconsistentes. El estado queda corrupto.

  3. Sin historial: Solo existe el estado actual y el backup del estado anterior. Si necesitas ver el estado de hace una semana, no tienes forma de recuperarlo.

  4. Sin seguridad: El archivo puede contener contraseñas y tokens en texto plano. Si alguien hace git push por error, esos secretos quedan expuestos.

  5. Frágil: Si el desarrollador borra el directorio o pierde el portátil, se pierde el estado y el control sobre la infraestructura.

Ventajas del estado remoto

Ventaja Descripción
Compartido Todo el equipo y los sistemas de CI/CD acceden al mismo estado
Locking Evita escrituras concurrentes que corrompen el estado
Versionado Historial completo de cambios con capacidad de rollback
Seguro Cifrado en tránsito y en reposo; control de acceso con IAM/RBAC
Resiliencia Almacenado en sistemas de alta disponibilidad (S3, Azure Blob, etc.)
Separación El estado no está mezclado con el código fuente

Backends Disponibles en Terraform

Un backend es el sistema que Terraform usa para almacenar el estado y ejecutar operaciones. Hay dos categorías principales:

Backends estándar (solo almacenamiento)

Almacenan el estado pero requieren que las operaciones de Terraform se ejecuten localmente o en CI/CD:

  • local: El valor por defecto. Estado en disco local.
  • s3: Amazon S3 (con locking opcional vía DynamoDB).
  • azurerm: Azure Blob Storage.
  • gcs: Google Cloud Storage.
  • http: Cualquier servidor HTTP que implemente la API de estado de Terraform.
  • kubernetes: Secretos de Kubernetes.
  • consul: HashiCorp Consul.
  • pg: PostgreSQL.

Backend especial: Terraform Cloud / Enterprise

Terraform Cloud es el backend recomendado por HashiCorp. Además de almacenar el estado, puede ejecutar los planes y applies en la nube, gestionar variables y secretos, proporcionar aprobaciones manuales y mucho más.


Tabla Comparativa de Backends Más Populares

Backend Locking Cifrado en Reposo Versionado Coste Dificultad de Configuración
local No No No (solo backup) Gratis Ninguna
S3 + DynamoDB Sí (DynamoDB) Sí (KMS) Sí (S3 versioning) Bajo (AWS) Media
Azure Blob Sí (nativo) Sí (Azure) Sí (blob versioning) Bajo (Azure) Media
GCS Sí (nativo) Sí (Google) Sí (object versioning) Bajo (GCP) Media
Terraform Cloud Sí (nativo) Sí (ilimitado) Gratis/Pago Baja
Consul Sí (nativo) Depende No Gratis/Pago Alta
PostgreSQL Sí (nativo) Depende No Variable Alta

Configurar Backend de S3 con DynamoDB para Locking

Esta es la configuración más común en entornos AWS. Combina S3 (para almacenar el estado) con DynamoDB (para el locking).

Paso 1: Crear la infraestructura del backend (manualmente o con Terraform)

Primero necesitas crear el bucket S3 y la tabla DynamoDB. Un truco habitual es usar un proyecto Terraform separado ("bootstrap") para crear el backend del proyecto principal:

# bootstrap/main.tf
# Este archivo se aplica UNA SOLA VEZ con estado local para crear el backend

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# El bucket S3 para almacenar el estado
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mi-empresa-terraform-state-prod"

  # Previene la eliminación accidental del bucket
  lifecycle {
    prevent_destroy = true
  }
}

# Activar versionado del bucket (crucial para el historial del estado)
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Cifrado del bucket con KMS
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

# Bloquear acceso público al bucket de estado
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Tabla DynamoDB para el locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  # LockID es el único atributo que necesita DynamoDB para el locking
  attribute {
    name = "LockID"
    type = "S"
  }
}

# Outputs útiles para configurar el backend en otros proyectos
output "nombre_bucket" {
  value = aws_s3_bucket.terraform_state.bucket
}

output "nombre_tabla_dynamodb" {
  value = aws_dynamodb_table.terraform_locks.name
}

Paso 2: Configurar el backend en el proyecto principal

# backend.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    # Nombre del bucket donde se almacenará el estado
    bucket = "mi-empresa-terraform-state-prod"

    # Ruta dentro del bucket (como un "directorio")
    # Convención recomendada: entorno/proyecto/terraform.tfstate
    key = "produccion/mi-proyecto/terraform.tfstate"

    # Región donde está el bucket
    region = "us-east-1"

    # Cifrado del estado en tránsito
    encrypt = true

    # Tabla DynamoDB para el locking
    dynamodb_table = "terraform-state-locks"
  }
}

provider "aws" {
  region = "us-east-1"
}

Paso 3: Inicializar con el nuevo backend

terraform init

Salida esperada:

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.23.0

Terraform has been successfully initialized!

Estructura de claves S3 recomendada para múltiples entornos

mi-empresa-terraform-state-prod/
├── global/
│   └── iam/
│       └── terraform.tfstate
├── desarrollo/
│   ├── red/
│   │   └── terraform.tfstate
│   └── aplicacion/
│       └── terraform.tfstate
├── staging/
│   ├── red/
│   │   └── terraform.tfstate
│   └── aplicacion/
│       └── terraform.tfstate
└── produccion/
    ├── red/
    │   └── terraform.tfstate
    └── aplicacion/
        └── terraform.tfstate

Configurar Backend de Azure Blob Storage

Para proyectos que usan Microsoft Azure:

Paso 1: Crear el recurso de almacenamiento (Azure CLI)

# Variables de configuración
RESOURCE_GROUP="rg-terraform-state"
STORAGE_ACCOUNT="tfstatemipresa$(date +%s)"  # Nombre único
CONTAINER_NAME="tfstate"
LOCATION="westeurope"

# Crear el grupo de recursos
az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION

# Crear la cuenta de almacenamiento
az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --sku Standard_LRS \
  --encryption-services blob \
  --min-tls-version TLS1_2

# Crear el contenedor de blobs
az storage container create \
  --name $CONTAINER_NAME \
  --account-name $STORAGE_ACCOUNT

# Habilitar versionado de blobs
az storage account blob-service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --enable-versioning true

echo "Storage Account: $STORAGE_ACCOUNT"

Paso 2: Configurar el backend en Terraform

# backend.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }

  backend "azurerm" {
    # Grupo de recursos donde está la cuenta de almacenamiento
    resource_group_name = "rg-terraform-state"

    # Nombre de la cuenta de almacenamiento
    storage_account_name = "tfstatemipresa1234567890"

    # Nombre del contenedor de blobs
    container_name = "tfstate"

    # Nombre del blob (el "archivo" dentro del contenedor)
    key = "produccion/mi-proyecto.tfstate"
  }
}

provider "azurerm" {
  features {}
}

Azure Blob Storage tiene locking nativo: usa la característica de "lease" de blobs, por lo que no necesitas una tabla adicional como DynamoDB.


Configurar Terraform Cloud como Backend

Terraform Cloud (cloud.hashicorp.com) es la opción más completa y la más fácil de configurar para equipos.

Paso 1: Crear una cuenta y organización en Terraform Cloud

  1. Ve a https://app.terraform.io y crea una cuenta gratuita.
  2. Crea una organización (por ejemplo, mi-empresa).
  3. Crea un workspace (por ejemplo, mi-proyecto-produccion).

Paso 2: Generar un token de API

# Autenticarte desde la CLI de Terraform
terraform login

Esto abre el navegador y guarda automáticamente el token en ~/.terraform.d/credentials.tfrc.json.

Paso 3: Configurar el backend

# backend.tf
terraform {
  required_version = ">= 1.1"

  cloud {
    # Nombre de tu organización en Terraform Cloud
    organization = "mi-empresa"

    workspaces {
      # Nombre del workspace en Terraform Cloud
      name = "mi-proyecto-produccion"
    }
  }
}

O usando el backend remote (forma más antigua pero compatible con más versiones):

terraform {
  backend "remote" {
    organization = "mi-empresa"

    workspaces {
      name = "mi-proyecto-produccion"
    }
  }
}

Paso 4: Inicializar

terraform init

Salida esperada:

Initializing Terraform Cloud...

Initializing provider plugins...

Terraform Cloud has been successfully initialized!

You may now begin working with Terraform Cloud. Try running "terraform plan" to
see any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.

A partir de este punto, cada terraform plan y terraform apply se ejecuta en los servidores de Terraform Cloud, no en tu máquina local.


Acceder al Estado Remoto de Otro Workspace

En proyectos grandes, es común dividir la infraestructura en múltiples proyectos de Terraform (por ejemplo, uno para la red y otro para las aplicaciones). El proyecto de aplicación necesita conocer los IDs de los recursos creados por el proyecto de red (como el ID de la VPC o la subred).

Esto se resuelve con el data source terraform_remote_state:

Proyecto de red (quien comparte el estado)

# red/main.tf
resource "aws_vpc" "principal" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "publica" {
  vpc_id     = aws_vpc.principal.id
  cidr_block = "10.0.1.0/24"
}

# IMPORTANTE: Los valores que quieres compartir deben ser outputs
output "vpc_id" {
  value       = aws_vpc.principal.id
  description = "ID de la VPC principal"
}

output "subnet_publica_id" {
  value       = aws_subnet.publica.id
  description = "ID de la subred pública"
}
# red/backend.tf
terraform {
  backend "s3" {
    bucket = "mi-empresa-terraform-state-prod"
    key    = "produccion/red/terraform.tfstate"
    region = "us-east-1"
  }
}

Proyecto de aplicación (quien lee el estado)

# aplicacion/main.tf

# Leer el estado del proyecto de red
data "terraform_remote_state" "red" {
  backend = "s3"

  config = {
    bucket = "mi-empresa-terraform-state-prod"
    key    = "produccion/red/terraform.tfstate"
    region = "us-east-1"
  }
}

# Usar los valores del estado remoto
resource "aws_instance" "servidor_app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  # Usando el output del proyecto de red
  subnet_id = data.terraform_remote_state.red.outputs.subnet_publica_id

  tags = {
    Name  = "ServidorApp"
    VPC   = data.terraform_remote_state.red.outputs.vpc_id
  }
}

Importante: Solo puedes acceder a los valores declarados como output en el otro proyecto. No puedes acceder directamente a los atributos de los recursos del otro estado. Esto es intencional: los outputs son la "API pública" de un proyecto Terraform.


Migrar Estado Local a Estado Remoto

Cuando comienzas un proyecto con estado local y luego quieres migrarlo a un backend remoto, Terraform lo hace automáticamente con terraform init -migrate-state.

Proceso de migración

Situación inicial: Tienes un proyecto con estado local y decides migrar a S3.

# Antes de migrar, verifica que tienes estado local
ls -la terraform.tfstate

# Modifica tu backend.tf para configurar el backend S3
# (o créalo si no existía)
# backend.tf (nuevo o modificado)
terraform {
  backend "s3" {
    bucket = "mi-empresa-terraform-state-prod"
    key    = "produccion/mi-proyecto/terraform.tfstate"
    region = "us-east-1"
    dynamodb_table = "terraform-state-locks"
    encrypt = true
  }
}
# Inicializar con migración
terraform init -migrate-state

Terraform te preguntará confirmación:

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the newly
configured backend...

Acquiring state lock. This may take a few moments...
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 backend?
  Enter a value:

Escribe yes y pulsa Enter.

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Después de la migración, el estado local ya no es relevante. Puedes eliminarlo:

rm terraform.tfstate terraform.tfstate.backup

Migrar entre backends remotos

Si necesitas migrar de un backend a otro (por ejemplo, de S3 a Terraform Cloud):

# Simplemente actualiza la configuración del backend en backend.tf
# y luego ejecuta:
terraform init -migrate-state

Terraform descargará el estado del backend antiguo y lo subirá al nuevo automáticamente.


Permisos Necesarios para el Backend

Permisos AWS para S3 + DynamoDB

El usuario o rol de IAM que ejecuta Terraform necesita los siguientes permisos:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::mi-empresa-terraform-state-prod",
        "arn:aws:s3:::mi-empresa-terraform-state-prod/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:*:table/terraform-state-locks"
    }
  ]
}

Permisos Azure para Azure Blob Storage

El principal de servicio necesita el rol Storage Blob Data Contributor en la cuenta de almacenamiento:

# Asignar rol al principal de servicio
az role assignment create \
  --assignee "<client-id-del-principal-de-servicio>" \
  --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/<id>/resourceGroups/rg-terraform-state/providers/Microsoft.Storage/storageAccounts/tfstatemipresa1234567890"

Ejercicio Práctico: Configurar un Backend Remoto Usando S3

Objetivo

Configurar un backend S3 funcional. Si no tienes acceso a AWS, seguirás los pasos con LocalStack (un emulador de AWS local).

Opción A: Con LocalStack (sin credenciales AWS reales)

# Instalar LocalStack (requiere Docker)
pip install localstack
localstack start -d

# Configurar credenciales ficticias para LocalStack
export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"
export AWS_DEFAULT_REGION="us-east-1"
# main.tf para LocalStack
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket                      = "mi-terraform-state-local"
    key                         = "ejercicio/terraform.tfstate"
    region                      = "us-east-1"
    access_key                  = "test"
    secret_key                  = "test"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    skip_requesting_account_id  = true
    force_path_style            = true
    endpoints = {
      s3       = "http://localhost:4566"
      dynamodb = "http://localhost:4566"
    }
    dynamodb_table = "terraform-locks"
  }
}

provider "aws" {
  region                      = "us-east-1"
  access_key                  = "test"
  secret_key                  = "test"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  force_path_style            = true

  endpoints {
    s3       = "http://localhost:4566"
    dynamodb = "http://localhost:4566"
  }
}
# Crear el bucket y la tabla en LocalStack
aws --endpoint-url=http://localhost:4566 s3 mb s3://mi-terraform-state-local

aws --endpoint-url=http://localhost:4566 dynamodb create-table \
  --table-name terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

# Inicializar Terraform con el backend S3
terraform init

# Verificar que el estado se guarda remotamente
terraform apply -auto-approve
aws --endpoint-url=http://localhost:4566 s3 ls s3://mi-terraform-state-local/ejercicio/

Opción B: Con AWS real

Si tienes acceso a AWS con las credenciales configuradas:

# Configurar credenciales (si aún no lo has hecho)
aws configure

# Crear el bucket
aws s3 mb s3://mi-empresa-terraform-state-ejercicio --region us-east-1

# Activar versionado
aws s3api put-bucket-versioning \
  --bucket mi-empresa-terraform-state-ejercicio \
  --versioning-configuration Status=Enabled

# Crear tabla DynamoDB
aws dynamodb create-table \
  --table-name terraform-state-locks-ejercicio \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1
# backend.tf
terraform {
  backend "s3" {
    bucket         = "mi-empresa-terraform-state-ejercicio"
    key            = "ejercicio/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks-ejercicio"
  }
}
# Inicializar con migración si ya tenías estado local
terraform init -migrate-state

# Verificar que el estado está en S3
aws s3 ls s3://mi-empresa-terraform-state-ejercicio/ejercicio/

Verificación del ejercicio

# 1. Confirma que no hay terraform.tfstate local
ls -la terraform.tfstate 2>/dev/null || echo "Correcto: no hay estado local"

# 2. Descarga el estado remoto para inspeccionarlo
terraform state pull

# 3. Lista los recursos en el estado remoto
terraform state list

# 4. Verifica que el locking funciona intentando
#    ejecutar dos terraform plan en paralelo (abre dos terminales)
# Terminal 1:
terraform plan

# Terminal 2 (mientras el primero está en ejecución):
terraform plan
# Deberías ver: "Error: Error acquiring the state lock"

Errores Comunes con Backends Remotos

Error 1: Credenciales insuficientes para acceder al backend

Error: Failed to get existing workspaces: AccessDenied: Access Denied
        status code: 403

Causa: El usuario o rol de IAM no tiene permisos suficientes sobre el bucket S3. Solución: Revisa y actualiza la política de IAM según los permisos documentados anteriormente.

Error 2: El bucket S3 no existe

Error: Failed to get existing workspaces: NoSuchBucket: The specified bucket does not exist

Causa: El bucket especificado en la configuración del backend no existe todavía. Solución: Crea el bucket antes de ejecutar terraform init.

Error 3: Tabla DynamoDB no encontrada

Error: Error acquiring the state lock: ResourceNotFoundException:
Requested resource not found

Causa: La tabla DynamoDB para locking no existe. Solución: Crea la tabla DynamoDB con el atributo LockID de tipo String.

Error 4: Estado bloqueado por un proceso anterior

Error: Error acquiring the state lock

Lock Info:
  ID:        abc123...
  Operation: OperationTypeApply
  Who:       usuario@ci-server
  Created:   2024-01-15 10:30:00

Causa: Un proceso de CI/CD anterior falló y no liberó el lock. Solución: Usa terraform force-unlock <ID> con el ID del lock mostrado en el error. Esto se verá en detalle en la siguiente lección.

Error 5: Conflicto de lineage al migrar

Error: Backend configuration changed

The "lineage" value "abc123..." of the existing state does not match
the "lineage" value "def456..." of the migrated state.

Causa: Estás intentando sobrescribir un estado existente en el backend remoto con un estado diferente (de otro proyecto). Solución: Verifica que la key en la configuración del backend es la correcta para este proyecto.


Resumen

En esta lección has aprendido a trasladar el estado de Terraform de un archivo local a sistemas remotos y seguros:

  • El estado remoto es esencial para equipos: permite colaboración, locking, historial y seguridad.
  • Los backends más populares son S3+DynamoDB (AWS), Azure Blob Storage y Terraform Cloud, cada uno con sus propias características de locking y versionado.
  • Para S3, necesitas crear el bucket (con versionado y cifrado) y una tabla DynamoDB para el locking. Los permisos de IAM son esenciales.
  • Para Azure Blob, el locking es nativo y no necesitas recursos adicionales.
  • Terraform Cloud es la opción más completa: almacena el estado, ejecuta los planes en la nube y gestiona secretos.
  • El data source terraform_remote_state permite que un proyecto de Terraform acceda a los outputs de otro, facilitando la arquitectura modular de infraestructura.
  • La migración de estado local a remoto se hace con terraform init -migrate-state.

En la siguiente lección (03-04-state-locking.md) profundizaremos específicamente en el mecanismo de bloqueo del estado: cómo funciona, qué backends lo soportan, qué hacer cuando un lock queda huérfano y cómo gestionar situaciones de emergencia con terraform force-unlock.

© Copyright 2026. Todos los derechos reservados