La infraestructura multimarca deriva en copy-paste por defecto. Una marca nueva empieza como un clon de la anterior, el clon acumula ediciones locales y, seis meses después, una corrección en un módulo compartido hay que aplicarla a mano en cada clon — o simplemente no se aplica. La jerarquía Terragrunt de cuatro capas a la que sigo volviendo existe para que esa deriva sea estructuralmente imposible, no solo desaconsejada.
El argumento es estrecho: la mayoría de los equipos toman "compartí módulos, sobreescribí en la hoja" como regla y se quedan ahí. Esa regla es necesaria pero no suficiente. Lo que realmente impide la deriva es ser explícito sobre qué capa tiene permiso para conocer qué, y no dejar que ninguna capa estire la mano hacia abajo.
Las cuatro capas
Raíz (root.hcl) gestiona la configuración del estado remoto y la generación del provider. Conoce el bucket S3, la tabla de locks de DynamoDB y qué versión del provider de AWS está aprobada. Nada por debajo lo duplica. Si alguna vez sentís la tentación de sobreescribir la versión del provider en una hoja, ya perdiste.
Marca (brand.hcl) centraliza las fuentes de módulos y las URIs de imágenes. Sabe qué módulos usa una marca y qué repositorios ECR contienen sus imágenes de contenedor. Agregar una marca nueva es exactamente un archivo nuevo en esta capa — y ese es el test para saber si la capa está haciendo su trabajo.
Entorno (env.hcl) contiene los parámetros de escala: tipos de instancia, recuentos mínimo/máximo de réplicas, almacenamiento de RDS, retención. No sabe nada de módulos ni imágenes — solo de parámetros de escala que difieren entre staging y producción. Cuando alguien cuela un override de módulo en env.hcl, acaba de fusionar dos responsabilidades que nunca deberían compartir archivo.
Hoja (terragrunt.hcl) conecta los outputs de dependencias a inputs tipados. Llama a un módulo una vez, pasa las variables correctas y declara de qué otras hojas depende. Si necesita el ID de la VPC, lo lee del output de la hoja VPC — no de una cadena hardcodeada, ni de una variable redeclarada en el archivo de marca.
Qué puede conocer cada capa
La restricción es estricta, y la estrictez es el punto: cada capa puede referenciar las capas que están por encima de ella, nunca las de abajo. Una hoja puede leer env.hcl y brand.hcl para descubrir sus inputs. Un archivo de marca no puede alcanzar a una hoja individual para sobreescribir su comportamiento. Un archivo de entorno no puede alcanzar a una hoja para hacer un caso especial en staging.
Esta dependencia unidireccional es lo que hace que la jerarquía sea componible en lugar de estar entrelazada. En el momento en que dejás que una capa superior alcance a una inferior, reinventaste el estado global mutable del que querías escapar — solo que ahora está escondido dentro de HCL.
El efecto práctico se acumula:
- Un entorno nuevo es un
env.hcleditado. - Una marca nueva es un
brand.hclcreado y un directorio copiado. - Un bump de versión de módulo ocurre en
brand.hcly se propaga a cada hoja de esa marca en el próximo plan de Atlantis. - Una región nueva para una marca existente es
cp -rde un directorio de entorno y el cambio de una sola línea.
Ninguna de estas operaciones debería requerir tocar código compartido. Si lo requiere, hay una capa que está filtrando.
Un ejemplo trabajado: agregar una marca nueva
Concretamente, así se ve "agregar una marca nueva" en una jerarquía bien diseñada. Lo hice suficientes veces como para que el diff me entre en la cabeza:
# live/brand-c/brand.hcl
locals {
brand = "brand-c"
module_versions = {
ecs_service = "v1.42.0"
rds_aurora = "v0.9.3"
waf_rules = "v2.1.0"
}
image_repos = {
api = "123456789012.dkr.ecr.us-east-1.amazonaws.com/brand-c-api"
worker = "123456789012.dkr.ecr.us-east-1.amazonaws.com/brand-c-worker"
}
}
# live/brand-c/staging/env.hcl
locals {
env = "staging"
api_min_size = 1
api_max_size = 3
rds_instance = "db.t4g.medium"
rds_storage_gb = 50
retention_days = 7
}
# live/brand-c/staging/api/terragrunt.hcl
include "root" { path = find_in_parent_folders("root.hcl") }
include "brand" { path = find_in_parent_folders("brand.hcl") }
include "env" { path = find_in_parent_folders("env.hcl") }
dependency "vpc" { config_path = "../vpc" }
terraform {
source = "${include.brand.locals.module_versions.ecs_service}"
}
inputs = {
service_name = "api"
image_uri = "${include.brand.locals.image_repos.api}:${include.env.locals.image_tag}"
min_size = include.env.locals.api_min_size
max_size = include.env.locals.api_max_size
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
Tres archivos chicos y la copia de un directorio. No se toca ningún módulo compartido. No se toca ninguna marca en producción. Ningún override de entorno se filtra a una hoja. El diff del PR es lo bastante pequeño como para que un revisor pueda retener el cambio entero en la cabeza, que es el único estilo de revisión que escala cuando tenés varias marcas en varias regiones.
El trade-off: el impuesto a la indirección
La contra honesta es que este layout impone un impuesto a la indirección sobre quien lee el código por primera vez. Un ingeniero junior que abra brand-c/staging/api/terragrunt.hcl por primera vez no va a ver los valores reales que entran al módulo. Va a ver include.brand.locals.module_versions.ecs_service y tener que perseguir el include hacia arriba del árbol. Comparado con un módulo Terraform monolítico que un recién llegado puede leer de arriba abajo en cinco minutos, es un costo real.
Pienso que vale la pena, porque el costo se paga una vez por onboarding y los ahorros se acumulan en cada cambio posterior. Pero no adoptaría este layout para un setup de una sola marca y un solo entorno. Dos marcas y dos entornos es más o menos donde el enfoque por capas empieza a dominar; por debajo de eso, la indirección es sobrecarga.
Contraargumento: por qué algunos equipos lo rechazan
Hay equipos que respeto que rechazan Terragrunt por capas de plano. Su argumento no es débil: cada capa de indirección es un lugar donde la magia puede esconderse, y "la magia está escondida" es una propiedad mala durante un incidente a las tres de la mañana. Prefieren Terraform totalmente expandido por entorno, generado por un script si hace falta, sin merging en tiempo de ejecución de Terragrunt. Los valores que leés en el archivo son los valores que van a la API.
Esa postura es defendible en dos casos. El primero son flotas chicas donde copy-paste sale genuinamente más barato que la indirección. El segundo son equipos donde el ingeniero de guardia no es el autor del IaC, y el costo de perseguir includes en debug supera el costo de mantener duplicación. Si alguna de las dos describe tu entorno, no lo adoptes.
Para el resto: la división en capas mueve la pregunta de "¿se acordaron todos de copiar la corrección?" a "¿la corrección aterrizó en la capa correcta?". La segunda pregunta tiene respuesta definitiva; la primera solo tiene una respuesta esperanzada.
Así que
Si mantenés Terragrunt para más de una marca o más de dos entornos, auditá tu repositorio este fin de semana contra la regla de conocimiento unidireccional. Buscá con grep cualquier referencia dentro de brand.hcl que apunte a una hoja específica. Buscá cualquier env.hcl que sobreescriba una fuente de módulo. Cada hit es una deriva futura que ya pagaste, solo que todavía no te llegó la factura.
Si estás empezando de cero, elegí la división en cuatro capas, declará la regla de conocimiento hacia arriba en el README del repo y rechazá los tres primeros PRs que la violen. Después del tercero, nadie la vuelve a violar.
[VERIFY: la propagación del plan de Atlantis a través del merge de include de Terragrunt ocurre en el siguiente ciclo de plan para todos los consumidores; confirmar contra la documentación de Terragrunt v0.55+ si se cita textualmente.]
Comentarios
Enviando…