Terraform para laboratorios reproducibles
Artículo 3 de 7
43%
completado
Módulos Terraform: composición, contratos y límites que sí explican
Diseño de módulos Terraform/OpenTofu como contratos: inputs, outputs, providers, invariantes, compatibilidad y composición sin cajas negras.
Prerequisitos recomendados
Si alguno de estos conceptos todavía está medio nebuloso, empieza por aquí. Te ahorra tropiezos y un par de cejas fruncidas.
Un módulo no es una carpeta elegante. Es una frontera: recibe valores, declara recursos, expone resultados y esconde decisiones que el consumidor no debería repetir. La documentación oficial define los módulos como contenedores de recursos que se usan juntos, pero la calidad de esa frontera depende de lo que dejamos visible. [1]
La pregunta anterior a module {}
Antes de extraer un módulo, pregunta qué cambio quieres aislar:
- ¿el consumidor necesita elegir el nombre exacto o solo un prefijo?;
- ¿debe controlar cada regla de red o seleccionar una política conocida?;
- ¿el output expone una identidad estable o filtra detalles internos?;
- ¿qué modificación sería compatible y cuál rompería a los callers?
Un módulo con cincuenta variables booleanas no es flexible: suele ser una copia del provider disfrazada de API. En el otro extremo, un módulo sin outputs obliga a que otras capas conozcan sus recursos internos. El contrato sano concentra intención, no sintaxis.
¿Extraer, componer o mantener plano?
Root module y child module no tienen la misma responsabilidad
El root module conoce el entorno: credenciales, región, backend, aliases y la combinación concreta de capacidades. El child module debería declarar qué providers requiere, pero recibir la configuración desde arriba. OpenTofu documenta que los provider configurations son globales para una configuración y solo deben definirse en el root; cada child module declara sus requisitos para que el runtime resuelva una versión compatible. [2]
module "network" {
source = "./modules/network"
providers = {
aws = aws.lab
}
name = "trautslab"
cidr = "10.42.0.0/16"
allowed_services = ["https"]
}Ese bloque cuenta una historia legible: el root elige la identidad de AWS y el módulo declara una red con una política explícita. No transmite access keys, no inventa otro backend y no obliga a conocer direcciones internas.
Inputs: intención, tipo y validación
Un input útil combina nombre, tipo, descripción, default solo cuando es seguro y validación cuando el dominio es acotado.
variable "environment" {
type = string
description = "Entorno lógico usado en nombres y tags."
validation {
condition = contains(["lab", "staging"], var.environment)
error_message = "Este módulo no admite producción."
}
}La validación no reemplaza políticas externas, pero evita estados imposibles antes de llegar al provider. Una variable environment = string sin dominio documentado solo mueve la ambigüedad de archivo.
Outputs: evidencia estable, no inventario completo
Expón outputs que permitan componer y verificar: IDs, endpoints, nombres o señales de estado que otros módulos realmente consumen. No expongas cada atributo “por si acaso”. Cada output público se vuelve parte de la compatibilidad del módulo.
output "private_subnet_ids" {
description = "Subredes privadas ordenadas por zona."
value = values(aws_subnet.private)[*].id
}Una migración interna de count a for_each puede cambiar direcciones de recursos sin cambiar el output. Esa es la ganancia: el consumidor depende de la capacidad, mientras el módulo conserva libertad para evolucionar.
Versionar no arregla una mala interfaz
Publicar v1.0.0 congela expectativas; no vuelve clara la semántica. Antes de etiquetar un módulo, registra:
- inputs obligatorios y límites;
- outputs y orden cuando aplique;
- providers y versiones mínimas;
- recursos que puede reemplazar;
- estrategia de upgrade;
- ejemplo mínimo ejecutable;
- prueba que demuestre el invariante principal.
El fallo común es usar un módulo remoto sin fijar versión. El siguiente init puede resolver una revisión distinta y convertir un cambio inocente en un plan de reemplazo. Los providers tienen lockfile; los módulos remotos necesitan una referencia de versión explícita en su source.
Composición y blast radius
No construyas un módulo “plataforma” que incluya red, base de datos, observabilidad, IAM y aplicaciones solo porque se despliegan juntos hoy. Pregunta qué partes cambian juntas y qué partes deben poder recuperarse por separado. Un módulo demasiado ancho amplía el plan, el state y el blast radius de cada modificación.
Diseño de contrato
De patrón repetido a módulo probado
Compatibilidad semántica del módulo
Un cambio compatible conserva la intención observable para los callers: acepta los inputs anteriores, mantiene outputs prometidos y no introduce reemplazos sorpresivos. Renombrar un recurso interno puede ser invisible si se acompaña con moved; cambiar el tipo de un output o convertir una opción en obligatoria sí altera el contrato. La versión debe describir ese impacto, no el tamaño del diff.
Prueba el módulo desde más de un root representativo. Un fixture mínimo descubre defaults rotos; otro cercano a producción revela composición, aliases de provider y combinaciones de variables. Inspecciona el plan para distinguir actualizaciones in-place de reemplazos. Un test que solo verifica validate demuestra sintaxis, no compatibilidad operacional.
Evita outputs que filtren estructuras enteras del provider. Parecen cómodos, pero convierten detalles internos en API pública y hacen que cualquier refactor tenga consumidores desconocidos. Publica capacidades estables: un identificador, endpoint o conjunto pequeño de atributos que otro módulo realmente necesita. La buena composición reduce el conocimiento compartido; no lo disfraza con un nombre corto.
Documenta deprecaciones y ofrece una migración antes de retirar cada entrada u output.
Contrato de lab
Extrae una red local simulada en un módulo y crea dos root modules que la consuman con nombres distintos. La aceptación exige:
fmt,validatey plan limpio;- provider configurado solo en cada root;
- input inválido rechazado antes del apply;
- outputs suficientes para un smoke check;
- segunda ejecución sin diff;
- cambio interno que no modifique la interfaz pública.
Conexiones dentro de la serie
- Antes: State, backends y locking.
- Después: Providers, restricciones y lockfile.
- Lab relacionado: Terraform lab network.
Bibliografía académica
- [1]
HashiCorp, "Modules overview," Terraform Language Documentation.
Módulos raíz, módulos hijos y composición.
- [2]
OpenTofu, "Providers Within Modules."
Providers compartidos, requisitos y paso explícito a child modules.
Rutas vecinas
Si este tema te abrió hambre, sigue por aquí.
Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.
Labs para convertir lectura en práctica
Sigue leyendo
Ideas conectadas con este artículo
Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.
Terraform y OpenTofu para laboratorios reproducibles
Base práctica para Terraform y OpenTofu en labs cloud: providers, state, plan, módulos, costos y teardown seguro.
Terraform state: backends, locking y recuperación sin superstición
Cómo razonar sobre state, backends y locking en Terraform/OpenTofu: invariantes, múltiples escritores, fallos parciales y recuperación verificable.
Testing IaC: del HCL válido al plan que una política puede aprobar
Capas de prueba para Terraform/OpenTofu: formato, validación, tests, planes guardados, políticas y evidencia sin crear infraestructura por accidente.
Terraform en CI/CD: planes revisables y credenciales que caducan
Arquitectura segura de CI/CD para Terraform/OpenTofu: plan de PR, apply protegido, OIDC, credenciales efímeras, concurrencia y artefactos sensibles.