Módulos de Terraform
Construye módulos de Terraform reutilizables con estructura adecuada, inputs, outputs y versionado. Cubre composición, testing y publicación en el Registry.
Introducción
Los módulos de Terraform encapsulan recursos de infraestructura en unidades reutilizables y componibles. Un módulo bien estructurado te permite provisionar la misma infraestructura across entornos (dev, staging, prod) con diferentes configuraciones. A continuación: estructura de módulos, inputs/outputs, composición, versionado, testing y publicación al Terraform Registry.
Estructura del Módulo
modules/
└── vpc/
├── main.tf # Definiciones de recursos
├── variables.tf # Variables de input
├── outputs.tf # Valores de output
├── versions.tf # Versiones requeridas de Terraform y providers
├── README.md # Documentación del módulo
└── examples/
└── basic/
└── main.tf # Ejemplo de uso
main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = var.enable_dns_support
enable_dns_hostnames = var.enable_dns_hostnames
tags = merge(
{
Name = var.name
},
var.tags
)
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = merge(
{
Name = "${var.name}-public-${count.index + 1}"
},
var.tags
)
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnets[count.index]
availability_zone = var.azs[count.index]
tags = merge(
{
Name = "${var.name}-private-${count.index + 1}"
},
var.tags
)
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(
{
Name = var.name
},
var.tags
)
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(
{
Name = "${var.name}-public"
},
var.tags
)
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
variables.tf
variable "name" {
description = "Prefijo de nombre para todos los recursos"
type = string
}
variable "cidr_block" {
description = "Bloque CIDR para el VPC"
type = string
default = "10.0.0.0/16"
}
variable "azs" {
description = "Zonas de disponibilidad"
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
variable "public_subnets" {
description = "Bloques CIDR de subnets públicas"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "private_subnets" {
description = "Bloques CIDR de subnets privadas"
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}
variable "enable_dns_support" {
description = "Habilitar DNS support en el VPC"
type = bool
default = true
}
variable "enable_dns_hostnames" {
description = "Habilitar DNS hostnames en el VPC"
type = bool
default = true
}
variable "tags" {
description = "Tags adicionales para todos los recursos"
type = map(string)
default = {}
}
outputs.tf
output "vpc_id" {
description = "ID del VPC"
value = aws_vpc.this.id
}
output "vpc_cidr_block" {
description = "Bloque CIDR del VPC"
value = aws_vpc.this.cidr_block
}
output "public_subnet_ids" {
description = "IDs de las subnets públicas"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs de las subnets privadas"
value = aws_subnet.private[*].id
}
output "internet_gateway_id" {
description = "ID del Internet Gateway"
value = aws_internet_gateway.this.id
}
output "public_route_table_id" {
description = "ID de la route table pública"
value = aws_route_table.public.id
}
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Consumir un Módulo
module "vpc" {
source = "git::https://github.com/example/terraform-modules.git//vpc?ref=v1.2.0"
name = "production-vpc"
cidr_block = "10.10.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.10.1.0/24", "10.10.2.0/24"]
private_subnets = ["10.10.101.0/24", "10.10.102.0/24"]
tags = {
Environment = "production"
Team = "platform"
}
}
module "eks" {
source = "git::https://github.com/example/terraform-modules.git//eks?ref=v1.2.0"
name = "production-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
cluster_version = "1.28"
}
Composición de Módulos
# modules/landing-zone/main.tf
module "vpc" {
source = "../vpc"
name = "${var.name}-vpc"
cidr_block = var.vpc_cidr
azs = var.azs
tags = var.tags
}
module "security_groups" {
source = "../security-groups"
vpc_id = module.vpc.vpc_id
name = var.name
tags = var.tags
}
module "eks" {
source = "../eks"
name = "${var.name}-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
cluster_version = var.kubernetes_version
node_groups = var.node_groups
tags = var.tags
depends_on = [module.security_groups]
}
Testing con Terratest
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",
Vars: map[string]interface{}{
"name": "terratest-vpc",
"cidr_block": "10.99.0.0/16",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
publicSubnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
assert.Len(t, publicSubnets, 3)
}
Estrategia de Versionado
| Versión | Significado | Cuándo Bump |
|---|---|---|
| 1.0.0 | Release estable inicial | Primer release estable |
| 1.1.0 | Nueva feature (backward compatible) | Nueva variable opcional, nuevo output |
| 1.2.0 | Otra feature | Nuevo recurso, nuevo default |
| 2.0.0 | Breaking change | Variable removida, default cambiado, output renombrado |
Git tags para versiones de módulos
git tag v1.0.0 -m "Initial release"
git tag v1.1.0 -m "Add NAT gateway support"
git tag v2.0.0 -m "Breaking: rename public_subnets to public_subnet_cidrs"
git push origin --tags
Pautas
- Un módulo por tipo de recurso — un módulo VPC, un módulo EKS, un módulo RDS
- Usar
for_eachsobrecountcuando sea posible — evita recreación de recursos al cambiar listas - Proveer defaults sensatos — los módulos deberían funcionar con inputs mínimos
- Usar bloques
validation— catchear inputs inválidos temprano - Documentar cada variable — description es obligatoria, ejemplos recomendados
- Usar
variable_validation— enforce constraints al plan time - Mantener módulos pequeños y enfocados — componer módulos en lugar de construir mega-módulos
- Usar
localspara valores computados — mantener main.tf legible - Taggear todo — usar el patrón
merge(var.tags, {Name = ...}) - Pinear versiones de provider — usar
~>para constraints de minor version - Usar
terraform fmtyterraform validate— nunca commitear código sin formatear - Escribir ejemplos — cada módulo debería tener al menos un ejemplo funcional
Errores Comunes
- Hardcodear valores que deberían ser variables — los módulos no son reutilizables
- No setear
required_version— el módulo se rompe en versiones viejas de Terraform - Usar
countcon listas — remover un item shiftea todos los recursos subsiguientes - No proveer outputs — los consumidores no pueden referenciar recursos del módulo
- Mezclar múltiples tipos de recursos en un módulo — viola single responsibility
- No testear módulos — los bugs de infraestructura son costosos
- Usar
latestpara versiones de provider — la reproducibilidad se pierde - No versionar módulos — los consumidores no pueden pinear una versión conocida
- Ignorar
terraform validate— errores de sintaxis aparecen al apply time
See Also
Preguntas frecuentes
¿Debo usar el Terraform Registry o Git repos privados?
Usar el Terraform Registry para módulos open-source de los que otros pueden beneficiarse. Usar Git repos privados para módulos específicos de la organización. Ambos soportan version pinning vía ?ref=v1.0.0.
¿Cómo migro de count a for_each?
Añadir una versión for_each del recurso junto a la versión count. Usar bloques moved para decirle a Terraform que los recursos son los mismos:
moved {
from = aws_subnet.public[0]
to = aws_subnet.public["a"]
}
¿Deben los módulos gestionar state?
No. Los módulos definen recursos. La configuración root que llama al módulo gestiona state. Esto mantiene los módulos portables across state backends.
Recursos Relacionados
Infrastructure as Code — Terraform y Pulumi
Guía práctica para gestionar infraestructura como código: beneficios de enfoques declarativo vs imperativo, manejo de estado, módulos y testing de cambios de infraestructura.
GuideTerraform Best Practices — Módulos, State y Workspaces
Guía práctica de mejores prácticas de Terraform: diseño de módulos, gestión de estado remoto, workspaces y seguridad para infraestructura como código de grado productivo.
GuideDespliegues Blue-Green y Canary
Guía práctica de estrategias de deploy: blue-green, canary, rolling y feature flags. Minimiza riesgo y tiempo de rollback al liberar a producción.
RecipeConstruye un Provider Personalizado de Terraform con
Extiende Terraform con un provider personalizado usando Python y terraform-plugin-framework para gestionar recursos externos.
DocPlantilla de Revisión de Infrastructure as Code
Una plantilla para revisar código de infraestructura Terraform y CloudFormation.
GuideOptimización de Costos en AWS
Reduce el gasto en AWS cloud en 40%. Cubre EC2 right-sizing, Spot instances, Reserved Instances, Savings Plans, S3 lifecycle, RDS optimization, networking, monitoreo y automatización.