# VPC with public (ALB-only) and private (Fargate + RDS) subnets. # Egress to AWS APIs flows through interface VPC endpoints; non-AWS internet # egress (e.g. the CMS ACCESS API) is optional via an egress-only NAT gateway # (var.enable_egress_nat), which is cheaper than a full NAT but single-AZ. resource "aws_vpc" "this" { cidr_block = var.cidr_block enable_dns_support = true enable_dns_hostnames = true tags = merge(var.tags, { Name = "${var.name}-vpc" }) } data "aws_availability_zones" "available" { state = "available" } locals { azs = slice(data.aws_availability_zones.available.names, 0, var.az_count) pub_cidr = [for i in range(var.az_count) : cidrsubnet(var.cidr_block, 8, i)] priv_cidr = [for i in range(var.az_count) : cidrsubnet(var.cidr_block, 8, i + 128)] } resource "aws_subnet" "public" { count = var.az_count vpc_id = aws_vpc.this.id cidr_block = local.pub_cidr[count.index] availability_zone = local.azs[count.index] tags = merge(var.tags, { Name = "${var.name}-public-${local.azs[count.index]}" }) } resource "aws_subnet" "private" { count = var.az_count vpc_id = aws_vpc.this.id cidr_block = local.priv_cidr[count.index] availability_zone = local.azs[count.index] tags = merge(var.tags, { Name = "${var.name}-private-${local.azs[count.index]}" }) } resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id tags = merge(var.tags, { Name = "${var.name}-igw" }) } 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.name}-rt-public" }) } resource "aws_route_table_association" "public" { count = var.az_count subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id } # Single-AZ egress-only NAT: cheaper than a full NAT gateway, and the only # non-AWS-API egress needed is outbound HTTPS from Fargate tasks. Interface # endpoint routes (s3/kms/sqs/...) are more specific, so AWS API traffic keeps # using them instead of this default route. resource "aws_egress_only_internet_gateway" "nat" { count = var.enable_egress_nat ? 1 : 0 vpc_id = aws_vpc.this.id tags = merge(var.tags, { Name = "${var.name}-egress-nat" }) } # Private subnets: AWS API access via VPC endpoints; default route to the # egress-only NAT only when enabled. resource "aws_route_table" "private" { vpc_id = aws_vpc.this.id dynamic "route" { for_each = var.enable_egress_nat ? [1] : [] content { cidr_block = "0.0.0.0/0" gateway_id = aws_egress_only_internet_gateway.nat[0].id } } tags = merge(var.tags, { Name = "${var.name}-rt-private" }) } resource "aws_route_table_association" "private" { count = var.az_count subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private.id }