AWS

Complete AWS VPC Setup with Terraform: Subnets, NAT, Security Groups

By CloudFormation Team ยท 2025-06-08 ยท 11 min read

Why VPC Design Matters

The VPC is the foundation of every AWS deployment. A poorly designed VPC creates security risks, limits scalability, and makes future changes painful. Getting it right from the start โ€” with Terraform โ€” means your network is reproducible, auditable, and consistent across all environments.

Production VPC Architecture

A production-grade VPC typically uses a three-tier architecture across three Availability Zones:

  • Public subnets โ€” load balancers, NAT gateways, bastion hosts
  • Private subnets โ€” application servers, container workloads
  • Database subnets โ€” RDS instances, ElastiCache, isolated from internet

Core VPC Terraform Resources

locals {
  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  db_subnets      = ["10.0.21.0/24", "10.0.22.0/24", "10.0.23.0/24"]
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = { Name = "${var.project}-vpc" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${var.project}-igw" }
}

# Public Subnets
resource "aws_subnet" "public" {
  count                   = length(local.azs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = local.public_subnets[count.index]
  availability_zone       = local.azs[count.index]
  map_public_ip_on_launch = true
  tags = { Name = "${var.project}-public-${count.index + 1}" }
}

# Private Subnets
resource "aws_subnet" "private" {
  count             = length(local.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = local.private_subnets[count.index]
  availability_zone = local.azs[count.index]
  tags = { Name = "${var.project}-private-${count.index + 1}" }
}

NAT Gateways

Private subnet resources need NAT gateways to reach the internet for software updates and API calls. For high availability, deploy one NAT gateway per AZ:

resource "aws_eip" "nat" {
  count  = length(local.azs)
  domain = "vpc"
  tags   = { Name = "${var.project}-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 = "${var.project}-nat-${count.index + 1}" }
  depends_on    = [aws_internet_gateway.main]
}

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
  }
}

Security Groups

resource "aws_security_group" "alb" {
  name        = "${var.project}-alb-sg"
  description = "Application Load Balancer"
  vpc_id      = aws_vpc.main.id

  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"]
  }
}

resource "aws_security_group" "app" {
  name        = "${var.project}-app-sg"
  description = "Application servers"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
}

VPC Flow Logs

Enable VPC Flow Logs for security analysis and troubleshooting:

resource "aws_flow_log" "main" {
  vpc_id          = aws_vpc.main.id
  traffic_type    = "ALL"
  iam_role_arn    = aws_iam_role.flow_logs.arn
  log_destination = aws_cloudwatch_log_group.flow_logs.arn
}

resource "aws_cloudwatch_log_group" "flow_logs" {
  name              = "/aws/vpc/flow-logs/${var.project}"
  retention_in_days = 30
}

VPC Endpoints (Save NAT Gateway Costs)

For S3 and DynamoDB access, use VPC Gateway Endpoints โ€” they're free and route traffic within the AWS network:

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
}

resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
}
VPC Gateway Endpoints for S3 and DynamoDB are completely free and eliminate NAT gateway charges for these services. For high-throughput S3 workloads this can save hundreds of dollars per month.

Outputs

output "vpc_id"              { value = aws_vpc.main.id }
output "public_subnet_ids"   { value = aws_subnet.public[*].id }
output "private_subnet_ids"  { value = aws_subnet.private[*].id }
output "nat_gateway_ips"     { value = aws_eip.nat[*].public_ip }

Try CloudFormation Free

Design infrastructure visually and generate production-ready Terraform in seconds โ€” 693 resources across 9 cloud providers.

๐Ÿš€ Open the Designer โ†’