Terraform Módulos: Reutiliza Infraestructura en 7 Pasos 2026

Terraform Módulos reutilizando infraestructura en varios entornos

Los Terraform Módulos son la diferencia entre copiar y pegar cuatrocientas líneas de HCL cada vez que levantas un entorno, o escribir seis líneas invocando algo que ya está probado. Si llevas tiempo con infraestructura como código y tus ficheros empiezan a repetirse, este es el paso que toca.

Y no es solo ahorro de tecleo: un módulo bien hecho convierte una decisión de arquitectura en algo que se revisa una vez y se reutiliza siempre igual.

Qué son los Terraform Módulos y qué problema resuelven

Un módulo de Terraform es, sencillamente, un directorio con ficheros .tf. Nada más. De hecho ya llevas usándolos desde el primer día sin saberlo: el directorio donde ejecutas terraform apply es el módulo raíz. La única diferencia es que un módulo reutilizable está pensado para ser invocado desde otro sitio, recibiendo valores por fuera.

El problema que resuelven es viejo y conocido. Tienes una VPC en desarrollo, otra en preproducción y otra en producción. Son iguales salvo los rangos de red y algún tamaño. Sin módulos acabas con tres ficheros casi idénticos que se van desincronizando: alguien arregla un fallo en producción y se olvida de los otros dos. Con Terraform Módulos hay una única definición y tres invocaciones con parámetros distintos.

Cuándo NO deberías escribir un módulo

Esto conviene decirlo pronto, porque es el error más caro. La documentación oficial es tajante: un buen módulo debe subir el nivel de abstracción describiendo un concepto nuevo de tu arquitectura, no envolver un recurso suelto.

Regla práctica: si no consigues ponerle al módulo un nombre distinto del recurso que contiene, no lo escribas. Un módulo llamado s3-bucket que solo crea un aws_s3_bucket no aporta nada; añade una capa de indirección y te complica la vida. Usa el recurso directamente.

En cambio, «red de tres capas con subredes públicas y privadas, tabla de rutas y NAT» sí es un concepto propio. Ese merece módulo.

Hay una segunda señal igual de útil, más humana que técnica: si al explicarle el módulo a un compañero tienes que enumerar los recursos que contiene, la abstracción no existe. Cuando funciona de verdad, la explicación es una sola frase —«esto te monta un servicio web en alta disponibilidad»— y quien lo usa no necesita saber qué hay dentro. Ese es el listón, y descarta la mayoría de los módulos que la gente escribe el primer mes.


Crear Terraform Módulos en 7 pasos

Vamos a construir un módulo real: un servicio web con su grupo de seguridad, sus instancias y su balanceador. Suficientemente compuesto para justificar la abstracción.

Paso 1: montar la estructura estándar

La convención de los Terraform Módulos está muy asentada y conviene respetarla, porque cualquiera que abra tu repositorio sabrá dónde mirar.

modules/
└── servicio-web/
    ├── main.tf        # los recursos
    ├── variables.tf   # entradas
    ├── outputs.tf     # salidas
    ├── versions.tf    # versiones requeridas
    └── README.md      # qué hace y cómo se usa

Los nombres no son obligatorios —Terraform lee todos los .tf del directorio— pero sepáralos igualmente. Un main.tf de mil líneas con variables mezcladas es exactamente lo que veníamos a evitar.

Paso 2: definir las variables de entrada

Las variables son el contrato de los Terraform Módulos. Tipa siempre, documenta siempre y pon valor por defecto solo cuando exista uno razonable.

variable "nombre" {
  description = "Nombre del servicio, usado como prefijo de los recursos"
  type        = string
}

variable "entorno" {
  description = "Entorno de despliegue"
  type        = string

  validation {
    condition     = contains(["dev", "pre", "pro"], var.entorno)
    error_message = "El entorno debe ser dev, pre o pro."
  }
}

variable "instancias" {
  description = "Número de instancias del servicio"
  type        = number
  default     = 2
}

variable "etiquetas" {
  description = "Etiquetas adicionales"
  type        = map(string)
  default     = {}
}

Fíjate en el bloque validation: falla en el plan, no a mitad del apply con recursos ya creados. Es de las cosas que más disgustos ahorran y casi nadie usa. Tienes la referencia completa en la documentación de variables de entrada.

Paso 3: escribir los recursos

Dentro de los Terraform Módulos, los recursos se escriben igual que siempre. La única disciplina nueva es que todo lo que varíe entre entornos debe venir de una variable, nunca estar escrito a fuego.

locals {
  etiquetas_comunes = merge(
    {
      Entorno   = var.entorno
      Servicio  = var.nombre
      Gestion   = "terraform"
    },
    var.etiquetas
  )
}

resource "aws_security_group" "este" {
  name        = "${var.nombre}-${var.entorno}-sg"
  description = "Grupo de seguridad de ${var.nombre}"
  vpc_id      = var.vpc_id
  tags        = local.etiquetas_comunes
}

resource "aws_instance" "este" {
  count                  = var.instancias
  ami                    = var.ami_id
  instance_type          = var.tipo_instancia
  subnet_id              = element(var.subnet_ids, count.index)
  vpc_security_group_ids = [aws_security_group.este.id]

  tags = merge(local.etiquetas_comunes, {
    Name = "${var.nombre}-${var.entorno}-${count.index + 1}"
  })
}

El uso de locals con merge para las etiquetas es un patrón que verás en todos los módulos serios: garantiza etiquetado consistente y deja al usuario añadir las suyas. Si trabajas con permisos, la misma lógica aplica a lo que vimos en roles y políticas de IAM como código.

Paso 4: exponer las salidas

Unos Terraform Módulos sin salidas son una caja negra inútil: quien lo invoca no puede conectar nada con lo que ha creado. Expón lo que otro módulo pueda necesitar, y nada más.

output "security_group_id" {
  description = "ID del grupo de seguridad creado"
  value       = aws_security_group.este.id
}

output "ips_privadas" {
  description = "IPs privadas de las instancias"
  value       = aws_instance.este[*].private_ip
}

output "endpoint" {
  description = "Punto de entrada del servicio"
  value       = aws_lb.este.dns_name
  sensitive   = false
}

Marca como sensitive = true cualquier salida con contraseñas o tokens: Terraform la ocultará en la consola. No la cifra en el estado, ojo — para eso está lo que vimos en la gestión de secretos con Vault.

Paso 5: invocarlo desde el módulo raíz

Aquí se ve el beneficio de los Terraform Módulos. Tres entornos, una sola definición, y las diferencias a la vista en diez líneas.

module "web_dev" {
  source = "./modules/servicio-web"

  nombre         = "tienda"
  entorno        = "dev"
  instancias     = 1
  tipo_instancia = "t3.micro"
  vpc_id         = module.red.vpc_id
  subnet_ids     = module.red.subnet_ids_privadas
}

module "web_pro" {
  source = "./modules/servicio-web"

  nombre         = "tienda"
  entorno        = "pro"
  instancias     = 6
  tipo_instancia = "m6i.large"
  vpc_id         = module.red.vpc_id
  subnet_ids     = module.red.subnet_ids_privadas

  etiquetas = {
    Criticidad = "alta"
  }
}

Tras añadir o cambiar un bloque module hay que ejecutar terraform init otra vez, porque Terraform necesita descargar o enlazar el código del módulo antes de poder planificar.

terraform init
terraform plan
terraform apply

Paso 6: versionar el módulo

Mientras tus Terraform Módulos vivan en ./modules/ del mismo repositorio, no hay versiones: usas lo que haya en esa carpeta. En cuanto lo compartas entre equipos, fijar versión pasa a ser obligatorio.

module "web_pro" {
  source = "git::https://github.com/miorg/tf-modulos.git//servicio-web?ref=v1.4.0"
  # ...
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
  # ...
}

Ese doble barra // indica un subdirectorio dentro del repositorio, y ?ref= fija la etiqueta de Git. Sin fijar versión, un cambio del módulo entra en producción sin que nadie lo apruebe. La documentación de orígenes de módulos recoge todos los formatos admitidos.

Paso 7: publicar o consumir del Registry

Antes de escribir Terraform Módulos desde cero, mira si ya existen. El Terraform Registry tiene módulos muy maduros y mantenidos por comunidades grandes; el módulo de VPC para AWS es el ejemplo canónico de cuánto trabajo te puedes ahorrar.

Para publicar el tuyo, el repositorio debe llamarse terraform-<proveedor>-<nombre>, ser público y usar etiquetas de versión semántica. A partir de ahí el Registry lo indexa solo.


Desplegar varias copias con for_each

Una vez tienes el módulo, invocarlo tres veces a mano se queda corto enseguida. Los bloques module admiten for_each, y ahí es donde los Terraform Módulos empiezan a rendir de verdad: defines los entornos como datos y el bucle hace el resto.

locals {
  entornos = {
    dev = { instancias = 1, tipo = "t3.micro" }
    pre = { instancias = 2, tipo = "t3.small" }
    pro = { instancias = 6, tipo = "m6i.large" }
  }
}

module "web" {
  source   = "./modules/servicio-web"
  for_each = local.entornos

  nombre         = "tienda"
  entorno        = each.key
  instancias     = each.value.instancias
  tipo_instancia = each.value.tipo
  vpc_id         = module.red.vpc_id
  subnet_ids     = module.red.subnet_ids_privadas
}

Ahora añadir un cuarto entorno es una línea en el mapa. Y como las claves son cadenas y no índices, dar de baja pre no reordena ni destruye los demás: es la diferencia práctica frente a count, y la razón de que for_each sea casi siempre la opción correcta cuando manejas Terraform Módulos parametrizados.

Para referenciar la salida de una instancia concreta usas la clave: module.web["pro"].endpoint. Conviene acostumbrarse pronto a esa sintaxis porque aparece en cuanto conectas módulos entre sí.


Buenas prácticas con Terraform Módulos

  • Árbol plano, no anidado. La recomendación oficial es componer módulos en horizontal en vez de anidarlos en profundidad. Un módulo que llama a otro que llama a otro se vuelve imposible de depurar y de reutilizar por partes.
  • No metas el provider dentro del módulo. Debe declararse en la raíz y heredarse. Si lo defines dentro, ese módulo no podrá usarse con varias regiones o cuentas.
  • Un README con ejemplo de uso. Lo agradecerás tú mismo dentro de seis meses.
  • Salidas mínimas. Cada salida es contrato público: cuantas más expongas, más difícil será cambiar el interior sin romper a nadie.
  • Prueba el módulo aislado. Un directorio examples/ con un caso mínimo que se pueda aplicar y destruir vale más que cualquier documentación.

Todo esto aplica igual si trabajas con OpenTofu en lugar de Terraform: la sintaxis de módulos es idéntica en ambos.

Problemas frecuentes con Terraform Módulos

SíntomaCausaSolución
Module not installedFalta init tras añadirloEjecutar terraform init
Cambios no se aplicanVersión fijada en cachéterraform init -upgrade
Recrea recursos al modularizarCambian las direcciones internasUsar bloques moved
provider configuration not presentProvider declarado dentroMoverlo al módulo raíz
Salida vacíacount a ceroRevisar índices y usar try()

El tercero merece un apunte, porque asusta mucho al modularizar: al mover recursos existentes dentro de un módulo, sus direcciones cambian y Terraform propone destruirlos y recrearlos. Los bloques moved le dicen dónde estaban antes, y el plan queda limpio sin tocar la infraestructura.

moved {
  from = aws_instance.web
  to   = module.web_pro.aws_instance.este[0]
}

Conclusión sobre Terraform Módulos

El error típico al descubrir los Terraform Módulos es modularizarlo todo el primer día. Acabas con veinte módulos de una línea y una configuración más difícil de leer que la original. La señal fiable es la repetición: cuando copies el mismo bloque por tercera vez, ahí tienes tu módulo.

Empieza por lo que ya tengas duplicado —la red suele ser el primer candidato— y crece desde ahí. Puedes ver el patrón en acción en los tutoriales de redes seguras con VPC y de clúster gestionado con EKS, dos casos donde modularizar se paga solo. La referencia oficial está en la guía de desarrollo de módulos y en la sintaxis del bloque module.

Preguntas frecuentes sobre Terraform Módulos

¿Cuántos recursos debe tener como mínimo?

No hay número mágico, pero si solo contiene uno probablemente no debería existir. La prueba es semántica: si puedes nombrarlo por el concepto de arquitectura que representa y no por el recurso que envuelve, va bien encaminado.

¿Puedo usar count o for_each al invocarlo?

Sí, ambos funcionan sobre el bloque module. Con for_each sobre un mapa consigues varias instancias del módulo con claves estables, que aguantan mucho mejor las altas y bajas que los índices numéricos de count.

¿Conviene un repositorio por módulo o uno con todos?

Un monorepo con subdirectorios es más cómodo mientras el equipo es pequeño, y con //subdirectorio?ref= puedes versionar igual. Separar por repositorio tiene sentido cuando cada módulo tiene ciclo de vida y responsables propios.

¿Los módulos guardan estado propio?

No. Todo el estado vive en el del módulo raíz que los invoca, con las direcciones prefijadas por module.nombre. Por eso al modularizar cambian las direcciones y hacen falta los bloques moved.

¿Cómo pruebo un módulo antes de usarlo en producción?

Con un directorio examples/ que lo invoque con valores mínimos y que puedas aplicar y destruir en una cuenta de pruebas. Terraform incluye además un marco de pruebas con ficheros .tftest.hcl para validar salidas y condiciones sin desplegar de verdad.

Avatar

Por Mid

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x