¿Qué es un módulo en Terraform?

Un módulo en Terraform es simplemente un conjunto de archivos de configuración .tf agrupados en un mismo directorio. Todo lo que has escrito hasta ahora ya es un módulo: el módulo raíz (root module). Cuando hablamos de módulos en el sentido más útil, nos referimos a bloques de configuración reutilizables que puedes invocar desde otros lugares de tu código.

Piensa en un módulo como una función en programación: encapsula lógica, acepta parámetros de entrada y devuelve resultados. En vez de copiar y pegar bloques de recursos cada vez que necesitas crear, por ejemplo, una VPC en AWS, defines un módulo vpc y lo llamas con distintos parámetros.

# Sin módulos: código repetido en cada entorno
resource "aws_vpc" "dev" {
  cidr_block = "10.0.0.0/16"
  tags = { Name = "dev-vpc", Environment = "dev" }
}

resource "aws_vpc" "staging" {
  cidr_block = "10.1.0.0/16"
  tags = { Name = "staging-vpc", Environment = "staging" }
}

# Con módulos: lógica centralizada, llamadas limpias
module "dev_vpc" {
  source      = "./modules/vpc"
  cidr_block  = "10.0.0.0/16"
  environment = "dev"
}

module "staging_vpc" {
  source      = "./modules/vpc"
  cidr_block  = "10.1.0.0/16"
  environment = "staging"
}

La diferencia es enorme en proyectos reales con decenas de recursos.


Por qué usar módulos: reutilización, organización y abstracción

Los tres pilares que justifican el uso de módulos son:

  1. Reutilización

Escribe la lógica una vez y úsala en múltiples lugares. Si hay un error en cómo creas tus buckets S3, lo corriges en un único módulo y el cambio se propaga a todos los entornos que lo usan.

  1. Organización

En proyectos grandes, tener toda la infraestructura en un único main.tf de miles de líneas es inmanejable. Los módulos permiten dividir la infraestructura en unidades lógicas:

  • modules/networking — VPCs, subredes, gateways
  • modules/compute — instancias, grupos de autoescalado
  • modules/database — RDS, parámetros, backups
  • modules/security — grupos de seguridad, roles IAM

  1. Abstracción

Los módulos pueden ocultar complejidad. Un usuario del módulo solo necesita conocer sus variables de entrada y sus outputs. No necesita entender cómo se configura internamente cada recurso. Esto permite que equipos distintos trabajen de forma independiente: el equipo de plataforma publica módulos, y los equipos de aplicación los consumen.


El módulo raíz vs módulos secundarios

Concepto Módulo raíz (root) Módulo secundario (child)
Ubicación Directorio donde ejecutas terraform Cualquier subdirectorio referenciado
Invocación Ejecutado directamente por Terraform Llamado con bloque module {}
Estado Gestiona su propio estado Su estado se integra en el módulo padre
Variables Se pasan con -var o terraform.tfvars Se pasan como argumentos en el bloque module
Outputs Disponibles en la CLI con terraform output Accesibles vía module.<nombre>.<output>

El módulo raíz es el punto de entrada. Desde él puedes llamar a módulos secundarios, que a su vez pueden llamar a otros módulos. Esto crea una jerarquía de módulos.

root/
├── main.tf          ← módulo raíz
├── variables.tf
├── outputs.tf
└── modules/
    ├── networking/  ← módulo secundario
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── compute/     ← módulo secundario
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Estructura de directorios de un módulo

Un módulo bien estructurado sigue una convención estándar:

mi-modulo/
├── main.tf          # Recursos principales del módulo
├── variables.tf     # Declaración de variables de entrada
├── outputs.tf       # Declaración de outputs
├── versions.tf      # Restricciones de versión de Terraform y providers
├── README.md        # Documentación del módulo
└── examples/        # (Opcional) Ejemplos de uso
    └── basic/
        ├── main.tf
        └── README.md

Descripción de cada archivo:

  • main.tf: Contiene los recursos que el módulo gestiona. Es el corazón del módulo.
  • variables.tf: Define todas las variables que el módulo acepta como entrada. Incluye descripción, tipo y valores por defecto.
  • outputs.tf: Define los valores que el módulo expone hacia el exterior. Son como el valor de retorno de una función.
  • versions.tf: Especifica qué versiones de Terraform y de los providers son compatibles con el módulo.
  • README.md: Documentación para los usuarios del módulo: qué hace, cómo usarlo, variables disponibles.

Tipos de módulos: locales, del registro y de Git

Terraform puede cargar módulos desde distintas fuentes:

Módulos locales

Viven en el mismo repositorio que el módulo raíz. Se referencian con rutas relativas:

module "mi_red" {
  source = "./modules/networking"
}

module "otro" {
  source = "../shared/modules/storage"
}

Son ideales para módulos específicos de tu proyecto o empresa.

Módulos del Terraform Registry

El Terraform Registry es el repositorio público oficial. Contiene módulos mantenidos por HashiCorp y por la comunidad:

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

  name = "mi-vpc"
  cidr = "10.0.0.0/16"
}

Módulos de Git

Puedes referenciar módulos directamente desde repositorios Git:

# Desde GitHub
module "servidor" {
  source = "github.com/mi-empresa/terraform-modules//servidor"
}

# Desde GitLab con tag específico
module "base_datos" {
  source = "git::https://gitlab.com/mi-empresa/tf-modules.git//database?ref=v2.1.0"
}

# Desde repositorio privado con SSH
module "seguridad" {
  source = "git::ssh://[email protected]/mi-empresa/terraform-privado.git//seguridad?ref=main"
}

Módulos desde S3 y otros

# Desde un bucket S3
module "infraestructura" {
  source = "s3::https://s3-eu-west-1.amazonaws.com/mi-bucket/modulos/servidor.zip"
}

Tabla comparativa de fuentes de módulos

Fuente Sintaxis de source Versionado Acceso privado Caso de uso típico
Local ./modules/nombre No (usa git del repo) Si (mismo repo) Módulos específicos del proyecto
Terraform Registry namespace/nombre/provider Si (version = "~> X.Y") Requiere Terraform Cloud Módulos públicos bien mantenidos
GitHub (público) github.com/org/repo//subdir Via ?ref=tag No Módulos open source no publicados
GitHub (privado) git::ssh://[email protected]/... Via ?ref=tag Si (con SSH key) Módulos internos de empresa
GitLab git::https://gitlab.com/... Via ?ref=tag Si (con token) Módulos en GitLab corporativo
S3 / GCS s3::https://... Via nombre de objeto Si (con IAM/SA) Módulos en almacenamiento corporativo
Terraform Cloud app.terraform.io/org/nombre Si Si Módulos privados empresariales

Ventajas y consideraciones al diseñar módulos

Ventajas

  • Consistencia: Todos los equipos crean recursos con la misma configuración base.
  • Velocidad de desarrollo: Reutilizar un módulo existente es mucho más rápido que escribir desde cero.
  • Reducción de errores: Menos código duplicado significa menos lugares donde pueden existir bugs.
  • Facilidad de prueba: Puedes testear un módulo de forma aislada.
  • Abstracción de complejidad: El usuario del módulo no necesita conocer todos los detalles de implementación.

Consideraciones

  • Sobre-modularización: No todo necesita ser un módulo. Crear módulos muy pequeños o muy específicos añade complejidad sin beneficio.
  • Compatibilidad de versiones: Cambiar la interfaz de un módulo (sus variables u outputs) puede romper a todos los que lo usan.
  • Gestión de estado: Los recursos de los módulos se incluyen en el estado del módulo padre. Esto puede complicar terraform state operations.
  • Debugging más complejo: Cuando algo falla, el error puede originarse en un módulo anidado varios niveles más abajo.

Cuándo extraer código en un módulo: la regla de los 3

Una heurística muy útil es la regla de los 3: si vas a reutilizar una misma pieza de código en 3 o más lugares, es momento de convertirla en un módulo.

Antes de crear un módulo, pregúntate:

  1. ¿Este conjunto de recursos se repetirá en múltiples contextos?
  2. ¿Tiene sentido como unidad lógica independiente?
  3. ¿Tiene una interfaz clara (inputs y outputs bien definidos)?
  4. ¿El usuario del módulo necesita conocer sus detalles internos?

Si la respuesta a 1-3 es "sí" y a 4 es "no", probablemente deberías crear un módulo.

Señales de que deberías crear un módulo:

  • Estás copiando y pegando bloques de recursos entre proyectos
  • Tienes más de ~20 recursos en un único main.tf
  • Varios equipos necesitan crear el mismo tipo de infraestructura
  • Quieres imponer estándares de configuración (etiquetas, cifrado, etc.)

Señales de que NO debes crear un módulo todavía:

  • Solo lo usarás en un lugar
  • La configuración cambia constantemente y todavía no está estabilizada
  • Los recursos son simples y ya son suficientemente legibles tal como están

Ejemplo básico de un módulo simple

Vamos a crear un módulo muy sencillo que gestiona la creación de archivos locales (sin necesidad de credenciales de cloud). Esto nos permitirá entender la mecánica sin distracciones.

Estructura:

proyecto-ejemplo/
├── main.tf              ← módulo raíz: aquí usamos el módulo
└── modules/
    └── archivo-config/  ← módulo secundario
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

modules/archivo-config/variables.tf:

# Declaramos las variables que el módulo acepta como entrada
# Cada variable tiene nombre, tipo, descripción y opcionalmente un valor por defecto

variable "nombre_archivo" {
  description = "Nombre del archivo de configuración a crear"
  type        = string
  # No hay default: esta variable es obligatoria
}

variable "contenido" {
  description = "Contenido del archivo de configuración"
  type        = string
  default     = "# Archivo de configuración generado por Terraform"
}

variable "directorio" {
  description = "Directorio donde se creará el archivo"
  type        = string
  default     = "/tmp"
}

modules/archivo-config/main.tf:

# Este es el recurso principal del módulo
# Usamos las variables de entrada (var.xxx) para hacer el módulo parametrizable

resource "local_file" "config" {
  # La función "${}" permite interpolar variables dentro de strings
  filename = "${var.directorio}/${var.nombre_archivo}"

  # El contenido del archivo viene de la variable de entrada
  content  = var.contenido
}

modules/archivo-config/outputs.tf:

# Los outputs son los valores que el módulo "devuelve" al módulo padre
# Permiten que el código que usa este módulo acceda a información del recurso creado

output "ruta_completa" {
  description = "Ruta completa del archivo creado"
  # Referenciamos el recurso creado en main.tf: resource_type.resource_name.attribute
  value       = local_file.config.filename
}

output "contenido_guardado" {
  description = "Contenido efectivamente guardado en el archivo"
  value       = local_file.config.content
}

main.tf (módulo raíz):

# Necesitamos el provider local para crear archivos
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}

# Aquí llamamos al módulo que hemos creado
# "mi_configuracion" es el nombre local que le damos a esta instancia del módulo
module "mi_configuracion" {
  # source indica dónde está el módulo (ruta relativa al directorio actual)
  source = "./modules/archivo-config"

  # Pasamos valores para las variables del módulo
  nombre_archivo = "app.conf"
  contenido      = "servidor=localhost\npuerto=8080\ndebug=false"
  directorio     = "/tmp/mi-app"
}

# Podemos usar el output del módulo en el módulo raíz
output "config_creada_en" {
  value = module.mi_configuracion.ruta_completa
}

Ejercicio: Identificar qué partes de una configuración deberían ser un módulo

Observa la siguiente configuración y determina qué partes deberían convertirse en módulos:

# Configuración actual sin módulos
# Entorno de DESARROLLO
resource "local_file" "dev_config_app" {
  filename = "/tmp/dev/app.conf"
  content  = "env=development\ndb_host=localhost\nport=3000"
}

resource "local_file" "dev_config_cache" {
  filename = "/tmp/dev/cache.conf"
  content  = "redis_host=localhost\nredis_port=6379\nttl=300"
}

resource "local_file" "dev_readme" {
  filename = "/tmp/dev/README.txt"
  content  = "Entorno de desarrollo - No usar en producción"
}

# Entorno de STAGING
resource "local_file" "staging_config_app" {
  filename = "/tmp/staging/app.conf"
  content  = "env=staging\ndb_host=staging-db.internal\nport=3000"
}

resource "local_file" "staging_config_cache" {
  filename = "/tmp/staging/cache.conf"
  content  = "redis_host=staging-redis.internal\nredis_port=6379\nttl=600"
}

resource "local_file" "staging_readme" {
  filename = "/tmp/staging/README.txt"
  content  = "Entorno de staging - Solo para pruebas de integración"
}

# Entorno de PRODUCCIÓN
resource "local_file" "prod_config_app" {
  filename = "/tmp/prod/app.conf"
  content  = "env=production\ndb_host=prod-db.internal\nport=3000"
}

resource "local_file" "prod_config_cache" {
  filename = "/tmp/prod/cache.conf"
  content  = "redis_host=prod-redis.internal\nredis_port=6379\nttl=3600"
}

resource "local_file" "prod_readme" {
  filename = "/tmp/prod/README.txt"
  content  = "Entorno de PRODUCCIÓN - Tratar con extremo cuidado"
}

Preguntas:

  1. ¿Qué patrón se repite en esta configuración?
  2. ¿Cuántas veces se repite?
  3. ¿Qué variables necesitaría el módulo?
  4. ¿Qué outputs debería exponer?

Solución y razonamiento

Análisis:

El patrón que se repite es la creación del conjunto de 3 archivos de configuración para un entorno (app.conf, cache.conf, README.txt). Se repite exactamente 3 veces (desarrollo, staging, producción), lo que activa la "regla de los 3".

Variables necesarias:

  • entorno — nombre del entorno (development, staging, production)
  • directorio_base — directorio base donde se crean los archivos
  • db_host — host de la base de datos (cambia por entorno)
  • redis_host — host de Redis (cambia por entorno)
  • redis_ttl — TTL de caché (varía según entorno)
  • descripcion_readme — texto descriptivo del entorno

Módulo resultante:

# modules/entorno-app/variables.tf
variable "entorno" {
  description = "Nombre del entorno (development, staging, production)"
  type        = string
}

variable "directorio_base" {
  description = "Directorio base para los archivos de configuración"
  type        = string
}

variable "db_host" {
  description = "Hostname del servidor de base de datos"
  type        = string
}

variable "redis_host" {
  description = "Hostname del servidor Redis"
  type        = string
}

variable "redis_ttl" {
  description = "TTL en segundos para las entradas de caché"
  type        = number
  default     = 300
}

variable "descripcion_readme" {
  description = "Descripción del entorno para el README"
  type        = string
}
# modules/entorno-app/main.tf
resource "local_file" "app_config" {
  filename = "${var.directorio_base}/app.conf"
  content  = "env=${var.entorno}\ndb_host=${var.db_host}\nport=3000"
}

resource "local_file" "cache_config" {
  filename = "${var.directorio_base}/cache.conf"
  content  = "redis_host=${var.redis_host}\nredis_port=6379\nttl=${var.redis_ttl}"
}

resource "local_file" "readme" {
  filename = "${var.directorio_base}/README.txt"
  content  = var.descripcion_readme
}
# Uso del módulo desde el módulo raíz
module "entorno_dev" {
  source          = "./modules/entorno-app"
  entorno         = "development"
  directorio_base = "/tmp/dev"
  db_host         = "localhost"
  redis_host      = "localhost"
  redis_ttl       = 300
  descripcion_readme = "Entorno de desarrollo - No usar en producción"
}

module "entorno_staging" {
  source          = "./modules/entorno-app"
  entorno         = "staging"
  directorio_base = "/tmp/staging"
  db_host         = "staging-db.internal"
  redis_host      = "staging-redis.internal"
  redis_ttl       = 600
  descripcion_readme = "Entorno de staging - Solo para pruebas de integración"
}

module "entorno_prod" {
  source          = "./modules/entorno-app"
  entorno         = "production"
  directorio_base = "/tmp/prod"
  db_host         = "prod-db.internal"
  redis_host      = "prod-redis.internal"
  redis_ttl       = 3600
  descripcion_readme = "Entorno de PRODUCCIÓN - Tratar con extremo cuidado"
}

El código pasó de 27 recursos individuales a 3 llamadas de módulo. Si el formato de app.conf cambia, solo hay que modificarlo en un lugar.


Errores comunes al empezar con módulos

Error 1: Olvidar ejecutar terraform init tras añadir un módulo

Cuando añades un bloque module {} con un nuevo source, Terraform necesita descargar o localizar ese módulo. Si intentas hacer terraform plan sin antes ejecutar terraform init, obtendrás:

Error: Module not installed

  on main.tf line 5:
   5: module "mi_modulo" {

This module is not yet installed. Run "terraform init" to install all modules
required by this configuration.

Solución: Siempre ejecuta terraform init después de añadir o cambiar módulos.

Error 2: Confundir la ruta source con módulos locales

Las rutas locales deben empezar con ./ o ../. Sin ese prefijo, Terraform intenta buscar el módulo en el registry:

# INCORRECTO: Terraform buscará "modules/vpc" en el registry
module "red" {
  source = "modules/vpc"
}

# CORRECTO: ruta relativa local
module "red" {
  source = "./modules/vpc"
}

Error 3: No definir outputs en el módulo hijo

Si creas recursos en un módulo pero no defines outputs, el módulo raíz no puede acceder a ningún atributo de esos recursos:

# Sin este output en el módulo hijo, no podrás usar el ID del recurso
output "bucket_id" {
  value = aws_s3_bucket.este.id
}

Error 4: Variables sin descripción

Las variables sin descripción hacen el módulo difícil de usar para otros (o para ti mismo en el futuro):

# MALO: Sin contexto para el usuario
variable "n" {
  type = number
}

# BUENO: Claro y documentado
variable "numero_instancias" {
  description = "Número de instancias EC2 a crear en el grupo de autoescalado"
  type        = number
  default     = 2
}

Resumen

En esta lección has aprendido los conceptos fundamentales de los módulos de Terraform:

  • Un módulo es un directorio con archivos .tf que encapsula una pieza de infraestructura reutilizable.
  • El módulo raíz es donde ejecutas Terraform; los módulos secundarios son referenciados desde él.
  • Los módulos pueden provenir de fuentes locales, del Terraform Registry, repositorios Git o almacenamiento en la nube.
  • La regla de los 3 es una buena heurística: si un patrón se repite 3 veces, extráelo como módulo.
  • La estructura estándar de un módulo incluye main.tf, variables.tf, outputs.tf y README.md.

En el siguiente tema aprenderás a crear módulos de Terraform desde cero, con todos los detalles de implementación: cómo definir variables con tipos y validaciones, cómo estructurar los outputs y cómo documentar correctamente un módulo para que otros equipos puedan usarlo con confianza.

© Copyright 2026. Todos los derechos reservados