# ============================================================================= # VPC Module — Production-Grade AWS VPC with Security-First Design # ============================================================================= # Features: # - Multi-AZ deployment (3 AZs minimum) # - Private subnets with NAT Gateway egress # - Public subnets for ALB/NLB only # - VPC Flow Logs → S3 (encrypted) + CloudWatch # - IPv6 dual-stack ready # - Network ACLs (default deny) # - Dedicated subnets for EKS, RDS, and workloads # ============================================================================= terraform { required_version = ">= 1.7.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } resource "aws_vpc" "this" { cidr_block = var.cidr_block enable_dns_support = true enable_dns_hostnames = true enable_network_address_usage_metrics = true assign_generated_ipv6_cidr_block = var.enable_ipv6 tags = merge(var.tags, { Name = "${var.name}-vpc" }) } # ---------- Internet Gateway ---------- resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id tags = merge(var.tags, { Name = "${var.name}-igw" }) } # ---------- Elastic IPs for NAT Gateways ---------- resource "aws_eip" "nat" { count = var.nat_gateway_count domain = "vpc" tags = merge(var.tags, { Name = "${var.name}-nat-eip-${count.index + 1}" }) } # ---------- Public Subnets (ALB/NLB only) ---------- 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 = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = false # Never auto-assign public IPs tags = merge(var.tags, { Name = "${var.name}-public-${data.aws_availability_zones.available.names[count.index]}" Tier = "public" }) } # ---------- Private Subnets (EKS Nodes) ---------- 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 = data.aws_availability_zones.available.names[count.index] tags = merge(var.tags, { Name = "${var.name}-private-${data.aws_availability_zones.available.names[count.index]}" Tier = "private" "kubernetes.io/role/internal-elb" = "1" "kubernetes.io/cluster/${var.eks_cluster_name}" = "shared" }) } # ---------- Database Subnets (RDS) ---------- resource "aws_subnet" "database" { count = length(var.database_subnet_cidrs) vpc_id = aws_vpc.this.id cidr_block = var.database_subnet_cidrs[count.index] availability_zone = data.aws_availability_zones.available.names[count.index] tags = merge(var.tags, { Name = "${var.name}-database-${data.aws_availability_zones.available.names[count.index]}" Tier = "database" }) } # ---------- NAT Gateways ---------- resource "aws_nat_gateway" "this" { count = var.nat_gateway_count allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id tags = merge(var.tags, { Name = "${var.name}-nat-${count.index + 1}" }) } # ---------- Route Tables ---------- 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 } dynamic "route" { for_each = var.enable_ipv6 ? [1] : [] content { ipv6_cidr_block = "::/0" gateway_id = aws_internet_gateway.this.id } } tags = merge(var.tags, { Name = "${var.name}-public-rt" }) } resource "aws_route_table" "private" { count = var.nat_gateway_count 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.name}-private-rt-${count.index + 1}" }) } # ---------- Route Table Associations ---------- 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 } resource "aws_route_table_association" "private" { count = length(aws_subnet.private) subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private[count.index % var.nat_gateway_count].id } # ---------- VPC Flow Logs → S3 ---------- resource "aws_flow_log" "s3" { vpc_id = aws_vpc.this.id traffic_type = "ALL" destination_type = "s3" destination_arn = var.flow_log_s3_arn tags = merge(var.tags, { Name = "${var.name}-flow-log-s3" }) } # ---------- VPC Flow Logs → CloudWatch ---------- resource "aws_cloudwatch_log_group" "flow_log" { name = "/aws/vpc/${var.name}/flow-log" retention_in_days = var.flow_log_retention_days tags = merge(var.tags, { Name = "${var.name}-flow-log-cw" }) } resource "aws_iam_role" "flow_log" { name = "${var.name}-vpc-flow-log-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" "flow_log" { name = "${var.name}-vpc-flow-log-policy" role = aws_iam_role.flow_log.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams" ] Resource = "*" } ] }) } resource "aws_flow_log" "cloudwatch" { vpc_id = aws_vpc.this.id traffic_type = "ALL" iam_role_arn = aws_iam_role.flow_log.arn log_destination = aws_cloudwatch_log_group.flow_log.arn tags = merge(var.tags, { Name = "${var.name}-flow-log-cw" }) } # ---------- Default Security Group — Deny All ---------- resource "aws_default_security_group" "this" { vpc_id = aws_vpc.this.id # No ingress/egress rules = deny all tags = merge(var.tags, { Name = "${var.name}-default-sg-locked" }) } # ---------- Default Network ACL — Deny All ---------- resource "aws_default_network_acl" "this" { default_network_acl_id = aws_vpc.this.default_network_acl_id # No rules = default deny tags = merge(var.tags, { Name = "${var.name}-default-nacl-locked" }) } # ---------- Data Sources ---------- data "aws_availability_zones" "available" { state = "available" }