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:
- qué valor necesitamos;
- 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:
- crear network
- crear subnet
- crear server
- 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