StackPractices
intermediate Por Mathias Paulenko

Provisiona una VPC de AWS con Terraform

Como usar Terraform para provisionar una VPC de AWS lista para produccion con subredes publicas y privadas, NAT gateways y security groups

Una VPC bien estructurada es la fundacion de infraestructura cloud segura. Terraform te permite definir toda la topologia de red — subredes, ruteo, gateways y security groups — como codigo versionado que puede recrearse identicamente entre ambientes.

Cuando Usar Esto

  • Necesitas infraestructura de red repetible y versionada entre dev/staging/prod. Consulta Git Workflow para infraestructura versionada.
  • Las aplicaciones requieren recursos tanto publicos como internos. Consulta AWS ECS Fargate para despliegue de contenedores.
  • Quieres enforcear segmentacion de red entre diferentes capas. Consulta Load Balancing HAProxy para separación de capas.

Requisitos Previos

  • AWS CLI configurado con credenciales apropiadas
  • Terraform 1.5+ instalado

Solucion

1. VPC y Subredes

# vpc.tf
locals {
  azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

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

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

resource "aws_subnet" "public" {
  count                   = length(local.azs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone       = local.azs[count.index]
  map_public_ip_on_launch = true

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

resource "aws_subnet" "private" {
  count             = length(local.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 100)
  availability_zone = local.azs[count.index]

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

2. Gateways de Internet y NAT

# gateways.tf
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "main-igw" }
}

resource "aws_eip" "nat" {
  count  = length(local.azs)
  domain = "vpc"
  tags   = { Name = "nat-eip-${count.index + 1}" }
}

resource "aws_nat_gateway" "main" {
  count         = length(local.azs)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags          = { Name = "nat-gateway-${count.index + 1}" }
}

3. Tablas de Ruteo

# routing.tf
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-rt" }
}

resource "aws_route_table_association" "public" {
  count          = length(local.azs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table" "private" {
  count  = length(local.azs)
  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-rt-${count.index + 1}" }
}

resource "aws_route_table_association" "private" {
  count          = length(local.azs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

4. Security Groups

# security.tf
resource "aws_security_group" "web" {
  name_prefix = "web-"
  vpc_id      = aws_vpc.main.id
  description = "Permitir trafico HTTP y HTTPS"

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

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

resource "aws_security_group" "database" {
  name_prefix = "db-"
  vpc_id      = aws_vpc.main.id
  description = "Permitir acceso a base de datos solo desde capa web"

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }

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

Como Funciona

  1. VPC define el espacio de direcciones IP privadas para todos los recursos
  2. Subredes Publicas rutean trafico a traves del Internet Gateway para acceso externo
  3. Subredes Privadas rutean trafico saliente a traves de NAT Gateways por seguridad
  4. Tablas de Ruteo controlan direccion de trafico por subred
  5. Security Groups actuan como firewalls stateful a nivel de instancia

Consideraciones de Produccion

  • Usa Terraform workspaces o archivos de estado separados para cada ambiente
  • Habilita VPC Flow Logs a CloudWatch para auditoria de trafico de red
  • Coloca bases de datos y servicios internos solo en subredes privadas
  • Usa AWS Network Firewall o Security Groups para defensa en profundidad

Errores Comunes

  • Olvidar habilitar map_public_ip_on_launch para instancias en subredes publicas
  • Colocar NAT Gateways en subredes privadas en lugar de publicas
  • Usar bloques CIDR demasiado permisivos como 0.0.0.0/0 en ingress de security groups

Tips de Rendimiento

  1. Usa Gateway endpoints para S3 y DynamoDB. Sin charge por GB, a diferencia de Interface endpoints:
# Gateway endpoint: gratis, sin costo por GB
vpc_endpoint_type = "Gateway"

# Interface endpoint: $7/mes + por GB
vpc_endpoint_type = "Interface"
  1. Coloca NAT Gateways en cada AZ. Trafico NAT cross-AZ incursa costos dobles de transferencia:
# Un NAT por AZ evita cargos cross-AZ
count = length(var.availability_zones)
  1. Usa aws_ec2_transit_gateway para multi-VPC. Peering escala como O(n²) conexiones; Transit Gateway es O(n):
resource "aws_ec2_transit_gateway" "main" {
  description = "Platform transit gateway"
  tags        = local.common_tags
}

Preguntas frecuentes

5. VPC Flow Logs
# flow_logs.tf
resource "aws_cloudwatch_log_group" "vpc_flow" {
  name              = "/aws/vpc/flow-logs"
  retention_in_days = 30
}

resource "aws_iam_role" "vpc_flow" {
  name = "vpc-flow-logs-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = { Service = "vpc-flow-logs.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "vpc_flow" {
  name = "vpc-flow-logs-policy"
  role = aws_iam_role.vpc_flow.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogGroups"
      ]
      Resource = "*"
    }]
  })
}

resource "aws_flow_log" "main" {
  vpc_id              = aws_vpc.main.id
  iam_role_arn        = aws_iam_role.vpc_flow.arn
  log_destination     = aws_cloudwatch_log_group.vpc_flow.arn
  log_destination_type = "cloud-watch-logs"
  traffic_type        = "ALL"
}
6. VPC Endpoints para Servicios AWS
# endpoints.tf
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [for rt in aws_route_table.private : rt.id]
}

resource "aws_vpc_endpoint" "secrets_manager" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.secretsmanager"
  vpc_endpoint_type = "Interface"
  subnet_ids        = [for s in aws_subnet.private : s.id]
  security_group_ids = [aws_security_group.database.id]
  private_dns_enabled = true
}
7. Network ACLs para Defensa en Profundidad
# nacls.tf
resource "aws_network_acl" "public" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [for s in aws_subnet.public : s.id]

  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }

  ingress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }

  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
}
8. Variables para Reusabilidad
# variables.tf
variable "environment" {
  type    = string
  default = "production"
}

variable "cidr_block" {
  type    = string
  default = "10.0.0.0/16"
}

variable "availability_zones" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "single_nat_gateway" {
  type    = bool
  default = false
}

# NAT condicional: uno por AZ para prod, uno solo para dev
resource "aws_nat_gateway" "main" {
  count = var.single_nat_gateway ? 1 : length(var.availability_zones)
  allocation_id = var.single_nat_gateway ? aws_eip.nat[0].id : aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
}