Módulos de Terraform y Mejores Prácticas

Los módulos de Terraform permiten la reutilización y abstracción de infraestructura agrupando recursos relacionados en unidades lógicas y autónomas. Los módulos te permiten crear componentes de infraestructura componibles, compartir código entre proyectos, aprovechar el Terraform Registry, mantener estándares consistentes y escalar la gestión de infraestructura. Esta guía cubre la estructura de módulos, el diseño de variables, las declaraciones de salida, la publicación en registry, estrategias de versionado, patrones de composición de módulos y enfoques de prueba.

Tabla de Contenidos

  1. Entendiendo Módulos de Terraform
  2. Estructura de Directorio de Módulos
  3. Variables y Entradas
  4. Salidas y Flujo de Datos
  5. Patrones de Composición de Módulos
  6. Publicación en Terraform Registry
  7. Estrategias de Versionado
  8. Prueba de Módulos
  9. Mejores Prácticas
  10. Conclusión

Entendiendo Módulos de Terraform

Un módulo de Terraform es un directorio que contiene archivos de configuración de Terraform que definen un conjunto de recursos. Los módulos proporcionan abstracción, reutilización y encapsulación de componentes de infraestructura. El módulo más simple es cualquier directorio con archivos .tf, pero los módulos bien diseñados siguen patrones consistentes.

Los módulos sirven varios propósitos:

Reutilización de Código: Crear una vez, usar muchas veces en proyectos y entornos Abstracción: Ocultar complejidad detrás de interfaces simples y bien definidas Consistencia: Imponer estándares y mejores prácticas en toda la infraestructura Mantenibilidad: Actualizar patrones de infraestructura en un solo lugar Composabilidad: Combinar módulos para crear infraestructura compleja

Los módulos se invocan usando el bloque module en la configuración:

module "vpc" {
  source = "./modules/vpc"

  cidr_block = "10.0.0.0/16"
  environment = var.environment
}

Estructura de Directorio de Módulos

Un módulo bien organizado sigue convenciones estándar:

my-module/
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf
├── README.md
├── versions.tf
├── terraform.tfvars.example
└── examples/
    ├── basic/
    │   ├── main.tf
    │   └── terraform.tfvars
    └── advanced/
        ├── main.tf
        └── terraform.tfvars

main.tf: Contiene las definiciones de recurso primarias. Para módulos complejos, dividir en archivos lógicos:

# main.tf - Crear VPC y subnets
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-vpc"
    }
  )
}

resource "aws_subnet" "public" {
  for_each = var.public_subnets

  vpc_id                  = aws_vpc.this.id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.availability_zone
  map_public_ip_on_launch = true

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-public-${each.key}"
      Type = "public"
    }
  )
}

variables.tf: Todas las variables de entrada del módulo:

# variables.tf
variable "name" {
  type        = string
  description = "Prefijo de nombre para todos los recursos"
  nullable    = false

  validation {
    condition     = can(regex("^[a-z0-9-]+$", var.name))
    error_message = "El nombre debe contener solo letras minúsculas, números y guiones."
  }
}

variable "cidr_block" {
  type        = string
  description = "Bloque CIDR para VPC"
  nullable    = false

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Debe ser un bloque CIDR válido."
  }
}

variable "public_subnets" {
  type = map(object({
    cidr_block       = string
    availability_zone = string
  }))
  description = "Mapa de configuraciones de subnet pública"
  default     = {}
}

variable "tags" {
  type        = map(string)
  description = "Etiquetas a aplicar a todos los recursos"
  default     = {}
}

outputs.tf: Exponer datos relevantes desde el módulo:

# outputs.tf
output "vpc_id" {
  value       = aws_vpc.this.id
  description = "ID de VPC"
}

output "vpc_cidr_block" {
  value       = aws_vpc.this.cidr_block
  description = "Bloque CIDR de VPC"
}

output "public_subnet_ids" {
  value       = { for name, subnet in aws_subnet.public : name => subnet.id }
  description = "Mapa de IDs de subnet pública"
}

output "public_route_table_id" {
  value       = aws_route_table.public.id
  description = "ID de tabla de enrutamiento pública"
}

versions.tf: Especificar versiones de proveedor y versión requerida de Terraform:

# versions.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

locals.tf: Valores locales calculados desde variables:

# locals.tf
locals {
  common_tags = merge(
    var.tags,
    {
      Managed     = "Terraform"
      Environment = var.environment
      Module      = "vpc"
    }
  )

  public_subnet_count = length(var.public_subnets)
}

README.md: Documentación comprensiva del módulo. Estructura de ejemplo:

# Módulo VPC

Crea una VPC de AWS con subnets pública y privada configurables.

## Características

- VPC con bloque CIDR configurable
- Subnets públicas con internet gateway
- Subnets privadas con NAT gateway
- Tablas de enrutamiento y grupos de seguridad
- Configuración automática de DNS

## Uso

```hcl
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "production"
  cidr_block = "10.0.0.0/16"

  public_subnets = {
    "us-east-1a" = { cidr_block = "10.0.1.0/24", availability_zone = "us-east-1a" }
    "us-east-1b" = { cidr_block = "10.0.2.0/24", availability_zone = "us-east-1b" }
  }
}

Variables

NombreTipoRequeridoPredeterminadoDescripción
namestring-Prefijo de nombre para recursos
cidr_blockstring-Bloque CIDR de VPC
public_subnetsmap(object)no{}Configuraciones de subnet pública

Salidas

NombreDescripción
vpc_idIdentificador de VPC
public_subnet_idsMapa de IDs de subnet pública

Ejemplos

Ver directorio examples/ para configuraciones completas.


## Variables y Entradas

Diseña variables cuidadosamente para crear interfaces de módulo intuitivas.

**Seguridad de Tipo**: Siempre declarar tipos de variable explícitamente:

```hcl
variable "instance_count" {
  type = number
  description = "Número de instancias a crear"
}

variable "environment" {
  type = string
  description = "Nombre de entorno"
}

variable "config" {
  type = object({
    name    = string
    cpu     = number
    memory  = number
  })
  description = "Objeto de configuración de instancia"
}

variable "tags" {
  type = map(string)
  description = "Etiquetas de recurso"
}

variable "allowed_protocols" {
  type = list(string)
  description = "Protocolos permitidos"
}

Validación: Validar valores de entrada para atrapar errores temprano:

variable "instance_type" {
  type        = string
  description = "Tipo de instancia EC2"

  validation {
    condition     = can(regex("^[tm][2-3]\\.", var.instance_type))
    error_message = "El tipo de instancia debe ser familia t2, t3, m2 o m3."
  }
}

variable "environment" {
  type        = string
  description = "Nombre de entorno"

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

variable "port" {
  type        = number
  description = "Número de puerto"

  validation {
    condition     = var.port >= 1 && var.port <= 65535
    error_message = "El puerto debe estar entre 1 y 65535."
  }
}

Valores por Defecto Sensatos: Proporcionar valores por defecto para parámetros opcionales:

variable "instance_count" {
  type        = number
  description = "Número de instancias"
  default     = 1

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 100
    error_message = "El recuento de instancias debe estar entre 1 y 100."
  }
}

variable "enable_monitoring" {
  type        = bool
  description = "Habilitar monitoreo de CloudWatch"
  default     = true
}

variable "tags" {
  type        = map(string)
  description = "Etiquetas de recurso adicionales"
  default     = {}
}

Salidas y Flujo de Datos

Las salidas exponen información relevante desde el módulo para ser usada por módulos padre o salidas en la configuración raíz.

Salidas Básicas:

output "instance_ids" {
  value       = aws_instance.web[*].id
  description = "Lista de IDs de instancia"
}

output "load_balancer_dns" {
  value       = aws_lb.main.dns_name
  description = "Nombre DNS del balanceador de carga"
}

output "database_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "Punto final de base de datos RDS"
}

Salidas Sensibles:

output "database_password" {
  value       = aws_db_instance.main.password
  description = "Contraseña maestra de base de datos"
  sensitive   = true
}

output "api_keys" {
  value       = {
    for key, secret in aws_secretsmanager_secret.api_keys : key => secret.arn
  }
  description = "Secretos de clave API"
  sensitive   = true
}

Salidas Complejas:

output "web_servers" {
  value = {
    for instance in aws_instance.web : instance.id => {
      private_ip = instance.private_ip
      public_ip  = instance.public_ip
      az         = instance.availability_zone
    }
  }
  description = "Información de servidor web"
}

output "security_group_rules" {
  value = {
    for rule in aws_security_group_rule.main : rule.id => {
      protocol   = rule.protocol
      from_port  = rule.from_port
      to_port    = rule.to_port
      cidr_blocks = rule.cidr_blocks
    }
  }
  description = "Reglas de grupo de seguridad"
}

Patrones de Composición de Módulos

Combinar módulos para construir infraestructura compleja.

Patrón de Módulo Raíz:

# main.tf - Componer módulos
module "vpc" {
  source = "./modules/vpc"

  name       = var.project_name
  cidr_block = var.vpc_cidr
  environment = var.environment
  tags        = var.tags
}

module "security_groups" {
  source = "./modules/security"

  vpc_id       = module.vpc.vpc_id
  environment  = var.environment
  tags         = var.tags

  depends_on = [module.vpc]
}

module "database" {
  source = "./modules/rds"

  vpc_id             = module.vpc.vpc_id
  private_subnet_ids = module.vpc.private_subnet_ids
  security_group_id  = module.security_groups.database_sg_id
  db_name            = var.database_name
  db_password        = var.database_password

  depends_on = [module.security_groups]
}

module "web_servers" {
  source = "./modules/ec2"

  vpc_id            = module.vpc.vpc_id
  subnet_ids        = module.vpc.public_subnet_ids
  security_group_id = module.security_groups.web_sg_id
  instance_count    = var.instance_count
  instance_type     = var.instance_type

  depends_on = [module.security_groups]
}

# Salidas raíz
output "vpc_id" {
  value = module.vpc.vpc_id
}

output "web_server_ips" {
  value = module.web_servers.public_ips
}

output "database_endpoint" {
  value = module.database.endpoint
}

Patrón de Anidamiento de Módulos:

# modules/platform/main.tf - Componer módulos de nivel inferior
module "networking" {
  source = "../networking"
  
  vpc_config = var.vpc_config
}

module "compute" {
  source = "../compute"
  
  vpc_id    = module.networking.vpc_id
  subnet_ids = module.networking.subnet_ids
  instance_config = var.instance_config
}

module "storage" {
  source = "../storage"
  
  environment = var.environment
  backup_config = var.backup_config
}

output "platform_id" {
  value = {
    vpc_id = module.networking.vpc_id
    compute_ids = module.compute.instance_ids
    storage_bucket = module.storage.bucket_id
  }
}

Publicación en Terraform Registry

Compartir módulos en el Terraform Registry para uso de la comunidad.

Requisitos previos:

# Estructura de repositorio GitHub
my-org/terraform-provider-resource-type/

# Ejemplo
my-org/terraform-aws-vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
├── examples/
└── tests/

Pasos de Publicación:

# 1. Crear repositorio GitHub
# my-org/terraform-aws-vpc
# (debe seguir la convención de nombres: terraform-PROVIDER-NAME)

# 2. Agregar versionado apropiado
git tag v1.0.0
git push origin v1.0.0

# 3. Agregar archivos requeridos
# - README.md con ejemplos de uso
# - versions.tf con requisitos de proveedor
# - Variables y salidas bien documentadas

# 4. Registrarse en Terraform Registry
# https://app.terraform.io
# Iniciar sesión con GitHub

# 5. Navegar a Registry > Modules > Publish
# Seleccionar repositorio GitHub
# Registry publica automáticamente en lanzamiento

# 6. Verificar publicación
# Módulo disponible en:
# registry.terraform.io/my-org/vpc/aws

Usar Módulos del Registry:

module "vpc" {
  source  = "my-org/vpc/aws"
  version = "~> 1.0"

  cidr_block  = "10.0.0.0/16"
  environment = "production"
}

# Usar restricciones de versión
# ~> 1.0  : versiones >= 1.0.0 y < 2.0.0
# >= 1.0  : cualquier versión 1.0.0 o superior
# 1.0.0   : coincidencia exacta de versión
# *       : cualquier versión

Estrategias de Versionado

Usar versionado semántico para comunicar cambios de módulo:

MAJOR.MINOR.PATCH

v1.0.0
├── MAJOR: Cambios que rompen compatibilidad (1 = lanzamiento estable inicial)
├── MINOR: Nuevas características, compatible hacia atrás
└── PATCH: Correcciones de errores, sin nuevas características

Ejemplos:
v1.0.0 → v1.1.0 (nueva salida, compatible hacia atrás)
v1.1.0 → v2.0.0 (variable renombrada, incompatible)
v1.0.0 → v1.0.1 (corregir typo en predeterminado)

Restricciones de Versión:

# Versión exacta
version = "1.0.0"

# Versión mínima
version = ">= 1.0.0"

# Rango
version = ">= 1.0.0, < 2.0.0"

# Lanzamientos compatibles
version = "~> 1.0"     # >= 1.0, < 2.0
version = "~> 1.0.0"   # >= 1.0.0, < 1.1.0

# Recomendado para módulos
version = ">= 1.0, < 2.0"

Prueba de Módulos

Probar módulos para asegurar confiabilidad.

terraform validate:

# Validar sintaxis
cd modules/vpc
terraform validate

# Verificar tipos de variable y referencias

terraform fmt:

# Formatear código consistentemente
terraform fmt -recursive .

# Verificar formato
terraform fmt -check -recursive .

Prueba Manual:

# Probar con ejemplo
cd examples/basic
terraform init -upgrade
terraform plan
terraform apply
terraform destroy

Prueba Automatizada con Terratest:

// test/vpc_test.go
package test

import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../examples/basic",
  }

  defer terraform.Destroy(t, terraformOptions)

  terraform.InitAndApply(t, terraformOptions)

  vpcId := terraform.Output(t, terraformOptions, "vpc_id")
  assert.NotEmpty(t, vpcId)

  subnetIds := terraform.OutputMap(t, terraformOptions, "subnet_ids")
  assert.NotEmpty(t, subnetIds)
}

Mejores Prácticas

Inmutabilidad: Una vez publicado, no modificar etiquetas de versión:

# Incorrecto - no hacer esto
git tag v1.0.0
# ... hacer cambios ...
git tag -d v1.0.0
git tag v1.0.0

# Correcto - usar nueva versión
git tag v1.0.1

Documentación: Mantener README.md actualizado con todas las entradas/salidas:

# Ejemplo de Uso

```hcl
module "vpc" {
  source = "my-org/vpc/aws"
  version = "~> 1.0"

  cidr_block = "10.0.0.0/16"
  environment = "prod"
  enable_nat = true
}

Entradas

NombreTipoRequeridoDescripción
cidr_blockstringBloque CIDR de VPC

Salidas

NombreDescripción
vpc_idID de VPC

**Compatibilidad Hacia Atrás**: Mantener compatibilidad cuando sea posible:

```hcl
# Incorrecto - cambio que rompe compatibilidad
variable "environment_name" {
  # renombrado desde "environment"
}

# Correcto - compatible hacia atrás
variable "environment" {
  # mantener variable original
}

variable "environment_name" {
  # agregar nueva variable con predeterminado de la anterior
  default = var.environment
}

Salidas Significativas: Exponer solo los valores necesarios:

# Demasiadas salidas
output "all_tags" {
  value = local.all_tags  # detalle interno
}

# Salidas correctas
output "vpc_id" {
  value = aws_vpc.this.id
}

output "subnet_ids" {
  value = aws_subnet.this[*].id
}

Conclusión

Los módulos de Terraform son fundamentales para infraestructura escalable y mantenible como código. Siguiendo patrones consistentes, diseñando interfaces claras con variables y salidas bien elegidas, aprovechando el Terraform Registry para compartir, usando versionado semántico y probando exhaustivamente, crearás componentes de infraestructura reutilizables que escalan entre equipos y proyectos. Los módulos bien diseñados aceleran el desarrollo de infraestructura mientras se mantiene la consistencia y se reducen los errores.