¿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:
- 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.
- 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, gatewaysmodules/compute— instancias, grupos de autoescaladomodules/database— RDS, parámetros, backupsmodules/security— grupos de seguridad, roles IAM
- 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.tfEstructura 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.mdDescripció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 stateoperations. - 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:
- ¿Este conjunto de recursos se repetirá en múltiples contextos?
- ¿Tiene sentido como unidad lógica independiente?
- ¿Tiene una interfaz clara (inputs y outputs bien definidos)?
- ¿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.tfmodules/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:
- ¿Qué patrón se repite en esta configuración?
- ¿Cuántas veces se repite?
- ¿Qué variables necesitaría el módulo?
- ¿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 archivosdb_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
.tfque 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.tfyREADME.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.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
