Introducción

Hasta ahora hemos visto cómo usar count y for_each para crear múltiples recursos del mismo tipo. Pero, ¿qué ocurre cuando necesitamos repetir bloques de configuración anidados dentro de un único recurso? Por ejemplo, un Security Group de AWS puede tener múltiples reglas de entrada (ingress) y salida (egress), y escribir cada una como un bloque estático se vuelve inmanejable cuando las reglas provienen de una variable.

Los bloques dinámicos (dynamic) resuelven exactamente este problema: permiten generar bloques de configuración repetitivos de forma programática, manteniendo el código limpio y flexible.


  1. ¿Qué son los Bloques Dinámicos?

Un bloque dinámico es una forma de iterar sobre una colección (lista o mapa) y generar múltiples bloques de configuración anidados dentro de un recurso. Son la solución de Terraform para evitar la repetición manual de bloques de configuración similares.

El problema que resuelven

Sin bloques dinámicos, tendrías que escribir así un Security Group con 4 reglas:

# SIN bloques dinámicos - Repetitivo y difícil de mantener
resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }

  # Si mañana necesitas añadir otra regla, debes editar el recurso
  # Si las reglas vienen de una variable externa, esto no es posible
}

Con bloques dinámicos, las reglas se generan automáticamente desde una variable:

# CON bloques dinámicos - Limpio y flexible
variable "ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))
}

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

  1. Sintaxis del Bloque dynamic

La sintaxis completa de un bloque dinámico es:

dynamic "nombre_del_bloque" {
  # for_each es el iterador: puede ser una lista, set o mapa
  for_each = coleccion

  # iterator (opcional): renombra la variable de iteración
  # Por defecto, tiene el mismo nombre que el bloque dinámico
  iterator = alias_personalizado

  # content define qué contiene cada bloque generado
  content {
    # Aquí se accede a los valores mediante nombre_del_bloque.value
    # o mediante alias_personalizado.value si se especificó iterator
    atributo = nombre_del_bloque.value.campo
  }
}

Variables disponibles dentro del bloque content

Variable Descripción
nombre_del_bloque.key La clave del elemento actual (índice si es lista, clave si es mapa)
nombre_del_bloque.value El valor completo del elemento actual
nombre_del_bloque.value.campo Un campo específico del elemento actual

  1. El Iterador y Cómo Personalizar su Nombre

Por defecto, la variable de iteración tiene el mismo nombre que el bloque dinámico. Esto puede ser confuso cuando el nombre del bloque es largo o cuando hay bloques dinámicos anidados. El atributo iterator te permite renombrarlo.

# Sin personalizar el iterador
dynamic "ingress" {
  for_each = var.ingress_rules
  content {
    from_port = ingress.value.port    # La variable se llama "ingress"
    to_port   = ingress.value.port
    protocol  = ingress.value.protocol
  }
}

# Con iterador personalizado (más legible)
dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule              # Renombramos "ingress" a "rule"
  content {
    from_port = rule.value.port    # Ahora se llama "rule"
    to_port   = rule.value.port
    protocol  = rule.value.protocol
    # Más claro semánticamente: estamos iterando sobre "rules"
  }
}

# El iterador personalizado es especialmente útil con bloques anidados
dynamic "statement" {
  for_each = var.policy_statements
  iterator = stmt                    # "stmt" en lugar de "statement"
  content {
    effect  = stmt.value.effect
    actions = stmt.value.actions
    # "stmt" es más conciso que "statement"
  }
}

  1. Cuándo Usar dynamic vs for_each en Recursos

Es importante distinguir cuándo usar bloques dinámicos y cuándo usar for_each directamente en recursos:

Escenario Usar for_each en recurso Usar dynamic
Crear múltiples instancias del mismo tipo de recurso Si No
Crear múltiples configuraciones anidadas dentro de UN recurso No Si
Las reglas de un Security Group No Si
Múltiples Security Groups Si No
Múltiples bloques ingress en un SG No Si
Múltiples buckets S3 Si No
Múltiples reglas de lifecycle en un bucket No Si

  1. Ejemplo 1: Security Group con Reglas Dinámicas

# variables.tf

variable "project_name" {
  type    = string
  default = "myapp"
}

variable "environment" {
  type    = string
  default = "prod"
}

variable "ingress_rules" {
  description = "Lista de reglas de entrada para el security group"
  type = list(object({
    description = string
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      description = "HTTP desde internet"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS desde internet"
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "SSH solo desde red interna"
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
      # Solo desde la red privada, no desde internet
    },
    {
      description = "API interna"
      from_port   = 8080
      to_port     = 8090
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

variable "egress_rules" {
  description = "Lista de reglas de salida para el security group"
  type = list(object({
    description = string
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      description = "Todo el tráfico de salida"
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      # "-1" significa "todos los protocolos" en AWS
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}
# main.tf

resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.environment}-web-sg"
  description = "Security group para servidores web en ${var.environment}"
  vpc_id      = aws_vpc.main.id

  # Bloque dinámico para reglas de ENTRADA
  dynamic "ingress" {
    for_each = var.ingress_rules
    # Iteramos sobre la lista de reglas de entrada
    iterator = rule
    # Renombramos el iterador a "rule" para mayor claridad

    content {
      description = rule.value.description
      # rule.value es el objeto completo de la regla actual
      from_port   = rule.value.from_port
      to_port     = rule.value.to_port
      protocol    = rule.value.protocol
      cidr_blocks = rule.value.cidr_blocks
    }
  }

  # Bloque dinámico para reglas de SALIDA
  dynamic "egress" {
    for_each = var.egress_rules
    iterator = rule

    content {
      description = rule.value.description
      from_port   = rule.value.from_port
      to_port     = rule.value.to_port
      protocol    = rule.value.protocol
      cidr_blocks = rule.value.cidr_blocks
    }
  }

  tags = {
    Name        = "${var.project_name}-${var.environment}-web-sg"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  lifecycle {
    create_before_destroy = true
    # Crea el nuevo SG antes de destruir el viejo para evitar downtime
  }
}

Resultado: ¿Qué genera Terraform?

Terraform generará automáticamente uno de estos bloques por cada elemento en var.ingress_rules:

# Terraform genera internamente algo equivalente a:
ingress {
  description = "HTTP desde internet"
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

ingress {
  description = "HTTPS desde internet"
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}
# ... y así para cada regla en la lista

  1. Ejemplo 2: IAM Policy con Statements Dinámicos

# iam.tf

variable "s3_policy_statements" {
  description = "Lista de statements para la política IAM de S3"
  type = list(object({
    sid       = string
    effect    = string
    actions   = list(string)
    resources = list(string)
    conditions = optional(list(object({
      test     = string
      variable = string
      values   = list(string)
    })), [])
    # conditions es opcional y por defecto una lista vacía
  }))
  default = [
    {
      sid    = "AllowReadPublicBucket"
      effect = "Allow"
      actions = [
        "s3:GetObject",
        "s3:ListBucket"
      ]
      resources = [
        "arn:aws:s3:::my-public-bucket",
        "arn:aws:s3:::my-public-bucket/*"
      ]
      conditions = []
    },
    {
      sid    = "AllowWritePrivateBucket"
      effect = "Allow"
      actions = [
        "s3:PutObject",
        "s3:DeleteObject"
      ]
      resources = [
        "arn:aws:s3:::my-private-bucket/*"
      ]
      conditions = [
        {
          test     = "StringEquals"
          variable = "s3:prefix"
          values   = ["uploads/"]
          # Solo permite escribir en el prefijo "uploads/"
        }
      ]
    }
  ]
}

resource "aws_iam_policy" "s3_access" {
  name        = "s3-access-policy"
  description = "Política de acceso a buckets S3"

  policy = jsonencode({
    Version = "2012-10-17"
    # jsonencode convierte el objeto HCL a JSON para la API de IAM

    Statement = [
      # Usamos una for expression en lugar de dynamic aquí porque
      # el bloque Statement es un array JSON, no bloques HCL anidados
      for stmt in var.s3_policy_statements : {
        Sid      = stmt.sid
        Effect   = stmt.effect
        Action   = stmt.actions
        Resource = stmt.resources

        # Usamos el operador ternario para condiciones opcionales
        Condition = length(stmt.conditions) > 0 ? {
          # Si hay condiciones, las procesamos
          for cond in stmt.conditions : cond.test => {
            (cond.variable) = cond.values
          }
        } : null
        # Si no hay condiciones, el campo Condition es null (se omite)
      }
    ]
  })
}

  1. Bloques Dinámicos Anidados

Los bloques dinámicos pueden anidarse para manejar estructuras más complejas. Hay que tener cuidado con los nombres de los iteradores para evitar confusiones.

# Ejemplo: ECS Task Definition con múltiples contenedores y múltiples variables de entorno
variable "containers" {
  description = "Lista de contenedores para la task definition"
  type = list(object({
    name   = string
    image  = string
    cpu    = number
    memory = number
    port_mappings = list(object({
      container_port = number
      host_port      = number
      protocol       = string
    }))
    environment_vars = list(object({
      name  = string
      value = string
    }))
  }))
}

resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "512"
  memory                   = "1024"

  container_definitions = jsonencode([
    for container in var.containers : {
      name      = container.name
      image     = container.image
      cpu       = container.cpu
      memory    = container.memory

      portMappings = [
        for pm in container.port_mappings : {
          containerPort = pm.container_port
          hostPort      = pm.host_port
          protocol      = pm.protocol
        }
      ]

      environment = [
        for env in container.environment_vars : {
          name  = env.name
          value = env.value
        }
      ]
    }
  ])
  # Aquí usamos for expressions dentro de jsonencode en lugar de
  # dynamic blocks porque estamos generando JSON, no bloques HCL
}

Cuándo los bloques dinámicos anidados son necesarios

# Caso donde SÍ necesitamos dynamic anidados: atributos HCL anidados
# Ejemplo: AWS WAF con reglas y condiciones anidadas

resource "aws_wafv2_web_acl" "main" {
  name  = "web-acl"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  # Bloque dinámico de PRIMER NIVEL: reglas
  dynamic "rule" {
    for_each = var.waf_rules
    iterator = waf_rule

    content {
      name     = waf_rule.value.name
      priority = waf_rule.key
      # waf_rule.key es el índice en la lista (0, 1, 2...)

      action {
        dynamic "allow" {
          for_each = waf_rule.value.action == "allow" ? [1] : []
          # [1] genera un bloque, [] no genera ninguno
          # Esto es un patrón común: usar [1] o [] como condición booleana
          content {}
        }

        dynamic "block" {
          for_each = waf_rule.value.action == "block" ? [1] : []
          content {}
        }
      }

      statement {
        # Bloque dinámico de SEGUNDO NIVEL: condiciones dentro de cada regla
        dynamic "ip_set_reference_statement" {
          for_each = waf_rule.value.ip_set_arn != null ? [waf_rule.value.ip_set_arn] : []
          iterator = ip_ref
          content {
            arn = ip_ref.value
          }
        }
      }

      visibility_config {
        cloudwatch_metrics_enabled = true
        metric_name                = waf_rule.value.name
        sampled_requests_enabled   = true
      }
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "web-acl"
    sampled_requests_enabled   = true
  }
}

  1. La Expresión for: Transformar Listas y Mapas

La expresión for (sin ser un bloque dinámico) permite transformar colecciones dentro de expresiones. Es complementaria a los bloques dinámicos.

Sintaxis

# Para generar una lista:
[for elemento in coleccion : expresion_de_transformacion]
[for elemento in coleccion : expresion IF condicion_filtro]

# Para generar un mapa:
{for clave, valor in mapa : nueva_clave => nuevo_valor}
{for elemento in lista : elemento.clave => elemento.valor}

Ejemplos prácticos

variable "instances" {
  type = list(object({
    name = string
    az   = string
    type = string
  }))
  default = [
    { name = "web-01", az = "us-east-1a", type = "t3.micro" },
    { name = "web-02", az = "us-east-1b", type = "t3.small" },
    { name = "db-01",  az = "us-east-1a", type = "t3.medium" }
  ]
}

locals {
  # Extraer solo los nombres de las instancias
  instance_names = [for inst in var.instances : inst.name]
  # → ["web-01", "web-02", "db-01"]

  # Filtrar solo instancias web
  web_instances = [for inst in var.instances : inst if startswith(inst.name, "web")]
  # → [{name="web-01",...}, {name="web-02",...}]

  # Convertir lista a mapa (nombre → tipo)
  instance_type_map = {for inst in var.instances : inst.name => inst.type}
  # → {"web-01" = "t3.micro", "web-02" = "t3.small", "db-01" = "t3.medium"}

  # Transformar valores de un mapa existente
  instance_names_upper = {
    for name, type in local.instance_type_map : upper(name) => type
  }
  # → {"WEB-01" = "t3.micro", "WEB-02" = "t3.small", "DB-01" = "t3.medium"}

  # Agrupar instancias por zona de disponibilidad
  instances_by_az = {
    for az in distinct([for inst in var.instances : inst.az]) :
    az => [for inst in var.instances : inst.name if inst.az == az]
  }
  # → {"us-east-1a" = ["web-01", "db-01"], "us-east-1b" = ["web-02"]}
}

  1. Diferencia entre for Expressions y for_each

Característica for expression for_each en recurso/bloque dinámico
Propósito Transformar datos en locals/variables Crear múltiples recursos o bloques
Resultado Una lista o mapa de valores Múltiples instancias de un recurso o bloque
Referenciable como recurso No Si (como aws_instance.web["key"])
Aparece en el plan No (solo datos internos) Si (como recursos a crear/modificar)
Dónde se usa Dentro de locals, variable, argumentos En el bloque de un recurso, dynamic

  1. Tabla: Comparativa de Técnicas de Iteración en Terraform

Técnica Escenario ideal Sintaxis básica Acceso a elementos
count N recursos idénticos o casi idénticos count = 3 aws_instance.web[0]
for_each en recurso N recursos con configuración variada por clave for_each = var.instances aws_instance.web["web-01"]
dynamic Múltiples bloques anidados en UN recurso dynamic "ingress" { for_each = ... } Solo dentro del recurso
for expression Transformar datos (listas/mapas) [for x in list : x.campo] Como cualquier local
for en jsonencode Generar JSON con arrays dinámicos Dentro de jsonencode(...) Solo como JSON

  1. Ejercicio Práctico: Security Group con Reglas Totalmente Dinámicas

Objetivo

Crear un módulo de Security Group que acepte las reglas como variables y las aplique dinámicamente. La solución debe manejar tanto reglas por puerto único como por rangos de puertos.

Instrucciones

  1. Crea un directorio sg-module/
  2. Define una variable ingress_rules con tipo flexible
  3. Usa bloques dinámicos para generar las reglas
  4. Incluye un output con información del SG creado

Solución Detallada

# sg-module/variables.tf

variable "name" {
  description = "Nombre del security group"
  type        = string
}

variable "description" {
  description = "Descripción del security group"
  type        = string
  default     = "Managed by Terraform"
}

variable "vpc_id" {
  description = "ID de la VPC donde crear el security group"
  type        = string
}

variable "ingress_rules" {
  description = "Reglas de entrada. Cada regla puede ser por puerto único o rango."
  type = list(object({
    description      = string
    from_port        = number
    to_port          = number
    protocol         = string
    cidr_blocks      = optional(list(string), [])
    # optional() permite omitir el campo; usa [] por defecto
    ipv6_cidr_blocks = optional(list(string), [])
    self             = optional(bool, false)
    # self = true permite tráfico entre instancias del mismo SG
  }))
  default = []
}

variable "egress_allow_all" {
  description = "Si true, añade una regla que permite todo el tráfico de salida"
  type        = bool
  default     = true
}

variable "tags" {
  description = "Tags adicionales para el security group"
  type        = map(string)
  default     = {}
}
# sg-module/main.tf

locals {
  # Preparamos las reglas de egress
  # Si egress_allow_all = true, creamos la regla "allow all"
  # Si egress_allow_all = false, no generamos ninguna regla de egress
  egress_rules = var.egress_allow_all ? [
    {
      description      = "Permite todo el tráfico de salida"
      from_port        = 0
      to_port          = 0
      protocol         = "-1"
      cidr_blocks      = ["0.0.0.0/0"]
      ipv6_cidr_blocks = ["::/0"]
      self             = false
    }
  ] : []
  # Lista vacía = no se generarán bloques egress
}

resource "aws_security_group" "this" {
  # "this" es una convención en módulos de Terraform: el recurso principal
  name        = var.name
  description = var.description
  vpc_id      = var.vpc_id

  # Bloque dinámico para reglas de ENTRADA
  dynamic "ingress" {
    for_each = var.ingress_rules
    # Terraform asigna automáticamente un índice (0, 1, 2...) como clave

    content {
      description      = ingress.value.description
      from_port        = ingress.value.from_port
      to_port          = ingress.value.to_port
      protocol         = ingress.value.protocol
      cidr_blocks      = ingress.value.cidr_blocks
      ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks
      self             = ingress.value.self
    }
  }

  # Bloque dinámico para reglas de SALIDA
  dynamic "egress" {
    for_each = local.egress_rules
    # Será vacío si egress_allow_all = false

    content {
      description      = egress.value.description
      from_port        = egress.value.from_port
      to_port          = egress.value.to_port
      protocol         = egress.value.protocol
      cidr_blocks      = egress.value.cidr_blocks
      ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks
      self             = egress.value.self
    }
  }

  tags = merge(var.tags, {
    Name      = var.name
    ManagedBy = "Terraform"
  })

  lifecycle {
    create_before_destroy = true
    # Siempre crear el nuevo SG antes de destruir el viejo
    # Esto evita que las instancias queden sin SG durante la actualización
  }
}
# sg-module/outputs.tf

output "security_group_id" {
  description = "ID del security group creado"
  value       = aws_security_group.this.id
}

output "security_group_name" {
  description = "Nombre del security group"
  value       = aws_security_group.this.name
}

output "ingress_rules_count" {
  description = "Número de reglas de entrada aplicadas"
  value       = length(var.ingress_rules)
}
# Uso del módulo desde el proyecto principal (main.tf)

module "web_sg" {
  source = "./sg-module"

  name    = "web-server-sg"
  vpc_id  = aws_vpc.main.id

  ingress_rules = [
    {
      description = "HTTP público"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS público"
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "SSH solo desde VPN"
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["203.0.113.0/24"]
      # Solo desde la IP de la VPN corporativa
    },
    {
      description      = "Monitoreo interno"
      from_port        = 9100
      to_port          = 9100
      protocol         = "tcp"
      cidr_blocks      = ["10.0.0.0/8"]
      # Puerto del Node Exporter de Prometheus
    }
  ]

  egress_allow_all = true

  tags = {
    Environment = "production"
    Team        = "platform"
  }
}

module "db_sg" {
  source = "./sg-module"

  name    = "database-sg"
  vpc_id  = aws_vpc.main.id

  ingress_rules = [
    {
      description = "PostgreSQL desde servidores web"
      from_port   = 5432
      to_port     = 5432
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/24"]
      # Solo desde la subred de los servidores web
    },
    {
      description = "PostgreSQL entre réplicas"
      from_port   = 5432
      to_port     = 5432
      protocol    = "tcp"
      self        = true
      # Tráfico entre instancias del mismo SG (réplicas de BD)
    }
  ]

  egress_allow_all = false
  # La base de datos no necesita acceso saliente irrestricto

  tags = {
    Environment = "production"
    Team        = "data"
  }
}

Verificar el resultado

# Inicializar y planificar
terraform init
terraform plan

# El plan mostrará algo como:
# + resource "aws_security_group" "this" {   (módulo web_sg)
#   + ingress {
#       + from_port   = 80
#       ...
#     }
#   + ingress {
#       + from_port   = 443
#       ...
#     }
#   + ingress {
#       + from_port   = 22
#       ...
#     }
#   + ingress {
#       + from_port   = 9100
#       ...
#     }
#   + egress {
#       + from_port   = 0
#       ...
#     }
# }

  1. Errores Comunes con Bloques Dinámicos

Error 1: Usar dynamic con bloques que no lo soportan

# Problema: No todos los bloques dentro de un recurso pueden ser dinámicos
resource "aws_s3_bucket" "example" {
  bucket = "my-bucket"

  # CORRECTO: lifecycle_rule es un bloque repetible → soporta dynamic
  dynamic "lifecycle_rule" {
    for_each = var.lifecycle_rules
    content {
      id      = lifecycle_rule.value.id
      enabled = lifecycle_rule.value.enabled
      # ...
    }
  }

  # INCORRECTO: El bloque "website" solo puede aparecer UNA vez
  # No tendría sentido hacerlo dynamic
  # dynamic "website" { ... }  ← Error o resultado inesperado
}

Error 2: Confundir for_each de recurso con for_each de dynamic

# Ambos usan for_each pero tienen propósitos totalmente diferentes

# for_each EN EL RECURSO: Crea 3 security groups distintos
resource "aws_security_group" "per_env" {
  for_each = toset(["dev", "staging", "prod"])
  name     = "sg-${each.key}"
}

# dynamic con for_each: Crea UN security group con 3 reglas
resource "aws_security_group" "with_rules" {
  name = "sg-with-rules"
  dynamic "ingress" {
    for_each = toset(["80", "443", "22"])
    content {
      from_port   = tonumber(ingress.value)
      to_port     = tonumber(ingress.value)
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

Error 3: Acceder a campos que no existen en value

variable "rules" {
  type = list(object({
    port     = number
    protocol = string
    # "description" NO está definido en el objeto
  }))
}

resource "aws_security_group" "example" {
  dynamic "ingress" {
    for_each = var.rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      description = ingress.value.description  # ERROR: campo no definido en el tipo
    }
  }
}

# Solución 1: Añadir el campo al tipo
variable "rules" {
  type = list(object({
    port        = number
    protocol    = string
    description = optional(string, "")  # optional con valor por defecto
  }))
}

# Solución 2: Usar un valor por defecto con try()
content {
  description = try(ingress.value.description, "")
  # try() devuelve el primer valor que no cause error
}

Error 4: Olvidar que for_each en dynamic NO acepta listas directamente cuando son de objetos complejos

# A veces necesitas convertir la lista a un mapa para mejor rastreabilidad
variable "rules" {
  type = list(object({
    name = string
    port = number
  }))
}

resource "aws_security_group" "example" {
  # Opción 1: Usar la lista directamente (índice numérico como key)
  dynamic "ingress" {
    for_each = var.rules      # El key será 0, 1, 2...
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = ingress.value.name
    }
  }

  # Opción 2: Convertir a mapa por nombre para mejor identificación en el estado
  # (más útil cuando las reglas pueden cambiar de orden)
  dynamic "ingress" {
    for_each = {for rule in var.rules : rule.name => rule}
    # Ahora el key es rule.name en lugar de un índice numérico
    iterator = rule_entry
    content {
      from_port   = rule_entry.value.port
      to_port     = rule_entry.value.port
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = rule_entry.key    # El key es el nombre de la regla
    }
  }
}

Resumen

En esta lección hemos dominado los bloques dinámicos de Terraform:

  • Los bloques dynamic permiten generar bloques de configuración anidados de forma programática, evitando la repetición manual.
  • La sintaxis básica es dynamic "nombre_bloque" { for_each = coleccion content { ... } }.
  • El iterador (iterator) permite renombrar la variable de acceso para mayor claridad, especialmente en bloques anidados.
  • Las for expressions complementan a los bloques dinámicos para transformar datos antes de usarlos.
  • El patrón for_each = condicion ? [1] : [] permite hacer bloques opcionales de forma elegante.
  • Es fundamental distinguir entre for_each en recursos (crea múltiples recursos) y for_each en dynamic (crea múltiples bloques dentro de un recurso).

En la siguiente y última lección del módulo 6 aprenderemos sobre terraform import, una herramienta esencial para adoptar Terraform en proyectos con infraestructura existente.

© Copyright 2026. Todos los derechos reservados