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
- Entendiendo Módulos de Terraform
- Estructura de Directorio de Módulos
- Variables y Entradas
- Salidas y Flujo de Datos
- Patrones de Composición de Módulos
- Publicación en Terraform Registry
- Estrategias de Versionado
- Prueba de Módulos
- Mejores Prácticas
- 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
| Nombre | Tipo | Requerido | Predeterminado | Descripción |
|---|---|---|---|---|
| name | string | sí | - | Prefijo de nombre para recursos |
| cidr_block | string | sí | - | Bloque CIDR de VPC |
| public_subnets | map(object) | no | {} | Configuraciones de subnet pública |
Salidas
| Nombre | Descripción |
|---|---|
| vpc_id | Identificador de VPC |
| public_subnet_ids | Mapa 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
| Nombre | Tipo | Requerido | Descripción |
|---|---|---|---|
| cidr_block | string | sí | Bloque CIDR de VPC |
Salidas
| Nombre | Descripción |
|---|---|
| vpc_id | ID 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.


