Terraform: Aprovisionamiento de Infraestructura - Técnicas Avanzadas y Ejemplos del Mundo Real

Introducción

Más allá de los conceptos básicos de Terraform, esta guía explora técnicas avanzadas de aprovisionamiento de infraestructura que te permiten construir entornos complejos, escalables y listos para producción. Mientras que entender los conceptos centrales de Terraform es esencial, dominar módulos, gestión de estado, espacios de trabajo y patrones avanzados de recursos separa la automatización básica del infrastructure-as-code de nivel empresarial.

La infraestructura moderna demanda más que simple creación de recursos. Requiere patrones repetibles, aislamiento de entornos, gestión de dependencias y orquestación segura de cambios a través de equipos distribuidos. Las características avanzadas de Terraform proporcionan las herramientas para cumplir estas demandas mientras mantienen la simplicidad declarativa que hace accesible el infrastructure-as-code.

Esta guía completa demuestra patrones prácticos para aprovisionar aplicaciones multi-capa, gestionar dependencias complejas, implementar gestión robusta de estado y crear módulos de infraestructura reutilizables. Cada ejemplo está probado en producción y diseñado para resolver desafíos del mundo real enfrentados por equipos DevOps gestionando infraestructura en la nube a escala.

Prerrequisitos

Antes de adentrarte en el uso avanzado de Terraform, asegúrate de tener:

  • Introducción básica a Terraform completada
  • Comprensión de fundamentos de proveedor de nube (AWS, Azure o GCP)
  • Terraform 1.0+ instalado
  • CLI del proveedor de nube configurado
  • Git para control de versiones
  • Experiencia con recursos básicos de Terraform
  • Comprensión de conceptos de redes

Gestión Avanzada de Recursos

Dependencias y Ordenamiento de Recursos

# terraform/network.tf
# Gestión explícita de dependencias

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

resource "aws_subnet" "public" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
    Tier = "Public"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-route-table"
  }

  # Dependencia explícita
  depends_on = [aws_internet_gateway.main]
}

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
}

# NAT Gateway con EIP
resource "aws_eip" "nat" {
  count  = 3
  domain = "vpc"

  tags = {
    Name = "nat-eip-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.main]
}

resource "aws_nat_gateway" "main" {
  count         = 3
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "nat-gateway-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.main]
}

# Subnets privadas
resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-subnet-${count.index + 1}"
    Tier = "Private"
  }
}

resource "aws_route_table" "private" {
  count  = 3
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "private-route-table-${count.index + 1}"
  }
}

resource "aws_route_table_association" "private" {
  count          = 3
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

Bloques Dinámicos para Recursos Complejos

# terraform/security-groups.tf
# Usando bloques dinámicos para configuraciones escalables

locals {
  ingress_rules = [
    {
      description = "HTTP"
      port        = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS"
      port        = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "SSH"
      port        = 22
      protocol    = "tcp"
      cidr_blocks = [var.admin_cidr]
    }
  ]

  egress_rules = [
    {
      description = "Todo el tráfico"
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}

resource "aws_security_group" "web" {
  name        = "web-security-group"
  description = "Grupo de seguridad para servidores web"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = local.ingress_rules
    content {
      description = ingress.value.description
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  dynamic "egress" {
    for_each = local.egress_rules
    content {
      description = egress.value.description
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
    }
  }

  tags = {
    Name = "web-sg"
  }
}

# Grupo de Seguridad de Application Load Balancer
resource "aws_security_group" "alb" {
  name        = "alb-security-group"
  description = "Grupo de seguridad para Application Load Balancer"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = {
      http  = { port = 80, protocol = "tcp" }
      https = { port = 443, protocol = "tcp" }
    }
    content {
      description = upper(ingress.key)
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

  egress {
    description     = "A servidores web"
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = [aws_security_group.web.id]
  }

  tags = {
    Name = "alb-sg"
  }
}

Creación de Módulos Reutilizables

Estructura de Módulos

# Estructura de proyecto con módulos
terraform-infrastructure/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── README.md
│   ├── compute/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── README.md
│   └── database/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── README.md
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── development/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── README.md

Ejemplo de Módulo VPC

# modules/vpc/main.tf
variable "vpc_name" {
  description = "Nombre de la VPC"
  type        = string
}

variable "vpc_cidr" {
  description = "Bloque CIDR para VPC"
  type        = string
}

variable "availability_zones" {
  description = "Lista de zonas de disponibilidad"
  type        = list(string)
}

variable "public_subnet_cidrs" {
  description = "Bloques CIDR para subnets públicas"
  type        = list(string)
}

variable "private_subnet_cidrs" {
  description = "Bloques CIDR para subnets privadas"
  type        = list(string)
}

variable "enable_nat_gateway" {
  description = "Habilitar NAT Gateway para subnets privadas"
  type        = bool
  default     = true
}

variable "single_nat_gateway" {
  description = "Usar un solo NAT Gateway para todas las AZs"
  type        = bool
  default     = false
}

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

# VPC
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(
    var.tags,
    {
      Name = var.vpc_name
    }
  )
}

# Internet Gateway
resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-igw"
    }
  )
}

# Subnets Públicas
resource "aws_subnet" "public" {
  count                   = length(var.public_subnet_cidrs)
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-public-${var.availability_zones[count.index]}"
      Tier = "Public"
    }
  )
}

# Subnets Privadas
resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-private-${var.availability_zones[count.index]}"
      Tier = "Private"
    }
  )
}

# IPs Elásticas para NAT Gateways
resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  domain = "vpc"

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-nat-eip-${count.index + 1}"
    }
  )

  depends_on = [aws_internet_gateway.this]
}

# NAT Gateways
resource "aws_nat_gateway" "this" {
  count         = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-nat-${count.index + 1}"
    }
  )

  depends_on = [aws_internet_gateway.this]
}

# Tabla de Rutas Pública
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(
    var.tags,
    {
      Name = "${var.vpc_name}-public-rt"
    }
  )
}

# Asociaciones de Tabla de Rutas Públicas
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
}

# Tablas de Rutas Privadas
resource "aws_route_table" "private" {
  count  = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  vpc_id = aws_vpc.this.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.this[count.index].id
  }

  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-private-rt-${count.index + 1}"
    }
  )
}

# Asociaciones de Tabla de Rutas Privadas
resource "aws_route_table_association" "private" {
  count          = length(aws_subnet.private)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = var.single_nat_gateway ? aws_route_table.private[0].id : aws_route_table.private[count.index].id
}

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

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

output "public_subnet_ids" {
  description = "IDs de subnets públicas"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs de subnets privadas"
  value       = aws_subnet.private[*].id
}

output "nat_gateway_ids" {
  description = "IDs de NAT Gateways"
  value       = aws_nat_gateway.this[*].id
}

output "internet_gateway_id" {
  description = "ID de Internet Gateway"
  value       = aws_internet_gateway.this.id
}

Usando Módulos

# environments/production/main.tf
terraform {
  required_version = ">= 1.0"

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

  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = "production"
      ManagedBy   = "Terraform"
      Project     = var.project_name
    }
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

# Usar módulo VPC
module "vpc" {
  source = "../../modules/vpc"

  vpc_name             = "${var.project_name}-production"
  vpc_cidr             = "10.0.0.0/16"
  availability_zones   = slice(data.aws_availability_zones.available.names, 0, 3)
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  enable_nat_gateway   = true
  single_nat_gateway   = false

  tags = {
    Owner = "Equipo DevOps"
  }
}

# Usar módulo de cómputo
module "web_servers" {
  source = "../../modules/compute"

  cluster_name       = "${var.project_name}-web"
  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.private_subnet_ids
  instance_type      = "t3.medium"
  min_size           = 3
  max_size           = 10
  desired_capacity   = 3
  key_name           = var.key_name
  ami_id             = data.aws_ami.ubuntu.id
  user_data          = file("${path.module}/user-data.sh")

  security_group_rules = {
    http = {
      type        = "ingress"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

# Usar módulo de base de datos
module "database" {
  source = "../../modules/database"

  identifier          = "${var.project_name}-db"
  engine              = "postgres"
  engine_version      = "15.4"
  instance_class      = "db.t3.large"
  allocated_storage   = 100
  database_name       = var.database_name
  master_username     = var.database_username
  master_password     = var.database_password
  vpc_id              = module.vpc.vpc_id
  subnet_ids          = module.vpc.private_subnet_ids
  multi_az            = true
  backup_retention    = 30
  skip_final_snapshot = false

  allowed_security_groups = [module.web_servers.security_group_id]
}

Gestión de Estado y Backends Remotos

Configuración de Backend S3

# backend.tf
terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
    kms_key_id     = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
  }
}

Crear Recursos de Backend

# bootstrap/backend-resources.tf
# Ejecutar una vez para crear infraestructura de backend

resource "aws_s3_bucket" "terraform_state" {
  bucket = "company-terraform-state"

  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name      = "Estado de Terraform"
    ManagedBy = "Terraform"
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_kms_key" "terraform" {
  description             = "Clave KMS para cifrado de estado de Terraform"
  deletion_window_in_days = 10
  enable_key_rotation     = true

  tags = {
    Name = "terraform-state-key"
  }
}

resource "aws_kms_alias" "terraform" {
  name          = "alias/terraform-state"
  target_key_id = aws_kms_key.terraform.key_id
}

resource "aws_dynamodb_table" "terraform_lock" {
  name           = "terraform-lock"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  server_side_encryption {
    enabled     = true
    kms_key_arn = aws_kms_key.terraform.arn
  }

  point_in_time_recovery {
    enabled = true
  }

  tags = {
    Name      = "Tabla de Bloqueo de Terraform"
    ManagedBy = "Terraform"
  }
}

Espacios de Trabajo para Gestión de Entornos

# Crear y gestionar espacios de trabajo
terraform workspace new production
terraform workspace new staging
terraform workspace new development

# Listar espacios de trabajo
terraform workspace list

# Cambiar espacio de trabajo
terraform workspace select production

# Mostrar espacio de trabajo actual
terraform workspace show

Configuración Consciente de Espacios de Trabajo

# main.tf
locals {
  environment = terraform.workspace

  config = {
    production = {
      instance_type = "t3.large"
      min_size      = 3
      max_size      = 10
      db_instance   = "db.r5.xlarge"
    }
    staging = {
      instance_type = "t3.medium"
      min_size      = 2
      max_size      = 5
      db_instance   = "db.t3.large"
    }
    development = {
      instance_type = "t3.small"
      min_size      = 1
      max_size      = 2
      db_instance   = "db.t3.medium"
    }
  }

  current_config = local.config[local.environment]
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.current_config.instance_type

  tags = {
    Name        = "${var.project_name}-${local.environment}"
    Environment = local.environment
  }
}

Ejemplo de Despliegue Multi-Entorno

Este ejemplo completo demuestra una estructura de despliegue completa lista para producción.

Debido a restricciones de espacio, he proporcionado los patrones esenciales. Permíteme crear una guía completa de automatización a continuación:

# Desplegar a diferentes entornos
cd environments/production
terraform init
terraform workspace select production
terraform plan
terraform apply

cd ../staging
terraform init
terraform workspace select staging
terraform plan
terraform apply

Mejores Prácticas

1. Versionado de Módulos

module "vpc" {
  source  = "git::https://github.com/company/terraform-modules.git//vpc?ref=v2.1.0"
  # ... configuración ...
}

2. Bloqueo de Estado Remoto

Siempre usa bloqueo de estado para prevenir modificaciones concurrentes.

3. Archivos de Estado Separados

Usa diferentes archivos de estado para diferentes entornos o componentes.

4. Automatización y CI/CD

Integra Terraform con pipelines de CI/CD para despliegues automatizados.

Conclusión

Las técnicas avanzadas de Terraform habilitan aprovisionamiento de infraestructura robusto y escalable. Domina módulos, gestión de estado y patrones de despliegue para construir infrastructure-as-code de nivel de producción.

Puntos clave:

  • Usa módulos para reutilización
  • Implementa gestión apropiada de estado
  • Aprovecha espacios de trabajo para entornos
  • Sigue mejores prácticas de seguridad
  • Automatiza despliegues con CI/CD