Un módulo de infraestructura es de esas cosas que escribes una vez y sufres muchas. Empiezas con un módulo limpio para levantar un servicio en ECS Fargate, y seis meses después es un monstruo con cuarenta variables, tres booleanos que activan comportamientos incompatibles y un count anidado que nadie entiende. Migré varios proyectos de Terraform a OpenTofu y en el camino reescribí casi todos mis módulos, y lo que aprendí no va de sintaxis: va de resistir la tentación de que un módulo lo haga todo. Este es el conjunto de reglas que sigo hoy para que la infraestructura como código siga siendo un activo y no una losa.
Un módulo modela un concepto, no una lista de recursos
El error que cometí más veces fue crear módulos alrededor de "cosas que suelo desplegar juntas" en lugar de alrededor de un concepto con una frontera clara. Un módulo "servicio-web" que crea el servicio de ECS, su balanceador, su DNS, su base de datos y sus alarmas parece cómodo, pero acopla decisiones que cambian a ritmos distintos. El día que quieres un servicio sin base de datos, o con la base de datos compartida con otro, el módulo te pelea.
La regla que me funciona es que un módulo debe modelar un concepto que tenga sentido nombrar por sí solo. "Un servicio en Fargate" es un concepto. "Una cola SQS con su DLQ" es un concepto. "Todo lo que necesita el proyecto X" no lo es: es una composición, y las composiciones van en la raíz, no dentro de un módulo. Cuando cada módulo tiene una frontera conceptual limpia, componerlos es fácil y sustituir uno por otro no rompe el resto.
Los booleanos de configuración son deuda disfrazada
Cada vez que añades una variable enable_algo a un módulo, estás metiendo una bifurcación en su comportamiento. Con dos o tres flags todavía se entiende. Con ocho, tu módulo tiene cientos de combinaciones posibles y solo has probado las cuatro que usas. El resto son campos de minas esperando a alguien.
# El módulo que envejece mal: se configura con flags
# que activan ramas de comportamiento incompatibles.
module "servicio" {
source = "./modules/servicio"
enable_https = true
enable_autoscaling = true
enable_spot = false
enable_efs = true
# ...y así hasta que nadie sabe qué combinaciones funcionan
}
Prefiero módulos con menos opciones y más pequeños, y resolver la variación componiendo en lugar de configurando. Si un servicio necesita almacenamiento persistente y otro no, no meto un flag enable_efs: hago que el volumen sea un módulo aparte que conecto cuando hace falta. El módulo de servicio no sabe nada de EFS y no tiene una rama sin probar. La composición se ve en la raíz, donde debe verse, y no escondida tras un booleano.
Las salidas son el contrato, cuídalas como una API
Lo que un módulo expone en sus outputs es su interfaz pública, y cambiarla rompe a quien lo consume igual que romperías a los clientes de una API. Al principio exponía de todo "por si acaso", y acabé con módulos cuyos consumidores dependían de detalles internos que yo quería cambiar. Ahora trato las salidas con la misma disciplina que un contrato: expongo lo mínimo que un consumidor necesita para conectar este módulo con otro —un ARN, un id, un endpoint— y nada de la fontanería interna.
# Expón identificadores para componer, no recursos enteros.
output "service_arn" {
value = aws_ecs_service.this.id
}
output "task_role_arn" {
# Otros módulos adjuntan políticas a este rol;
# ese es el punto de extensión que quiero ofrecer.
value = aws_iam_role.task.arn
}
Exponer el ARN de un rol en vez del rol entero es un ejemplo pequeño pero importante: le doy al consumidor el punto exacto donde extender (adjuntar una política) sin abrirle el recurso completo para que manosee cosas que romperían mi módulo.
💡 Antes de añadir una variable a un módulo, pregúntate si la variación pertenece dentro del módulo o en quien lo compone. La mayoría de las veces pertenece fuera. Un módulo con pocas entradas y salidas claras se reutiliza; uno con veinte flags se copia y se pega.
El estado es donde se pagan los errores de diseño
En IaC el diseño no se prueba de verdad hasta que aplicas cambios sobre infraestructura existente, y ahí es donde el estado te pasa factura. Un módulo mal fronterizado se nota cuando un plan quiere destruir y recrear algo que solo querías tocar de refilón, porque moviste un recurso de sitio o cambiaste un count por un for_each. Migrando a OpenTofu aprendí a tratar la estructura del estado como parte del diseño, no como un detalle: los moved blocks existen precisamente para refactorizar sin destruir, y usarlos con cuidado es lo que te deja reorganizar módulos sin un susto en producción.
Elegí módulos pequeños y de frontera clara sabiendo el precio: hay más piezas que componer en la raíz y la composición es más verbosa que un mega-módulo con flags. A cambio, cada pieza es entendible, testeable y sustituible por su cuenta, y un cambio en una no amenaza con recrear media infraestructura. Para código que va a vivir años y sobre el que aplico cambios cada semana, esa previsibilidad vale mucho más que la comodidad de escribir menos líneas el primer día.
Dónde pongo el esfuerzo hoy
Si tuviera que resumir todo en una frase: diseña módulos pensando en cómo se van a componer y a cambiar, no en cuánto escribes hoy. La infraestructura como código no es un script que ejecutas una vez, es una base de código que mantienes, y sufre los mismos males que cualquier otra —acoplamiento, ramas sin probar, contratos que se rompen— con el agravante de que aquí un error puede tirar un servicio. Menos opciones, fronteras claras y salidas tratadas como API es lo que hace que un módulo siga siendo tuyo dentro de seis meses en vez de ser el trozo del repo que nadie quiere tocar.