Translate

miércoles, 21 de octubre de 2026

Terraform avanzado: dynamic blocks y dependencias entre recursos


En los posts anteriores empezamos con los conceptos básicos de Terraform y fuimos aumentando la complejidad.

Ahora podemos empezar a enfrentarnos con un problema que aparece constantemente en infraestructura real.


Un resource puede contener bloques de configuración repetitivos.


Por ejemplo, imaginemos una regla de firewall:

regla 1 → puerto 80

regla 2 → puerto 443

regla 3 → puerto 8080


Podríamos escribir cada bloque manualmente.

Pero si las reglas vienen de una variable, ¿cómo hacemos para generar esos bloques dinámicamente?

Ahí aparece: dynamic

Un dynamic block permite generar bloques de configuración repetidos dentro de un resource.

La idea es parecida a un for_each, pero hay una diferencia fundamental.

Con for_each podemos crear resources:


resource "..." "..." {

  for_each = ...

}


Con dynamic podemos generar nested blocks dentro de un resource.


Conceptualmente:


for_each

   │

   └── crea resources


dynamic

   │

   └── crea bloques dentro de un resource


Supongamos que un resource tuviera una estructura como:


resource "example" "server" {

  rule {

    port = 80

  }


  rule {

    port = 443

  }

}


Tenemos dos bloques:


server

 ├── rule

 └── rule


Ahora supongamos que los puertos vienen de una variable:


variable "ports" {

  default = [80, 443, 8080]

}


Queremos generar:


rule {

  port = 80

}


rule {

  port = 443

}


rule {

  port = 8080

}


Podemos utilizar dynamic.


resource "example" "server" {


  dynamic "rule" {

    for_each = var.ports


    content {

      port = rule.value

    }

  }

}


Terraform genera conceptualmente:


server

 ├── rule { port = 80 }

 ├── rule { port = 443 }

 └── rule { port = 8080 }


¿Qué significa content?

Esta parte suele resultar extraña al principio. Podemos leerlo como: Por cada elemento de var.ports, generá un bloque llamado rule cuyo contenido sea el bloque content.


Si: var.ports = [80, 443, 8080]


Terraform conceptualmente produce:


rule {

  port = 80

}


rule {

  port = 443

}


rule {

  port = 8080

}


dynamic necesita que el provider soporte ese bloque

Este detalle es importante.

No podemos inventar cualquier bloque:


dynamic "foo" {

  ...

}


El resource tiene que soportar un bloque foo.

Por eso dynamic depende de la estructura definida por el provider.


Por ejemplo, un resource puede tener:


resource "..." "..." {


  ingress {

    ...

  }


  egress {

    ...

  }

}


Entonces podemos generar dinámicamente ingress o egress.

Pero no podemos inventar:


dynamic "banana" {

  ...

}


si ese resource no tiene un bloque banana.

Los recursos de infraestructura suelen tener configuraciones como reglas de red.


Podríamos tener:


variable "ingress_rules" {

  default = [

    {

      port     = 80

      protocol = "tcp"

    },

    {

      port     = 443

      protocol = "tcp"

    }

  ]

}


Y un resource que soporte bloques ingress.


Conceptualmente:


resource "example_security_group" "app" {


  dynamic "ingress" {

    for_each = var.ingress_rules


    content {

      port     = ingress.value.port

      protocol = ingress.value.protocol

    }

  }

}


La configuración resultante tendría dos bloques:


ingress

  port = 80

  protocol = tcp


ingress

  port = 443

  protocol = tcp


Esto es mucho más flexible que escribir las reglas manualmente.

¿Por qué no utilizar simplemente for_each? Porque for_each y dynamic resuelven problemas diferentes.

Supongamos que tenemos:


resource "example_server" "server" {

  for_each = var.servers

}


Esto crea:

server["web"]

server["api"]

server["worker"]


Estamos creando tres resources.


En cambio:


resource "example_server" "server" {

  dynamic "rule" {

    for_each = var.rules

    content {

      ...

    }

  }

}


crea un solo resource pero con varios bloques:


server

 ├── rule

 ├── rule

 └── rule


La diferencia es fundamental.


Ahora pasemos a otro concepto importante.

Terraform necesita conocer las dependencias entre los recursos.


Supongamos que tenemos:


network

   │

   ▼

server


La máquina necesita que la red exista antes.

¿Cómo sabe Terraform que existe esa relación?

Muchas veces la dependencia aparece automáticamente a través de referencias.


Por ejemplo:


resource "example_network" "main" {

  name = "main"

}


resource "example_server" "app" {

  network_id = example_network.main.id

}


La expresión:

example_network.main.id


no solamente obtiene un valor.

También establece una dependencia.

Terraform entiende:


example_network.main

          │

          ▼

example_server.app


Por lo tanto, primero debe crear la red y después el servidor.


Dependencias implícitas: Este tipo de dependencia se conoce como una dependencia implícita.


Por ejemplo:

resource "A" "a" {

  ...

}


resource "B" "b" {

  resource_id = A.a.id

}


Terraform puede deducir: A → B


No necesitamos decir explícitamente: "Creá A antes que B."

La referencia ya expresa esa relación.

Esto es una de las ventajas del enfoque declarativo.


A veces existe una dependencia que Terraform no puede deducir automáticamente.

Por ejemplo:

resource "example_network" "main" {

  ...

}


resource "example_server" "app" {

  ...

}


Supongamos que sabemos que el servidor depende de la red, pero nuestra configuración no tiene ninguna referencia directa entre ellos.


Podemos indicarlo:


resource "example_server" "app" {


  depends_on = [

    example_network.main

  ]

}


Ahora estamos diciendo explícitamente:


example_network.main

          │

          ▼

example_server.app


¿Cuándo usar depends_on?

La regla general es: Utilizá dependencias implícitas cuando sea posible.

Es decir, preferimos:

network_id = example_network.main.id


antes que:

depends_on = [

  example_network.main

]


si realmente necesitamos el id de la red.

¿Por qué? Porque la referencia expresa dos cosas al mismo tiempo:

  1. qué valor necesitamos;
  2. de qué recurso dependemos.

Es mucho más descriptivo. depends_on no debería ser nuestra primera herramienta

Un error frecuente es empezar a agregar: depends_on = [...] por todas partes.


Por ejemplo:


resource "A" "a" {

  ...

}


resource "B" "b" {

  depends_on = [A.a]

}


resource "C" "c" {

  depends_on = [B.b]

}


resource "D" "d" {

  depends_on = [C.c]

}


Podemos terminar creando una enorme cadena artificial: A → B → C → D

aunque Terraform podría haber determinado parte de esas relaciones por sí mismo.


Esto reduce la capacidad de Terraform para ejecutar operaciones en paralelo.

Este punto es muy interesante.


Supongamos:


network

   │

   ├── server

   │

   └── database


server y database dependen de network, pero no dependen entre ellos.

Terraform puede conceptualizarlo como:

        network

        /     \

       /       \

   server    database


Una vez creada la red, ambos recursos pueden avanzar independientemente.

En cambio, si agregamos dependencias innecesarias:


network

   │

   ▼

server

   │

   ▼

database


estamos obligando a Terraform a realizar operaciones secuencialmente.

Por eso las dependencias correctas son importantes tanto para la correctitud como para el rendimiento.

Todo esto nos lleva a una idea muy interesante.

Terraform puede representar nuestra infraestructura como un grafo de dependencias.


Por ejemplo:

             network

            /       \

           /         \

       subnet       database

          |

          ▼

        server

          |

          ▼

    load_balancer


Los nodos representan recursos.

Las flechas representan dependencias.

Terraform utiliza esta información para determinar el orden de las operaciones.

Por eso una configuración declarativa puede funcionar sin que nosotros escribamos:

  1. crear network
  2. crear subnet
  3. crear server
  4. crear load balancer


Terraform construye ese orden a partir de las relaciones.

Ahora podemos combinar varios conceptos que vimos en los últimos artículos.

Supongamos:


variable "servers" {

  type = map(object({

    size = string

  }))


  default = {

    web = {

      size = "small"

    }


    api = {

      size = "medium"

    }

  }

}


Podemos crear múltiples servidores:


resource "example_server" "server" {

  for_each = var.servers


  name = each.key

  size = each.value.size

}


Y podríamos tener otro recurso que dependa de esos servidores.

Por ejemplo:


resource "example_load_balancer" "main" {


  backend_ids = [

    for server in example_server.server :

    server.id

  ]

}


La expresión:


[

  for server in example_server.server :

  server.id

]


construye una lista con los IDs de todos los servidores.

Terraform puede deducir que el load balancer depende de ellos.

Tenemos entonces:


                      network

                            │

                            ▼

      ┌───────┴───────┐

      ▼                                         ▼

    web                                        api

      \                                           /

        \                                       /

          ▼                              ▼

                 load balancer


Esto ya se parece bastante a una infraestructura real.

Aunque dynamic es útil, hay que tener cuidado.

Es tentador transformar cualquier cosa en:


dynamic "something" {

  for_each = ...

  content {

    ...

  }

}


Pero podemos terminar con una configuración difícil de leer.

Por ejemplo, si solamente tenemos dos reglas:


ingress {

  port = 80

}


ingress {

  port = 443

}


puede ser mucho más claro escribirlas directamente.

No todo necesita ser dinámico.

Una buena regla práctica es: Usá dynamic cuando realmente necesites generar un número variable de bloques.


Si la configuración es pequeña y fija, escribirla explícitamente puede ser mejor.


Esto:


dynamic "ingress" {

  for_each = var.ingress_rules


  content {

    port     = ingress.value.port

    protocol = ingress.value.protocol

  }

}


es muy útil cuando:


development → 2 reglas

production  → 8 reglas


Pero si siempre tenemos exactamente:


HTTP

HTTPS


quizás sea más claro:


ingress {

  port     = 80

  protocol = "tcp"

}


ingress {

  port     = 443

  protocol = "tcp"

}


Terraform es código.

Y, como cualquier código, la claridad importa.

En los primeros artículos Terraform parecía bastante sencillo: resource

Después agregamos:

  • variables
  • outputs
  • modules


Y ahora tenemos:


resources

   │

   ├── count

   ├── for_each

   ├── expressions

   └── dynamic blocks

            │

            ▼

      dependencias

            │

            ▼

       dependency graph


Esto cambia bastante la forma en que podemos modelar infraestructura.

Ya no estamos simplemente escribiendo: "Crear un servidor."

Estamos describiendo relaciones: "Quiero estos servidores, con estas configuraciones, conectados a esta infraestructura y utilizados por este balanceador."

Terraform se ocupa de convertir ese modelo en un conjunto de operaciones.


No hay comentarios.:

Publicar un comentario