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

Esta 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.tf

modules/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 directorio

El módulo debe aceptar las siguientes variables:

  • nombre_proyecto (obligatorio): nombre del proyecto
  • descripcion (obligatorio): descripción del proyecto
  • version (opcional, default "1.0.0"): versión del proyecto
  • autor (opcional, default "Sin especificar"): nombre del autor
  • caracteristicas (opcional, default []): lista de características habilitadas

El módulo debe exponer los siguientes outputs:

  • directorio_base: ruta del directorio raíz del proyecto
  • ruta_readme: ruta del README.md creado
  • ruta_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.tf

modules/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.json

Errores 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 Terraform

Solució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.tf y README.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.

© Copyright 2026. Todos los derechos reservados