Estructura de archivos de un módulo
Crear un módulo bien estructurado desde el primer momento ahorra mucho tiempo y evita problemas futuros. La convención estándar de la comunidad Terraform establece la siguiente estructura:
mi-modulo/
├── main.tf # Recursos principales del módulo
├── variables.tf # Declaraciones de variables de entrada
├── outputs.tf # Declaraciones de outputs
├── versions.tf # Restricciones de versiones de Terraform y providers
├── README.md # Documentación (obligatoria para el registry público)
└── examples/ # Ejemplos de uso (muy recomendado)
├── basico/
│ ├── main.tf
│ └── README.md
└── avanzado/
├── main.tf
└── README.mdEsta estructura no es obligatoria técnicamente — Terraform solo necesita los archivos .tf — pero seguirla hace que cualquier desarrollador que conozca el ecosistema Terraform pueda orientarse inmediatamente en tu módulo.
Principio clave: cada archivo tiene una responsabilidad única y bien definida.
Definir variables de entrada del módulo
Las variables de entrada son la interfaz pública del módulo. Definen qué información necesita el módulo para funcionar. Se declaran en variables.tf con el bloque variable.
Anatomía completa de una variable
# variables.tf
variable "nombre_variable" {
# description: OBLIGATORIO en la práctica. Explica para qué sirve la variable.
# Aparece en la salida de `terraform plan` y en la documentación autogenerada.
description = "Descripción clara y concisa del propósito de esta variable"
# type: El tipo de dato que acepta la variable.
# Tipos básicos: string, number, bool
# Tipos complejos: list(string), map(string), object({...}), set(string)
type = string
# default: Valor por defecto. Si se omite, la variable es OBLIGATORIA.
# Si se incluye, la variable es OPCIONAL (puede omitirse al llamar al módulo).
default = "valor-por-defecto"
# validation: Reglas de validación para rechazar valores inválidos.
# Disponible desde Terraform 0.13
validation {
# condition: expresión booleana. Si es false, terraform lanza el error.
condition = length(var.nombre_variable) > 0
error_message = "El nombre no puede estar vacío."
}
# sensitive: Si es true, Terraform oculta el valor en los logs y outputs.
# Usar para contraseñas, tokens, claves privadas.
sensitive = false
}Tipos de variables y cuándo usar cada uno
# variables.tf — Ejemplos de todos los tipos
# STRING: texto simple
variable "entorno" {
description = "Nombre del entorno de despliegue"
type = string
default = "development"
validation {
condition = contains(["development", "staging", "production"], var.entorno)
error_message = "El entorno debe ser 'development', 'staging' o 'production'."
}
}
# NUMBER: número entero o decimal
variable "numero_instancias" {
description = "Número de instancias a crear"
type = number
default = 1
validation {
condition = var.numero_instancias >= 1 && var.numero_instancias <= 20
error_message = "El número de instancias debe estar entre 1 y 20."
}
}
# BOOL: verdadero o falso
variable "habilitar_logs" {
description = "Si true, activa el sistema de logging detallado"
type = bool
default = false
}
# LIST(STRING): lista ordenada de strings
variable "zonas_disponibilidad" {
description = "Lista de zonas de disponibilidad donde desplegar recursos"
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
# MAP(STRING): diccionario clave-valor de strings
variable "etiquetas" {
description = "Mapa de etiquetas (tags) a aplicar a todos los recursos"
type = map(string)
default = {}
}
# OBJECT: estructura con campos tipados
variable "configuracion_base_datos" {
description = "Configuración completa del servidor de base de datos"
type = object({
nombre_bd = string
puerto = number
usuario = string
multi_az = bool
})
# No tiene default: es obligatoria
}
# SENSITIVE: para datos secretos
variable "contrasena_bd" {
description = "Contraseña del administrador de la base de datos"
type = string
sensitive = true
# No tiene default: el usuario DEBE proporcionarla
}Definir outputs del módulo
Los outputs son los valores de retorno del módulo. Permiten que el código llamante acceda a atributos de los recursos creados dentro del módulo. Se declaran en outputs.tf:
# outputs.tf
output "nombre_output" {
# description: Documenta qué representa este output
description = "Descripción de qué información contiene este output"
# value: La expresión que produce el valor del output.
# Típicamente referencia atributos de recursos del módulo.
value = resource_type.resource_name.attribute
# sensitive: Si true, el valor se oculta en la CLI.
# Un output sensitive solo puede pasarse a inputs también sensitive.
sensitive = false
}Convenciones de nomenclatura
Seguir convenciones consistentes hace que los módulos sean predecibles y fáciles de usar:
| Elemento | Convención | Ejemplo |
|---|---|---|
| Nombre del módulo | terraform-<provider>-<nombre> |
terraform-aws-vpc |
| Variables | snake_case descriptivo | numero_instancias, habilitar_https |
| Outputs | snake_case, con prefijo del recurso si hay varios | instancia_id, bucket_arn |
| Recursos internos | snake_case, nombre semántico | resource "aws_instance" "web_server" |
| Archivos | Nombres estándar del ecosistema | main.tf, variables.tf, outputs.tf |
| Directorios de módulos | snake_case o kebab-case | modules/servidor-web |
Ejemplo completo: módulo para gestionar archivos locales
Crearemos un módulo que genera un conjunto de archivos de configuración para una aplicación. Este ejemplo usa el provider local (no necesita credenciales cloud) y muestra todos los conceptos de creación de módulos.
Estructura del proyecto
proyecto-modulo/
├── main.tf ← módulo raíz (usa el módulo)
├── variables.tf
├── outputs.tf
├── terraform.tfvars ← valores de las variables
└── modules/
└── configuracion-app/
├── main.tf ← módulo secundario
├── variables.tf
├── outputs.tf
└── versions.tfmodules/configuracion-app/versions.tf
# Especificamos las versiones mínimas requeridas.
# Esto protege a los usuarios del módulo contra incompatibilidades.
terraform {
# required_version: versión mínima de Terraform CLI necesaria
required_version = ">= 1.0"
# required_providers: providers que usa el módulo y sus versiones
required_providers {
local = {
source = "hashicorp/local"
# ~> 2.0 significa: >= 2.0 y < 3.0 (acepta parches y menores, no mayores)
version = "~> 2.0"
}
}
}modules/configuracion-app/variables.tf
# ============================================================
# Variables OBLIGATORIAS (sin valor default)
# ============================================================
variable "nombre_aplicacion" {
description = "Nombre de la aplicación. Usado como identificador en los archivos."
type = string
validation {
# Aseguramos que el nombre solo contiene caracteres válidos
condition = can(regex("^[a-z][a-z0-9-]*$", var.nombre_aplicacion))
error_message = "El nombre debe comenzar con letra minúscula y solo contener letras, números y guiones."
}
}
variable "directorio_destino" {
description = "Directorio absoluto donde se crearán los archivos de configuración."
type = string
}
variable "configuracion_servidor" {
description = "Parámetros de configuración del servidor de aplicaciones."
type = object({
host = string # hostname o IP del servidor
puerto = number # puerto en que escucha la aplicación
workers = number # número de procesos worker
})
}
# ============================================================
# Variables OPCIONALES (tienen valor default)
# ============================================================
variable "entorno" {
description = "Entorno de despliegue. Afecta a la configuración de logging y debug."
type = string
default = "development"
validation {
condition = contains(["development", "staging", "production"], var.entorno)
error_message = "El entorno debe ser uno de: development, staging, production."
}
}
variable "habilitar_debug" {
description = "Si true, activa el modo debug en los archivos de configuración."
type = bool
default = false
}
variable "etiquetas_extra" {
description = "Líneas adicionales a añadir al archivo de configuración principal."
type = list(string)
default = []
}
variable "permisos_archivo" {
description = "Permisos de los archivos en formato octal (ej: '0644')."
type = string
default = "0644"
}modules/configuracion-app/main.tf
# ============================================================
# Archivo de configuración principal de la aplicación
# ============================================================
# local_file crea un archivo en el sistema de archivos local.
# Es el recurso principal del provider "hashicorp/local".
resource "local_file" "config_principal" {
# filename: ruta completa del archivo a crear.
# Usamos la función join() para construir la ruta: combina directorio y nombre de archivo.
filename = "${var.directorio_destino}/${var.nombre_aplicacion}.conf"
# content: contenido del archivo como string.
# La función templatefile() nos permite usar plantillas con variables,
# pero aquí usamos string interpolation simple para mayor claridad.
content = <<-EOT
# Configuración de ${var.nombre_aplicacion}
# Generado automáticamente por Terraform - NO EDITAR MANUALMENTE
# Entorno: ${var.entorno}
# Fecha de generación: controlada por Terraform
[servidor]
host = ${var.configuracion_servidor.host}
puerto = ${var.configuracion_servidor.puerto}
workers = ${var.configuracion_servidor.workers}
[aplicacion]
nombre = ${var.nombre_aplicacion}
entorno = ${var.entorno}
debug = ${var.habilitar_debug}
${length(var.etiquetas_extra) > 0 ? "[extra]" : ""}
${join("\n ", var.etiquetas_extra)}
EOT
# file_permission: permisos del archivo en formato octal
file_permission = var.permisos_archivo
}
# ============================================================
# Archivo de logging
# ============================================================
resource "local_file" "config_logging" {
filename = "${var.directorio_destino}/${var.nombre_aplicacion}-logging.conf"
# El nivel de log cambia según el entorno y el flag de debug.
# Usamos expresiones condicionales: condicion ? valor_si_true : valor_si_false
content = <<-EOT
[logging]
nivel = ${var.habilitar_debug ? "DEBUG" : (var.entorno == "production" ? "WARNING" : "INFO")}
archivo_log = /var/log/${var.nombre_aplicacion}/app.log
rotar = ${var.entorno == "production" ? "true" : "false"}
max_tamano = ${var.entorno == "production" ? "100MB" : "10MB"}
max_archivos = ${var.entorno == "production" ? "30" : "5"}
EOT
file_permission = var.permisos_archivo
}
# ============================================================
# Archivo de metadatos (para auditoría)
# ============================================================
resource "local_file" "metadatos" {
filename = "${var.directorio_destino}/.${var.nombre_aplicacion}-metadata.json"
# Usamos la función jsonencode() para generar JSON válido automáticamente.
# Esto evita errores de formato manual en JSON.
content = jsonencode({
aplicacion = var.nombre_aplicacion
entorno = var.entorno
gestionado_por = "terraform"
modulo_version = "1.0.0"
servidor = {
host = var.configuracion_servidor.host
puerto = var.configuracion_servidor.puerto
workers = var.configuracion_servidor.workers
}
})
# Archivos de metadatos con permisos más restrictivos
file_permission = "0600"
}modules/configuracion-app/outputs.tf
# ============================================================
# Outputs del módulo
# Los outputs permiten al módulo raíz acceder a los resultados
# ============================================================
output "ruta_config_principal" {
description = "Ruta completa del archivo de configuración principal creado."
value = local_file.config_principal.filename
}
output "ruta_config_logging" {
description = "Ruta completa del archivo de configuración de logging creado."
value = local_file.config_logging.filename
}
output "ruta_metadatos" {
description = "Ruta del archivo de metadatos JSON."
value = local_file.metadatos.filename
}
# Output con múltiples valores agrupados en un objeto
output "resumen" {
description = "Resumen de todos los archivos creados por el módulo."
value = {
aplicacion = var.nombre_aplicacion
entorno = var.entorno
archivos_creados = [
local_file.config_principal.filename,
local_file.config_logging.filename,
local_file.metadatos.filename,
]
}
}main.tf (módulo raíz que usa el módulo)
terraform {
required_version = ">= 1.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}
# Primera instancia del módulo: entorno de desarrollo
module "config_desarrollo" {
source = "./modules/configuracion-app"
# Variables obligatorias
nombre_aplicacion = "mi-api"
directorio_destino = "/tmp/config/dev"
configuracion_servidor = {
host = "localhost"
puerto = 3000
workers = 2
}
# Variables opcionales (sobreescribimos los defaults)
entorno = "development"
habilitar_debug = true
etiquetas_extra = [
"modo_desarrollo = true",
"hot_reload = true",
]
}
# Segunda instancia del módulo: entorno de producción
module "config_produccion" {
source = "./modules/configuracion-app"
nombre_aplicacion = "mi-api"
directorio_destino = "/tmp/config/prod"
configuracion_servidor = {
host = "prod-api.internal"
puerto = 8080
workers = 16
}
entorno = "production"
habilitar_debug = false
# etiquetas_extra usa el default: []
}
# Outputs del módulo raíz: exponemos información de ambos entornos
output "resumen_desarrollo" {
value = module.config_desarrollo.resumen
}
output "resumen_produccion" {
value = module.config_produccion.resumen
}Ejemplo avanzado: módulo para un servidor web en AWS (conceptual)
Este ejemplo muestra cómo diseñaría un módulo para AWS, explicando las decisiones de diseño aunque no necesites credenciales AWS para entenderlo:
# modules/servidor-web-aws/variables.tf
variable "nombre" {
description = "Nombre identificativo para el servidor web y sus recursos asociados."
type = string
}
variable "tipo_instancia" {
description = "Tipo de instancia EC2 (determina CPU y RAM)."
type = string
default = "t3.micro"
validation {
# Verificamos que sea un tipo de instancia válido (simplificado)
condition = can(regex("^(t3|t3a|m5|c5)\\.(micro|small|medium|large|xlarge)$", var.tipo_instancia))
error_message = "Tipo de instancia no válido. Use t3, t3a, m5 o c5 en tamaños micro a xlarge."
}
}
variable "ami_id" {
description = "ID de la Amazon Machine Image a usar para la instancia."
type = string
}
variable "subnet_id" {
description = "ID de la subred donde se creará la instancia."
type = string
}
variable "vpc_id" {
description = "ID de la VPC donde se encuentra la subred."
type = string
}
variable "puerto_aplicacion" {
description = "Puerto en que escucha la aplicación web."
type = number
default = 80
}
variable "permitir_ssh" {
description = "Si true, abre el puerto 22 para acceso SSH desde cualquier IP. NO recomendado en producción."
type = bool
default = false
}
variable "ip_ssh_permitida" {
description = "CIDR de la IP desde la que se permite SSH. Solo relevante si permitir_ssh es true."
type = string
default = "0.0.0.0/0"
}
variable "tags" {
description = "Etiquetas adicionales a aplicar a todos los recursos."
type = map(string)
default = {}
}# modules/servidor-web-aws/main.tf
# Grupo de seguridad: controla el tráfico de red de la instancia
resource "aws_security_group" "web" {
name = "${var.nombre}-sg"
description = "Security group para servidor web ${var.nombre}"
vpc_id = var.vpc_id # asociamos al VPC correcto
# Regla de entrada: permite tráfico HTTP en el puerto de la aplicación
ingress {
from_port = var.puerto_aplicacion
to_port = var.puerto_aplicacion
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # tráfico desde cualquier IP
description = "Tráfico web de entrada"
}
# Regla de entrada SSH: solo si está habilitada
# count: crea 0 o 1 recursos según la condición (patrón "feature flag")
dynamic "ingress" {
# for_each con una lista de 0 o 1 elementos: patrón condicional
for_each = var.permitir_ssh ? [1] : []
content {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.ip_ssh_permitida]
description = "Acceso SSH"
}
}
# Regla de salida: permite todo el tráfico de salida
egress {
from_port = 0
to_port = 0
protocol = "-1" # -1 significa todos los protocolos
cidr_blocks = ["0.0.0.0/0"]
description = "Todo el tráfico de salida permitido"
}
# Merge combina dos mapas: las tags del módulo con las tags específicas del recurso
tags = merge(var.tags, {
Name = "${var.nombre}-sg"
ManagedBy = "terraform"
})
}
# Instancia EC2: el servidor web en sí
resource "aws_instance" "web" {
ami = var.ami_id # imagen del sistema operativo
instance_type = var.tipo_instancia # tamaño de la instancia
subnet_id = var.subnet_id # subred donde se lanza
# Asociamos el grupo de seguridad que acabamos de crear
vpc_security_group_ids = [aws_security_group.web.id]
# Script de inicio que se ejecuta al lanzar la instancia
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Servidor ${var.nombre} funcionando</h1>" > /var/www/html/index.html
EOF
tags = merge(var.tags, {
Name = var.nombre
ManagedBy = "terraform"
})
}# modules/servidor-web-aws/outputs.tf
output "instancia_id" {
description = "ID de la instancia EC2 creada."
value = aws_instance.web.id
}
output "ip_publica" {
description = "Dirección IP pública de la instancia (si la subred asigna IPs públicas)."
value = aws_instance.web.public_ip
}
output "ip_privada" {
description = "Dirección IP privada de la instancia dentro de la VPC."
value = aws_instance.web.private_ip
}
output "security_group_id" {
description = "ID del grupo de seguridad creado para la instancia."
value = aws_security_group.web.id
}
output "url_acceso" {
description = "URL para acceder al servidor web (si tiene IP pública)."
value = "http://${aws_instance.web.public_ip}:${var.puerto_aplicacion}"
}Documentación del módulo con README
Un README bien escrito es tan importante como el código. Este es el formato recomendado:
# Módulo: configuracion-app
Módulo de Terraform para generar archivos de configuración de una aplicación.
Crea los archivos de configuración principal, logging y metadatos en el
directorio especificado.
## Uso
module "config_mi_app" { source = "./modules/configuracion-app"
nombre_aplicacion = "mi-servicio" directorio_destino = "/etc/mi-servicio"
configuracion_servidor = { host = "0.0.0.0" puerto = 8080 workers = 4 }
entorno = "production" }
## Variables de entrada | Variable | Tipo | Obligatoria | Default | Descripción | |---|---|---|---|---| | nombre_aplicacion | string | Sí | - | Nombre de la aplicación | | directorio_destino | string | Sí | - | Directorio donde crear los archivos | | configuracion_servidor | object | Sí | - | Parámetros del servidor | | entorno | string | No | "development" | Entorno de despliegue | | habilitar_debug | bool | No | false | Activa modo debug | ## Outputs | Output | Descripción | |---|---| | ruta_config_principal | Ruta del archivo .conf principal | | ruta_config_logging | Ruta del archivo de logging | | resumen | Objeto con resumen de todos los archivos creados |
Versionado de módulos
Cuando publicas un módulo, es fundamental usar versionado semántico:
MAYOR.MENOR.PARCHE | | | | | └─ Correcciones de bugs, sin cambios en la interfaz | └─ Nuevas funcionalidades, compatible hacia atrás └─ Cambios que rompen la compatibilidad (breaking changes)
| Tipo de cambio | Acción | Ejemplo |
|---|---|---|
| Añadir variable con default | MENOR | 1.0.0 → 1.1.0 |
| Corregir bug sin cambio de interfaz | PARCHE | 1.1.0 → 1.1.1 |
| Eliminar una variable | MAYOR | 1.1.0 → 2.0.0 |
| Renombrar un output | MAYOR | 1.1.0 → 2.0.0 |
| Añadir un output nuevo | MENOR | 1.1.0 → 1.2.0 |
Buenas prácticas en diseño de módulos
Principio de responsabilidad única (Single Responsibility)
Cada módulo debe hacer una cosa y hacerla bien. Evita módulos "todo en uno" que creen demasiados tipos de recursos no relacionados:
# MAL: Módulo que hace demasiado
module "infraestructura_completa" {
source = "./modules/todo"
# Crea VPC, instancias, bases de datos, buckets S3, DNS...
}
# BIEN: Módulos especializados
module "red" {
source = "./modules/networking"
}
module "computo" {
source = "./modules/compute"
subnet_id = module.red.subnet_id
}
module "almacenamiento" {
source = "./modules/storage"
}Interfaz mínima
Expón solo las variables que realmente necesitan ser configuradas. Demasiadas variables hacen el módulo difícil de usar:
# MAL: demasiadas opciones expuestas
variable "instance_type" { ... }
variable "ebs_volume_size" { ... }
variable "ebs_volume_type" { ... }
variable "ebs_iops" { ... }
variable "ebs_throughput" { ... }
variable "ebs_encrypted" { ... }
variable "ebs_kms_key_id" { ... }
# ... 20 variables más
# BIEN: agrupar opciones relacionadas o usar valores sensatos por defecto
variable "tipo_instancia" {
description = "Tamaño de la instancia: small, medium, large"
type = string
default = "small"
# El módulo decide internamente qué tipo EC2 corresponde a cada tamaño
}No gestiones el estado del provider dentro del módulo
El módulo raíz es responsable de configurar los providers. Los módulos secundarios no deben tener bloques provider {}:
# MAL: configurar el provider dentro del módulo
# modules/mi-modulo/main.tf
provider "aws" { # ← INCORRECTO en un módulo secundario
region = "us-east-1"
}
# BIEN: el módulo raíz configura el provider
# main.tf (módulo raíz)
provider "aws" {
region = "us-east-1"
}
module "mi_modulo" {
source = "./modules/mi-modulo"
# El módulo hereda automáticamente la configuración del provider
}Ejercicio: Crear un módulo que gestione una "aplicación"
Crea un módulo llamado gestion-proyecto que, dado un nombre de proyecto, cree la siguiente estructura de archivos en el directorio /tmp/proyectos/<nombre>:
/tmp/proyectos/<nombre>/
├── README.md ← descripción del proyecto
├── config/
│ └── settings.json ← configuración en JSON
└── logs/
└── .gitkeep ← archivo vacío para mantener el directorioEl módulo debe aceptar las siguientes variables:
nombre_proyecto(obligatorio): nombre del proyectodescripcion(obligatorio): descripción del proyectoversion(opcional, default "1.0.0"): versión del proyectoautor(opcional, default "Sin especificar"): nombre del autorcaracteristicas(opcional, default[]): lista de características habilitadas
El módulo debe exponer los siguientes outputs:
directorio_base: ruta del directorio raíz del proyectoruta_readme: ruta del README.md creadoruta_configuracion: ruta del settings.json
Solución completa con estructura de directorios
Estructura
solucion-ejercicio/
├── main.tf
└── modules/
└── gestion-proyecto/
├── versions.tf
├── variables.tf
├── main.tf
└── outputs.tfmodules/gestion-proyecto/versions.tf
terraform {
required_version = ">= 1.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}modules/gestion-proyecto/variables.tf
variable "nombre_proyecto" {
description = "Nombre único del proyecto. Usado como nombre de directorio."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]*$", var.nombre_proyecto))
error_message = "El nombre del proyecto solo puede contener letras minúsculas, números y guiones, y debe comenzar con letra."
}
}
variable "descripcion" {
description = "Descripción del propósito del proyecto."
type = string
}
variable "version" {
description = "Versión actual del proyecto en formato semántico."
type = string
default = "1.0.0"
}
variable "autor" {
description = "Nombre del autor o equipo responsable del proyecto."
type = string
default = "Sin especificar"
}
variable "caracteristicas" {
description = "Lista de características o módulos habilitados en el proyecto."
type = list(string)
default = []
}modules/gestion-proyecto/main.tf
# Variable local: calculamos el directorio base una sola vez y lo reutilizamos.
# Las variables locales (locals) son valores intermedios calculados en el módulo.
locals {
# Construimos la ruta base del proyecto
directorio_base = "/tmp/proyectos/${var.nombre_proyecto}"
}
# README.md del proyecto
resource "local_file" "readme" {
filename = "${local.directorio_base}/README.md"
content = <<-EOT
# ${var.nombre_proyecto}
${var.descripcion}
## Información del proyecto
- **Versión**: ${var.version}
- **Autor**: ${var.autor}
- **Gestionado por**: Terraform
${length(var.caracteristicas) > 0 ? "## Características habilitadas\n" : ""}
${join("\n", [for c in var.caracteristicas : "- ${c}"])}
EOT
file_permission = "0644"
}
# Archivo de configuración en JSON
resource "local_file" "settings" {
filename = "${local.directorio_base}/config/settings.json"
# jsonencode genera JSON correctamente formateado
content = jsonencode({
proyecto = {
nombre = var.nombre_proyecto
descripcion = var.descripcion
version = var.version
autor = var.autor
}
caracteristicas = var.caracteristicas
metadata = {
gestionado_por = "terraform"
}
})
file_permission = "0644"
}
# Archivo .gitkeep para crear el directorio de logs
resource "local_file" "logs_gitkeep" {
filename = "${local.directorio_base}/logs/.gitkeep"
content = ""
file_permission = "0644"
}modules/gestion-proyecto/outputs.tf
output "directorio_base" {
description = "Ruta absoluta del directorio raíz del proyecto creado."
value = local.directorio_base
}
output "ruta_readme" {
description = "Ruta absoluta del archivo README.md del proyecto."
value = local_file.readme.filename
}
output "ruta_configuracion" {
description = "Ruta absoluta del archivo settings.json del proyecto."
value = local_file.settings.filename
}main.tf (uso del módulo)
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}
module "proyecto_alpha" {
source = "./modules/gestion-proyecto"
nombre_proyecto = "proyecto-alpha"
descripcion = "Sistema de gestión de inventario para almacenes"
version = "2.3.1"
autor = "Equipo Backend"
caracteristicas = [
"autenticacion-jwt",
"api-rest",
"exportacion-excel",
]
}
module "proyecto_beta" {
source = "./modules/gestion-proyecto"
nombre_proyecto = "proyecto-beta"
descripcion = "Aplicación móvil de seguimiento de entregas"
# version y autor usan sus valores por defecto
}
output "info_alpha" {
value = {
directorio = module.proyecto_alpha.directorio_base
readme = module.proyecto_alpha.ruta_readme
configuracion = module.proyecto_alpha.ruta_configuracion
}
}Para ejecutar:
# Inicializar (descarga providers y localiza módulos)
terraform init
# Ver el plan de ejecución
terraform plan
# Aplicar los cambios
terraform apply
# Verificar los archivos creados
ls -la /tmp/proyectos/proyecto-alpha/
cat /tmp/proyectos/proyecto-alpha/README.md
cat /tmp/proyectos/proyecto-alpha/config/settings.jsonErrores comunes al crear módulos
Error 1: Referenciar providers en módulos secundarios
# INCORRECTO en modules/mi-modulo/main.tf
provider "aws" {
region = "eu-west-1"
}
# Esto causará comportamientos inesperados y warnings de TerraformSolución: Los providers se configuran únicamente en el módulo raíz.
Error 2: Usar paths absolutos hardcodeados
# INCORRECTO: ruta absoluta hardcodeada
resource "local_file" "config" {
filename = "/home/usuario/configs/app.conf" # ← frágil, no portable
}
# CORRECTO: usar variable para la ruta
resource "local_file" "config" {
filename = "${var.directorio_base}/app.conf" # ← flexible y reutilizable
}Error 3: No incluir descripción en variables y outputs
Sin descripciones, los usuarios del módulo no saben qué espera cada variable. Terraform muestra las descripciones en terraform plan y en la documentación autogenerada del registry.
Error 4: Olvidar el bloque versions.tf
Sin restricciones de versión, el módulo podría romperse al actualizar Terraform o los providers. Siempre especifica versiones mínimas.
Error 5: Outputs que exponen información sensible sin marcarlos como sensitive
# INCORRECTO: expone contraseñas en texto plano
output "password_bd" {
value = var.password_bd # aparecerá en logs y en terraform.tfstate
}
# CORRECTO
output "password_bd" {
value = var.password_bd
sensitive = true # Terraform oculta este valor en la CLI
}Resumen
En esta lección has aprendido a crear módulos de Terraform desde cero:
- La estructura estándar de un módulo:
main.tf,variables.tf,outputs.tf,versions.tfyREADME.md. - Cómo definir variables de entrada con tipos, descripciones, valores por defecto y validaciones.
- Cómo definir outputs para exponer información de los recursos creados.
- Las convenciones de nomenclatura del ecosistema Terraform.
- El principio de responsabilidad única y la interfaz mínima como guías de diseño.
- La importancia del versionado semántico para gestionar cambios en módulos.
Con este conocimiento ya puedes crear módulos sólidos, bien documentados y mantenibles. En el siguiente tema aprenderás a usar módulos de Terraform: cómo invocar módulos locales, del Terraform Registry y de repositorios Git, cómo pasar variables y acceder a sus outputs, y cómo gestionar versiones de módulos externos.
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
