Terraform para laboratorios reproducibles

Artículo 3 de 7

43%

completado

Cloud e infraestructuraEngineering guide6 min de lectura

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.

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.

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.

Matriz de Decisión

¿Extraer, componer o mantener plano?

Reglas de Decisión Rápida
¿El patrón ya apareció en dos contextos?Mismos invariantes; nombres y tamaños distintos.
Extrae un módulo pequeño
¿Solo se repiten tres líneas de HCL?No existe una decisión arquitectónica común.
Mantén la configuración plana
¿Cada caller necesita excepciones distintas?La interfaz crece con flags y mapas sin límite.
Rediseña la frontera
¿El módulo configura su propio provider?Impide composición limpia con aliases, count o for_each.
Mueve el provider al root
La reutilización se gana con una frontera estable, no con una carpeta adicional.

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.

hcl
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.

hcl
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.

hcl
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:

  1. inputs obligatorios y límites;
  2. outputs y orden cuando aplique;
  3. providers y versiones mínimas;
  4. recursos que puede reemplazar;
  5. estrategia de upgrade;
  6. ejemplo mínimo ejecutable;
  7. 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

  1. Observar repeticiónmismo invariante en dos callers
  2. Nombrar intenciónqué capacidad entrega
  3. Reducir interfazinputs y outputs necesarios
  4. Probar contratoplan, assertions y reemplazos
  5. Versionarcompatibilidad y upgrade note

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, validate y 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

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    HashiCorp, "Modules overview," Terraform Language Documentation.

    official docsID: TF-MODULES🔗 Abrir fuente

    Módulos raíz, módulos hijos y composición.

  2. [2]

    OpenTofu, "Providers Within Modules."

    official docsID: TOFU-PROVIDERS-MODULES🔗 Abrir fuente

    Providers compartidos, requisitos y paso explícito a child modules.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

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.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.