Introducción

Los bloques resource y data son los pilares centrales de cualquier configuración de Terraform. Los recursos definen la infraestructura que queremos crear y gestionar, mientras que los data sources nos permiten leer información de infraestructura existente sin modificarla. Dominar estos dos conceptos, junto con los meta-argumentos que los potencian, te permitirá escribir configuraciones de Terraform profesionales y flexibles.


El bloque resource: sintaxis completa

Estructura básica

resource "tipo_proveedor_recurso" "nombre_local" {
  # Argumentos específicos del recurso
  argumento_1 = valor_1
  argumento_2 = valor_2

  # Bloques anidados dentro del recurso
  bloque_anidado {
    sub_argumento = valor
  }
}

El tipo del recurso siempre tiene la forma proveedor_tiporecurso, por ejemplo:

  • aws_instance (AWS + instancia EC2)
  • azurerm_virtual_machine (Azure + máquina virtual)
  • google_compute_instance (Google Cloud + instancia compute)

Ejemplo detallado de un recurso

resource "aws_instance" "servidor_web" {
  # --- Configuración de hardware ---
  ami           = "ami-0c55b159cbfafe1f0"  # ID de la imagen de sistema operativo
  instance_type = "t3.micro"               # Tipo de instancia: 2 vCPUs, 1GB RAM

  # --- Configuración de red ---
  subnet_id                   = aws_subnet.publica.id  # Subnet donde se coloca
  vpc_security_group_ids      = [aws_security_group.web.id]  # Grupos de seguridad
  associate_public_ip_address = true   # Asignar IP pública automáticamente

  # --- Configuración de almacenamiento ---
  root_block_device {
    volume_type = "gp3"   # Tipo de volumen: SSD de propósito general v3
    volume_size = 20      # Tamaño en GB
    encrypted   = true    # Cifrado en reposo habilitado
  }

  # --- Script de inicialización (se ejecuta al arrancar la instancia) ---
  user_data = <<-EOF
    #!/bin/bash
    # Actualizar paquetes del sistema
    apt-get update -y
    # Instalar el servidor web nginx
    apt-get install -y nginx
    # Iniciar nginx y habilitarlo para que arranque con el sistema
    systemctl start nginx
    systemctl enable nginx
  EOF

  # --- Etiquetas (metadata) ---
  tags = {
    Name        = "servidor-web-principal"
    Entorno     = "produccion"
    Gestionado  = "terraform"
  }
}

Meta-argumentos de recursos

Los meta-argumentos son argumentos especiales que Terraform entiende para todos los tipos de recursos, independientemente del proveedor. Son:

Meta-argumento Propósito
depends_on Define dependencias explícitas entre recursos
count Crea múltiples instancias del mismo recurso
for_each Crea recursos a partir de un mapa o set
provider Especifica qué configuración de proveedor usar
lifecycle Controla el comportamiento durante el ciclo de vida

Meta-argumento depends_on

Normalmente Terraform infiere automáticamente el orden de creación analizando las referencias entre recursos. Pero a veces hay dependencias implícitas que Terraform no puede detectar. En esos casos usamos depends_on.

resource "aws_iam_role_policy" "politica_acceso_s3" {
  name   = "acceso-s3"
  role   = aws_iam_role.ec2_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:PutObject"]
      Resource = "*"
    }]
  })
}

resource "aws_instance" "servidor" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  iam_instance_profile = aws_iam_instance_profile.perfil.name

  # La instancia necesita la política aplicada ANTES de arrancar
  # aunque no hay referencia directa a "politica_acceso_s3"
  depends_on = [
    aws_iam_role_policy.politica_acceso_s3
  ]
}

Regla de oro: Usa depends_on solo cuando sea absolutamente necesario. La mayoría de las veces las referencias implícitas son suficientes y más limpias.


Meta-argumento count

count permite crear múltiples instancias del mismo bloque de recurso. Terraform crea count copias, cada una accesible como recurso[índice].

variable "num_servidores" {
  type    = number
  default = 3
}

# Con count = 3, Terraform crea tres instancias EC2
resource "aws_instance" "servidores" {
  count = var.num_servidores  # Crea 3 instancias

  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  # count.index es el índice actual (0, 1, 2, ...)
  # Lo usamos para dar nombres únicos a cada instancia
  tags = {
    Name = "servidor-${count.index + 1}"
    # count.index empieza en 0, sumamos 1 para nombres más naturales
    # Resultado: "servidor-1", "servidor-2", "servidor-3"
  }
}

# Acceder a instancias individuales por índice
output "ip_primer_servidor" {
  value = aws_instance.servidores[0].public_ip  # Primera instancia (índice 0)
}

output "ip_segundo_servidor" {
  value = aws_instance.servidores[1].public_ip  # Segunda instancia (índice 1)
}

# Obtener una lista de todos los IPs usando [*] (splat expression)
output "todas_las_ips" {
  value = aws_instance.servidores[*].public_ip
  # Resultado: ["1.2.3.4", "5.6.7.8", "9.10.11.12"]
}

Distribución en múltiples zonas con count

variable "zonas" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

# Creamos tantas subnets como zonas de disponibilidad haya
resource "aws_subnet" "publicas" {
  count = length(var.zonas)  # Una subnet por zona

  vpc_id            = aws_vpc.principal.id
  # cidrsubnet calcula un sub-bloque CIDR único para cada subnet
  # "10.0.0.0/16", 8 bits adicionales, índice -> genera 10.0.0.0/24, 10.0.1.0/24, etc.
  cidr_block        = cidrsubnet("10.0.0.0/16", 8, count.index)
  availability_zone = var.zonas[count.index]  # Zona correspondiente al índice

  tags = {
    Name = "subnet-publica-${var.zonas[count.index]}"
  }
}

Limitaciones de count

El principal problema de count es que los recursos se identifican por índice numérico. Si eliminas un elemento del medio de la lista, Terraform puede querer recrear recursos innecesariamente.

# PROBLEMA con count: si la lista cambia, los índices cambian
variable "nombres_servidores" {
  default = ["web-1", "web-2", "web-3"]
}

resource "aws_instance" "web" {
  count = length(var.nombres_servidores)
  # [0] = web-1, [1] = web-2, [2] = web-3

  # Si eliminamos "web-2", Terraform ve:
  # [0] = web-1 (sin cambio)
  # [1] = web-3 (antes era web-2 -> Terraform modifica o recrea!)
  # Esto puede causar destrucciones innecesarias
}

Para estos casos, for_each es mejor opción.


Meta-argumento for_each

for_each crea recursos a partir de un mapa o un set de strings. Cada recurso se identifica por su clave, lo que lo hace mucho más robusto que count cuando la colección puede cambiar.

for_each con un map

variable "instancias" {
  type = map(object({
    tipo = string
    zona = string
  }))
  default = {
    "web-1" = { tipo = "t2.micro",  zona = "us-east-1a" }
    "web-2" = { tipo = "t2.small",  zona = "us-east-1b" }
    "api-1" = { tipo = "t3.medium", zona = "us-east-1a" }
  }
}

resource "aws_instance" "servidores" {
  # for_each recibe el mapa completo
  for_each = var.instancias

  ami           = "ami-0c55b159cbfafe1f0"
  # each.value contiene el valor del elemento actual del mapa
  # each.key contiene la clave del elemento actual ("web-1", "web-2", etc.)
  instance_type = each.value.tipo
  availability_zone = each.value.zona

  tags = {
    # each.key es el nombre del servidor: "web-1", "web-2", "api-1"
    Name = each.key
  }
}

# Acceder a un servidor específico por su clave
output "ip_web_1" {
  value = aws_instance.servidores["web-1"].public_ip
}

# Obtener todos los IPs en un mapa (clave -> IP)
output "todas_las_ips" {
  value = { for k, v in aws_instance.servidores : k => v.public_ip }
  # Resultado: { "web-1" = "1.2.3.4", "web-2" = "5.6.7.8", "api-1" = "9.10.11.12" }
}

for_each con un set de strings

variable "buckets_s3" {
  type    = set(string)  # Set: sin duplicados, sin orden garantizado
  default = ["logs-app", "backups-db", "assets-web"]
}

resource "aws_s3_bucket" "buckets" {
  for_each = var.buckets_s3  # Un bucket por elemento del set

  # each.key es el elemento del set (en sets, key y value son iguales)
  bucket = each.key  # "logs-app", "backups-db", "assets-web"

  tags = {
    Name = each.key
  }
}

output "arns_buckets" {
  description = "ARNs de todos los buckets creados"
  value = { for k, v in aws_s3_bucket.buckets : k => v.arn }
}

Comparación count vs for_each

Característica count for_each
Identifica recursos por Índice numérico (0, 1, 2...) Clave string
Acceso al elemento actual count.index each.key y each.value
Tipo de colección aceptada Cualquier número map o set(string)
Impacto al eliminar elementos Puede recrear recursos innecesariamente Solo afecta el elemento eliminado
Cuándo usar Recursos idénticos, conteo simple Recursos con configuración por clave
Salida en state recurso[0], recurso[1] recurso["clave"]

Bloque lifecycle

El bloque lifecycle controla cómo Terraform maneja los cambios en el ciclo de vida de un recurso. Se coloca dentro del bloque resource.

resource "aws_instance" "produccion" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.large"

  lifecycle {
    # create_before_destroy: primero crea el nuevo recurso y luego destruye el viejo
    # Útil para recursos que no pueden tener downtime (como balanceadores de carga)
    create_before_destroy = true

    # prevent_destroy: bloquea la destrucción accidental del recurso
    # Si alguien ejecuta "terraform destroy" en este recurso, Terraform dará error
    prevent_destroy = true

    # ignore_changes: ignora cambios en ciertos atributos durante "terraform apply"
    # Útil cuando el recurso se modifica fuera de Terraform (cambios manuales)
    ignore_changes = [
      # Ignoramos el tag "LastModified" que se actualiza externamente
      tags["LastModified"],
      # Ignoramos cambios en la AMI (para no recrear si se actualiza la imagen)
      ami,
    ]
  }

  tags = {
    Name = "servidor-produccion"
  }
}

Ejemplo práctico de cada opción lifecycle

# --- create_before_destroy ---
# Caso de uso: balanceador de carga (no podemos tener downtime)
resource "aws_lb" "principal" {
  name               = "lb-produccion"
  internal           = false
  load_balancer_type = "application"
  subnets            = aws_subnet.publicas[*].id

  lifecycle {
    create_before_destroy = true
    # Si necesitamos cambiar algo que requiere recrear el LB:
    # 1. Terraform crea el nuevo LB
    # 2. Actualiza todas las referencias al nuevo LB
    # 3. Destruye el LB antiguo
    # -> Sin downtime durante el cambio
  }
}

# --- prevent_destroy ---
# Caso de uso: base de datos de producción (datos críticos)
resource "aws_db_instance" "produccion" {
  identifier        = "db-produccion"
  engine            = "postgres"
  instance_class    = "db.t3.medium"
  allocated_storage = 100

  lifecycle {
    prevent_destroy = true
    # Si alguien ejecuta "terraform destroy" o elimina este bloque del código,
    # Terraform dará error:
    # Error: Instance cannot be destroyed
    # on main.tf line X, in resource "aws_db_instance" "produccion":
    #   prevent_destroy = true
    # Esta salvaguarda evita borrado accidental de datos de producción
  }
}

# --- ignore_changes ---
# Caso de uso: grupo de autoescalado donde el número de instancias
# lo gestiona AWS (no queremos que Terraform lo sobreescriba)
resource "aws_autoscaling_group" "app" {
  name             = "asg-app"
  min_size         = 2
  max_size         = 10
  desired_capacity = 2  # Valor inicial, puede cambiar por autoescalado

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  lifecycle {
    ignore_changes = [
      # Ignoramos desired_capacity porque AWS lo ajusta automáticamente
      # Si no lo ignoramos, Terraform lo resetearía a 2 en cada apply
      desired_capacity,
    ]
  }
}

El bloque data: leer datos existentes

Los data sources permiten leer información de recursos que ya existen (ya sea creados por otro proceso de Terraform, manualmente, o por otro equipo) sin modificarlos.

Sintaxis básica

data "tipo_proveedor_dato" "nombre_local" {
  # Argumentos para filtrar/identificar el dato que queremos leer
  argumento_filtro = valor
}

# Usar el dato leído
data.tipo_proveedor_dato.nombre_local.atributo

Ejemplos de data sources

# Leer la AMI de Ubuntu más reciente (en lugar de hardcodear el ID)
data "aws_ami" "ubuntu" {
  most_recent = true  # Queremos la versión más reciente
  owners      = ["099720109477"]  # Cuenta oficial de Canonical (Ubuntu)

  # Filtros para encontrar la AMI exacta que queremos
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
    # El asterisco * es un comodín que coincide con cualquier texto
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]  # Solo AMIs con virtualización HVM (más modernas)
  }
}

# Ahora usamos el dato en lugar de hardcodear el ID de la AMI
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id  # ID dinámico obtenido del data source
  instance_type = "t2.micro"

  tags = {
    Name     = "servidor-web"
    # También podemos mostrar la versión de la AMI en los tags
    AMIName  = data.aws_ami.ubuntu.name
  }
}

# Leer información de la VPC actual (útil cuando la VPC la creó otro equipo)
data "aws_vpc" "seleccionada" {
  # Filtramos por tags para encontrar la VPC correcta
  tags = {
    Entorno = "produccion"
    Equipo  = "red"
  }
}

# Leer las subnets de esa VPC
data "aws_subnets" "privadas" {
  # Filtramos por VPC y por el tag que identifica subnets privadas
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.seleccionada.id]
  }

  tags = {
    Tipo = "privada"
  }
}

# Usar los datos leídos para crear instancias en las subnets existentes
resource "aws_instance" "app" {
  count = 2

  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.small"
  # Distribuimos las instancias entre las subnets privadas existentes
  subnet_id     = data.aws_subnets.privadas.ids[count.index]
}

Datos del proveedor y cuenta

# Obtener información de la cuenta y región actual de AWS
data "aws_caller_identity" "actual" {}
# No necesita argumentos: obtiene la info de quien ejecuta Terraform

data "aws_region" "actual" {}

# Usar los datos para construir ARNs dinámicamente
resource "aws_iam_policy" "acceso_bucket" {
  name = "acceso-mi-bucket"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = ["s3:*"]
      # Construimos el ARN del bucket usando la cuenta y región actuales
      # Así el código funciona en cualquier cuenta/región sin cambios
      Resource = "arn:aws:s3:::mi-bucket-${data.aws_caller_identity.actual.account_id}"
    }]
  })
}

output "id_cuenta_aws" {
  value = data.aws_caller_identity.actual.account_id
}

output "region_actual" {
  value = data.aws_region.actual.name
}

Diferencia entre resource y data con tabla comparativa

Característica resource data
Propósito Crear/modificar/destruir infraestructura Leer información existente
Gestión del ciclo de vida Sí: create, update, destroy No: solo lectura
Aparece en el plan Sí: como "add", "change" o "destroy" Sí: como "data source refresh"
Se almacena en el estado Sí (temporalmente, se refresca)
Sintaxis resource "tipo" "nombre" data "tipo" "nombre"
Acceso tipo_recurso.nombre.atributo data.tipo.nombre.atributo
Cuándo usar Cuando Terraform debe CREAR el recurso Cuando el recurso YA EXISTE
Riesgo si se borra El recurso real se destruye Nada: solo se deja de leer

Ejemplos de código detallados: combinando resource y data

Escenario real: aplicación que usa red existente

# =============================================
# Infraestructura de red (creada por otro equipo)
# La leemos con data sources
# =============================================

# Leer la VPC de producción existente
data "aws_vpc" "produccion" {
  filter {
    name   = "tag:Name"
    values = ["vpc-produccion"]
  }
}

# Leer las subnets privadas de esa VPC
data "aws_subnets" "privadas" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.produccion.id]
  }
  filter {
    name   = "tag:Tipo"
    values = ["privada"]
  }
}

# Leer el security group de aplicaciones existente
data "aws_security_group" "app" {
  vpc_id = data.aws_vpc.produccion.id
  tags = {
    Name = "sg-aplicaciones"
  }
}

# =============================================
# Infraestructura de la aplicación
# La CREAMOS con resource
# =============================================

# Security group adicional específico para esta app
resource "aws_security_group" "mi_app" {
  name        = "sg-mi-app"
  description = "Security group para mi-aplicacion"
  # Usamos la VPC leída con el data source
  vpc_id      = data.aws_vpc.produccion.id

  ingress {
    description = "HTTP interno"
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    # Solo permitimos tráfico desde dentro de la VPC
    cidr_blocks = [data.aws_vpc.produccion.cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"  # -1 = todos los protocolos
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "sg-mi-app"
  }
}

# Instancias de la aplicación en las subnets existentes
resource "aws_instance" "app" {
  count = length(data.aws_subnets.privadas.ids)  # Una instancia por subnet

  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.small"

  # Colocamos cada instancia en una subnet privada diferente
  subnet_id = data.aws_subnets.privadas.ids[count.index]

  # Asignamos ambos security groups: el existente y el nuevo
  vpc_security_group_ids = [
    data.aws_security_group.app.id,  # Security group existente (leído)
    aws_security_group.mi_app.id      # Security group nuevo (creado)
  ]

  tags = {
    Name   = "mi-app-${count.index + 1}"
    Subnet = data.aws_subnets.privadas.ids[count.index]
  }
}

Ejercicio práctico

Enunciado

Crea una configuración Terraform que:

  1. Use count para crear 3 instancias EC2 con nombres únicos (servidor-1, servidor-2, servidor-3).
  2. Use for_each para crear 3 buckets S3 con diferentes configuraciones: logs, backups y assets.
  3. Añada un bloque lifecycle con prevent_destroy = true solo al bucket de backups.
  4. Use un data source para leer la región actual de AWS y usarla en los nombres de los buckets.
  5. Defina outputs para las IPs de los servidores y los ARNs de los buckets.

Solución detallada

# =============================================
# Archivo: variables.tf
# =============================================

variable "num_servidores" {
  type        = number
  description = "Número de servidores EC2 a crear"
  default     = 3
}

variable "ami_id" {
  type        = string
  description = "ID de la AMI para las instancias EC2"
  default     = "ami-0c55b159cbfafe1f0"  # Ubuntu 20.04 en us-east-1
}

variable "tipo_instancia" {
  type        = string
  description = "Tipo de instancia EC2"
  default     = "t2.micro"
}

# =============================================
# Archivo: main.tf
# =============================================

# Data source para obtener la región actual dinámicamente
# Así no necesitamos hardcodear "us-east-1" en ningún lado
data "aws_region" "actual" {}

# Data source para obtener la cuenta actual
data "aws_caller_identity" "actual" {}

# ------ PARTE 1: count para instancias EC2 ------

resource "aws_instance" "servidores" {
  # Crea 'num_servidores' instancias (3 por defecto)
  count = var.num_servidores

  ami           = var.ami_id
  instance_type = var.tipo_instancia

  tags = {
    # count.index va de 0 a (count-1), sumamos 1 para nombres naturales
    Name        = "servidor-${count.index + 1}"
    Numero      = count.index + 1
    Total       = var.num_servidores
    Gestionado  = "terraform"
  }
}

# ------ PARTE 2: for_each para buckets S3 ------

# Definimos la configuración de cada bucket
locals {
  # Mapa con la configuración de cada bucket
  # La clave es el nombre lógico; el valor es la configuración
  config_buckets = {
    "logs" = {
      descripcion  = "Almacenamiento de logs de la aplicacion"
      es_privado   = true
      evitar_borrado = false  # Los logs no son tan críticos
    }
    "backups" = {
      descripcion  = "Backups de bases de datos"
      es_privado   = true
      evitar_borrado = true  # LOS BACKUPS SON CRÍTICOS: no borrar
    }
    "assets" = {
      descripcion  = "Archivos estáticos públicos (imágenes, CSS, JS)"
      es_privado   = false  # Los assets son públicos
      evitar_borrado = false
    }
  }

  # Construimos el nombre del bucket con la región (de la data source)
  # Resultado: "mi-empresa-logs-us-east-1", "mi-empresa-backups-us-east-1", etc.
  # Los nombres de S3 deben ser únicos globalmente, incluir la región ayuda
  prefijo_bucket = "mi-empresa-${data.aws_region.actual.name}"
}

# Creamos los buckets usando for_each con el mapa de configuración
resource "aws_s3_bucket" "buckets" {
  # for_each itera sobre el mapa: each.key = "logs"/"backups"/"assets"
  for_each = local.config_buckets

  # Nombre único: prefijo + tipo de bucket
  bucket = "${local.prefijo_bucket}-${each.key}"
  # Resultado: "mi-empresa-us-east-1-logs", "mi-empresa-us-east-1-backups", etc.

  tags = {
    Name        = each.key               # "logs", "backups" o "assets"
    Descripcion = each.value.descripcion # Descripción del bucket
    Privado     = each.value.es_privado ? "si" : "no"
    Cuenta      = data.aws_caller_identity.actual.account_id
  }
}

# ------ PARTE 3: lifecycle con prevent_destroy ------

# Para aplicar lifecycle solo al bucket de backups, necesitamos un recurso separado
# (no podemos poner lifecycle condicional dentro de for_each de forma directa)

# Alternativa: usar un recurso separado para el bucket de backups con lifecycle
resource "aws_s3_bucket" "backups_protegido" {
  bucket = "${local.prefijo_bucket}-backups-criticos"

  tags = {
    Name        = "backups-criticos"
    Descripcion = "Backups críticos de bases de datos (PROTEGIDO)"
    Privado     = "si"
  }

  # El lifecycle se aplica a ESTE recurso específico
  lifecycle {
    # Terraform dará error si alguien intenta destruir este bucket
    prevent_destroy = true
  }
}

# ------ Configuración de acceso público en los buckets ------

# El bucket de assets es público; los demás son privados
resource "aws_s3_bucket_public_access_block" "buckets" {
  for_each = local.config_buckets

  bucket = aws_s3_bucket.buckets[each.key].id

  # Si es privado, bloqueamos TODO el acceso público
  block_public_acls       = each.value.es_privado
  block_public_policy     = each.value.es_privado
  ignore_public_acls      = each.value.es_privado
  restrict_public_buckets = each.value.es_privado
}

# =============================================
# Archivo: outputs.tf
# =============================================

# Output 1: IPs privadas de los servidores (lista)
output "ips_privadas_servidores" {
  description = "Lista de IPs privadas de los servidores EC2"
  # [*] es la splat expression: obtiene el atributo de todos los elementos count
  value = aws_instance.servidores[*].private_ip
  # Ejemplo: ["10.0.1.10", "10.0.1.11", "10.0.1.12"]
}

# Output 2: IPs públicas de los servidores (lista)
output "ips_publicas_servidores" {
  description = "Lista de IPs públicas de los servidores EC2"
  value = aws_instance.servidores[*].public_ip
}

# Output 3: Mapa de nombre -> ID de instancia
output "ids_servidores" {
  description = "Mapa de nombre de servidor a su ID"
  # Usamos una expresión for para construir el mapa
  value = {
    for i, servidor in aws_instance.servidores :
    "servidor-${i + 1}" => servidor.id
  }
  # Ejemplo: { "servidor-1" = "i-abc123", "servidor-2" = "i-def456" }
}

# Output 4: ARNs de todos los buckets S3
output "arns_buckets" {
  description = "Mapa de nombre de bucket a su ARN"
  value = {
    for k, v in aws_s3_bucket.buckets : k => v.arn
  }
  # Ejemplo: { "logs" = "arn:aws:s3:::mi-empresa-us-east-1-logs", ... }
}

# Output 5: Nombres de todos los buckets creados
output "nombres_buckets" {
  description = "Lista con los nombres de todos los buckets S3 creados"
  value = [for k, v in aws_s3_bucket.buckets : v.bucket]
}

# Output 6: ARN del bucket de backups protegido
output "arn_backups_protegido" {
  description = "ARN del bucket de backups con protect_destroy habilitado"
  value       = aws_s3_bucket.backups_protegido.arn
}

# Output 7: Información de la región y cuenta
output "info_cuenta" {
  description = "Información de la cuenta y región de AWS"
  value = {
    region  = data.aws_region.actual.name
    cuenta  = data.aws_caller_identity.actual.account_id
  }
}

Para probar el ejercicio:

# 1. Inicializar el directorio
terraform init

# 2. Revisar el plan de creación
terraform plan

# El plan mostrará:
# + aws_instance.servidores[0]    (crear servidor-1)
# + aws_instance.servidores[1]    (crear servidor-2)
# + aws_instance.servidores[2]    (crear servidor-3)
# + aws_s3_bucket.buckets["logs"]     (crear bucket logs)
# + aws_s3_bucket.buckets["backups"]  (crear bucket backups)
# + aws_s3_bucket.buckets["assets"]   (crear bucket assets)
# + aws_s3_bucket.backups_protegido   (crear bucket backups protegido)

# 3. Aplicar los cambios
terraform apply

# 4. Consultar los outputs
terraform output ips_privadas_servidores
terraform output arns_buckets
terraform output -json  # Todos los outputs en JSON

Errores comunes

Error 1: Dependencias circulares

# INCORRECTO: A depende de B y B depende de A -> Terraform no puede resolverlo
resource "aws_security_group" "sg_a" {
  vpc_id = aws_vpc.mi_vpc.id

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.sg_b.id]  # A depende de B
  }
}

resource "aws_security_group" "sg_b" {
  vpc_id = aws_vpc.mi_vpc.id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.sg_a.id]  # B depende de A -> ERROR!
  }
}

# CORRECTO: Separar las reglas en recursos aws_security_group_rule
resource "aws_security_group" "sg_a" {
  vpc_id = aws_vpc.mi_vpc.id
  # Sin referencias a sg_b aquí
}

resource "aws_security_group" "sg_b" {
  vpc_id = aws_vpc.mi_vpc.id
  # Sin referencias a sg_a aquí
}

# Las reglas que crean la dependencia se añaden después
resource "aws_security_group_rule" "a_permite_b" {
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  security_group_id        = aws_security_group.sg_a.id
  source_security_group_id = aws_security_group.sg_b.id
}

Error 2: count y for_each en el mismo recurso

# INCORRECTO: no puedes usar count y for_each juntos
resource "aws_instance" "web" {
  count    = 3
  for_each = var.instancias  # Error: solo uno de los dos
}

Error 3: for_each con lista en lugar de map o set

# INCORRECTO: for_each no acepta listas directamente
variable "nombres" {
  default = ["web", "api", "db"]
}

resource "aws_instance" "servidores" {
  for_each = var.nombres  # Error: debe ser map o set, no list
}

# CORRECTO: convertir la lista a set
resource "aws_instance" "servidores" {
  for_each = toset(var.nombres)  # Convertimos a set
}

Error 4: prevent_destroy no funciona con terraform destroy -target

# IMPORTANTE: prevent_destroy evita "terraform destroy" completo
# pero NO bloquea la eliminación del recurso del estado con terraform state rm
# Solo bloquea si el plan de Terraform intenta destruirlo

# Si necesitas destruir a pesar del prevent_destroy, debes:
# 1. Eliminar o comentar el bloque lifecycle en el código
# 2. Ejecutar terraform apply (para que el cambio entre en efecto)
# 3. Luego terraform destroy

Resumen

En este módulo has aprendido los conceptos fundamentales de los bloques resource y data:

  • El bloque resource crea y gestiona infraestructura. Su tipo tiene la forma proveedor_tiporecurso y necesita un nombre local único.
  • El meta-argumento depends_on define dependencias explícitas cuando Terraform no puede inferirlas automáticamente.
  • El meta-argumento count crea múltiples instancias identificadas por índice numérico; ideal para recursos idénticos.
  • El meta-argumento for_each crea recursos a partir de mapas o sets, identificados por clave string; más robusto ante cambios en la colección.
  • El bloque lifecycle controla el comportamiento en operaciones críticas: create_before_destroy para cero downtime, prevent_destroy para proteger recursos críticos, e ignore_changes para ignorar cambios externos.
  • El bloque data lee información de recursos existentes sin modificarlos; esencial para integrar con infraestructura existente.

Preparación para el siguiente módulo

En el siguiente módulo estudiaremos los Proveedores (Providers) en profundidad: cómo configurarlos, versionarlos, usar múltiples providers simultáneamente y gestionar la autenticación. Los providers son la capa que conecta Terraform con los servicios reales (AWS, Azure, GCP, etc.), y entenderlos bien es crucial para trabajar en entornos multi-cloud.

© Copyright 2026. Todos los derechos reservados