# ============================================================================= # AI Software Factory — Network (VPC, 2 AZs, public/private subnets, IGW, NAT) # # Internet # └── IGW # ├── public subnets (10.42.0.0/20, 10.42.16.0/20) # │ └── web ALB (+ NAT gateway EIP for outbound from private) # └── private subnets (10.42.32.0/20, 10.42.48.0/20) # └── app EC2 ASGs + RDS + ElastiCache + OpenSearch # # Private subnets have NO internet route; outbound (npm, apt, image pulls, # S3 via NAT) goes through the NAT gateway in the first public subnet. # ============================================================================= data "aws_availability_zones" "available" { state = "available" } locals { # Pin the first two AZs for deterministic subnet placement. azs = slice(data.aws_availability_zones.available.names, 0, local.az_count) } resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = local.names.vpc } } # --- Internet gateway -------------------------------------------------------- resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = { Name = local.names.igw } } # --- Public subnets ----------------------------------------------------------- resource "aws_subnet" "public" { count = local.az_count vpc_id = aws_vpc.main.id cidr_block = var.public_subnet_cidrs[count.index] availability_zone = local.azs[count.index] # Auto-assign public IPs so the ALB (and NAT) get IPs without extra config. map_public_ip_on_launch = true tags = { Name = "${local.names.vpc}-public-${local.azs[count.index]}" Tier = "public" } } # --- Private subnets ---------------------------------------------------------- resource "aws_subnet" "private" { count = local.az_count vpc_id = aws_vpc.main.id cidr_block = var.private_subnet_cidrs[count.index] availability_zone = local.azs[count.index] tags = { Name = "${local.names.vpc}-private-${local.azs[count.index]}" Tier = "private" } } # --- NAT gateway (outbound for private subnets) -------------------------------- resource "aws_eip" "nat" { domain = "vpc" tags = { Name = local.names.eip } } resource "aws_nat_gateway" "main" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public[0].id tags = { Name = local.names.nat } depends_on = [aws_internet_gateway.main] } # --- Route tables --------------------------------------------------------------- resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id tags = { Name = "${local.names.vpc}-public" } } resource "aws_route" "public_internet" { route_table_id = aws_route_table.public.id destination_cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main.id } resource "aws_route_table_association" "public" { count = local.az_count subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id } resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id tags = { Name = "${local.names.vpc}-private" } } resource "aws_route" "private_nat" { route_table_id = aws_route_table.private.id destination_cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.main.id } resource "aws_route_table_association" "private" { count = local.az_count subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private.id } # --- VPC endpoints (optional hardening, cheap, no NAT needed for these) -------- # Private access to S3 without leaving the VPC (and without NAT charges). # OpenSearch is placed in the VPC itself, so no endpoint needed for it. resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.aws_region}.s3" tags = { Name = "${local.names.vpc}-s3-endpoint" } }