Módulos de Terraform que escalan con su equipo, no contra él
Un módulo de Terraform es la forma en que un equipo deja de copiar y pegar infraestructura. Pero un módulo que no se versiona, revisa ni prueba no escala al equipo: escala el radio del daño. Un cambio descuidado en un módulo 'vpc' compartido y cada entorno que lo referencia hereda el fallo en el siguiente apply. La solución es dejar de tratar los módulos como carpetas compartidas y empezar a tratarlos como productos con versiones.
Esta es la versión práctica: qué hace fiable a un módulo, cómo publicarlo a través de un pipeline de verdad, y cómo los equipos lo consumen —en local y en CI— directamente desde GitHub, fijado a un tag o, cuando la seguridad lo exige, a un commit hash inmutable. Sin necesidad de un registry privado.
Qué hace fiable a un módulo
La fiabilidad empieza antes de cualquier pipeline. Un módulo en el que otros equipos confían es pequeño y hace una sola cosa —una red, una base de datos, un servicio— no una 'plataforma' que lo hace todo. Expone un conjunto claro y documentado de inputs y outputs, trae defaults sensatos, y fija sus propias versiones de provider para comportarse igual hoy que dentro de seis meses.
- Responsabilidad única — un módulo, una preocupación; se componen, en lugar de anidar un monolito.
- Interfaz explícita — variables tipadas con descripciones y validación, y los outputs que otros módulos realmente necesitan.
- Providers fijados — required_providers con restricciones de versión, para que una nueva versión del provider no cambie el comportamiento sin avisar.
- Ejemplos y documentación — una carpeta examples/ que es también aquello contra lo que corren los tests.
- Sin sorpresas — sin nombres, regiones ni IDs de cuenta escritos a fuego; nada que vaya a buscar cosas fuera de sus inputs.
Nada de esto sobrevive a un equipo que crece a menos que dos cosas sean innegociables: cada cambio se revisa y cada cambio se prueba contra infraestructura real y desechable. Un módulo es el único trozo de código cuyos fallos se multiplican por cada consumidor: merece el listón más alto que tengas.
La versión es el contrato
A medida que la organización crece y aparecen nuevos entornos —un sandbox aquí, una segunda región allá— la versión deja de ser un lujo y se convierte en el contrato entre el módulo y todo el que lo usa. Cada consumidor fija una versión explícita; nadie sigue a main. El versionado semántico le dice entonces al consumidor qué significa un salto: un patch es seguro, un minor añade, un major le obliga a cambiar código.
Fijar es también cómo avanzas y retrocedes con confianza. Promover a producción es un salto de versión; una mala release es un salto de vuelta a la anterior. Sin versiones, 'hacer rollback del módulo' significa de verdad 'ir a averiguar en qué commit estaba el martes pasado'.
Consúmelo desde GitHub — sin registry
No necesitas un registry privado ni Artifactory para compartir módulos como es debido. Referencia el módulo directamente por su source de Git y un ref. La misma source funciona en un portátil y en CI; la autenticación es solo un token de GitHub —un PAT en local, el token del runner en CI— enlazado una vez para que los clones por HTTPS lleven la credencial.
# main.tf — fijar a un tag publicado
module "vpc" {
source = "git::https://github.com/acme/tf-modules.git//modules/vpc?ref=v1.4.0"
cidr_block = "10.20.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
}La credencial se define una vez con el insteadOf de git, así que ni tu código ni tu state contienen un secreto: Terraform se limita a clonar por HTTPS en tu nombre:
# local + CI: dejar que git use un token para github.com por HTTPS
git config --global url."https://oauth2:${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"Inmutabilidad: fija un commit hash cuando tiene que quedar bloqueado
Un tag es cómodo, pero es móvil: cualquiera con permisos de escritura puede reapuntar o forzar v1.4.0 a otro código, y tu siguiente apply lo trae. Para la mayoría de los módulos ese compromiso vale. Para los que no pueden cambiar bajo tus pies —todo lo sensible en seguridad o cumplimiento— fija mejor el commit hash completo. Un commit no se puede intercambiar: ref=<sha> resuelve siempre exactamente a los bytes que revisaste.
# bloqueado a un commit exacto e imposible de intercambiar
module "kms" {
source = "git::https://github.com/acme/tf-modules.git//modules/kms?ref=3f9a1c4e8b7d6c5a4f3e2d1c0b9a8f7e6d5c4b3a"
}Publica el módulo como un producto
Si los consumidores fijan versiones, algo tiene que producir esas versiones de forma fiable: un pipeline en el repositorio del módulo. Y vale la pena ser preciso: un módulo no 'se despliega'. Se valida, se prueba y se publica. El despliegue es trabajo del consumidor.
- En cada pull request, corre las comprobaciones estáticas: terraform fmt -check, terraform validate y tflint.
- Luego prueba de verdad — levanta el ejemplo del módulo en una cuenta sandbox desechable, valídalo (terraform test, o Terratest para comprobaciones más ricas) y destrúyelo. Un módulo que nunca se ha aplicado es un módulo sin probar.
- En el merge a main, deja que GitVersion calcule la siguiente versión semántica a partir del historial de commits, cree el tag de Git correspondiente y publique — anuncia la release en el canal de Slack o Teams del equipo (y espéjala a un registry, si por casualidad tienes uno).
# .github/workflows/release.yml (esquema)
on: { pull_request: {}, push: { branches: [main] } }
jobs:
check:
steps:
- run: terraform fmt -check -recursive
- run: terraform validate
- run: tflint
- run: terraform test # aplica los ejemplos en un sandbox y destruye
release:
if: github.ref == 'refs/heads/main'
needs: check
steps:
- uses: gittools/actions/gitversion/execute@v3 # -> siguiente semver
- run: gh release create "v${VERSION}"
- run: ./notify.sh "tf-modules ${VERSION} publicado" # Slack / TeamsLa herramienta exacta da igual —GitVersion, release-please y semantic-release sirven todas. Lo que importa es que una versión solo pasa a existir después de que el cambio se haya revisado, validado y de verdad aplicado.
Ahora úsalo: de Terraform a Terragrunt
Con un módulo probado y con tag, un proyecto de Terraform sencillo se limita a fijarlo: el bloque module de arriba es toda la historia. Basta para uno o dos entornos. Pero en cuanto corres la misma stack en dev, staging y producción, en una o varias regiones, copiar y pegar ese enlace por entorno es exactamente la duplicación que los módulos venían a matar.
Terragrunt mantiene los entornos DRY
Terragrunt envuelve a Terraform para quitar esa repetición: genera la configuración de backend y de provider, mantiene la versión del módulo en un solo sitio por stack, y deja que cada entorno sea un fichero fino que dice 'este módulo, esta versión, estos inputs'. Promover por la cadena pasa a ser un salto de versión de una línea.
# live/prod/eu-west-1/vpc/terragrunt.hcl
terraform {
source = "git::https://github.com/acme/tf-modules.git//modules/vpc?ref=v1.4.0"
}
include "root" { path = find_in_parent_folders() }
inputs = {
cidr_block = "10.30.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}Una estructura que escala: proyecto / entorno / región
Organiza el repositorio live para que la ruta sea la dirección de la infraestructura. Una forma deliberadamente aburrida es entorno → región → componente — adapta los niveles a cómo piensa de verdad tu organización (hay quien separa primero por proyecto, cuenta o equipo):
live/
├── root.hcl # state remoto, provider, tags comunes
├── dev/
│ └── eu-west-1/
│ ├── vpc/terragrunt.hcl
│ └── eks/terragrunt.hcl
├── staging/
│ └── eu-west-1/ ...
└── prod/
├── eu-west-1/
│ ├── vpc/terragrunt.hcl
│ └── eks/terragrunt.hcl
└── eu-central-1/ ...Cada hoja fija una versión del módulo, y la carpeta te dice exactamente qué corre dónde. Actualizar producción pasa a ser un PR revisado que cambia un ref= de v1.4.0 a v1.5.0: pequeño, obvio y trivial de revertir.
Módulos fiables y de propósito único; revisados y probados en cada cambio; versionados y publicados por un pipeline; consumidos desde GitHub por tag —o por commit hash inmutable cuando importa— y montados con una estructura de Terragrunt que refleja tu parque. Esa es la diferencia entre módulos que escalan con tu equipo y módulos que, en silencio, escalan contra él.