VPC and Networking
AWS networking forms the foundation of every well-architected system, but designing VPCs that scale, perform well, and maintain security requires understanding both AWS networking concepts and Terraform patterns for managing complex network topologies. The decisions you make about CIDR blocks, subnet design, and connectivity patterns affect everything you’ll build on top.
This part covers the networking patterns that work well in production—from basic VPC design to complex multi-tier architectures with proper isolation and connectivity.
VPC Design Patterns
A well-designed VPC balances security, scalability, and operational simplicity:
# VPC with carefully planned CIDR blocks
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.name_prefix}-vpc"
Type = "networking"
}
}
# Internet Gateway for public connectivity
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name_prefix}-igw"
}
}
# Data source for availability zones
data "aws_availability_zones" "available" {
state = "available"
}
# Calculate subnet CIDRs automatically
locals {
az_count = min(length(data.aws_availability_zones.available.names), 3)
# Public subnets: 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
public_subnet_cidrs = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 1)
]
# Private subnets: 10.0.11.0/24, 10.0.12.0/24, 10.0.13.0/24
private_subnet_cidrs = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 11)
]
# Database subnets: 10.0.21.0/24, 10.0.22.0/24, 10.0.23.0/24
database_subnet_cidrs = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 21)
]
}
Multi-Tier Subnet Architecture
Separate tiers provide security isolation and traffic control:
# Public subnets for load balancers and NAT gateways
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.name_prefix}-public-${count.index + 1}"
Type = "public"
Tier = "public"
}
}
# Private subnets for application servers
resource "aws_subnet" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.private_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.name_prefix}-private-${count.index + 1}"
Type = "private"
Tier = "application"
}
}
# Database subnets with additional isolation
resource "aws_subnet" "database" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.database_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.name_prefix}-database-${count.index + 1}"
Type = "private"
Tier = "database"
}
}
# Database subnet group for RDS
resource "aws_db_subnet_group" "main" {
name = "${var.name_prefix}-db-subnet-group"
subnet_ids = aws_subnet.database[*].id
tags = {
Name = "${var.name_prefix}-db-subnet-group"
}
}
NAT Gateway Configuration
NAT Gateways provide secure internet access for private subnets:
# Elastic IPs for NAT Gateways
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? local.az_count : 0
domain = "vpc"
depends_on = [aws_internet_gateway.main]
tags = {
Name = "${var.name_prefix}-nat-eip-${count.index + 1}"
}
}
# NAT Gateways in public subnets
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? local.az_count : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.name_prefix}-nat-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
Route Table Management
Proper routing ensures traffic flows correctly between tiers:
# Public route table
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 = "${var.name_prefix}-public-rt"
Type = "public"
}
}
# Associate public subnets with public route table
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
}
# Private route tables (one per AZ for NAT Gateway redundancy)
resource "aws_route_table" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
tags = {
Name = "${var.name_prefix}-private-rt-${count.index + 1}"
Type = "private"
}
}
# Associate private subnets with their route tables
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[count.index].id
}
# Database route tables (isolated from internet)
resource "aws_route_table" "database" {
count = local.az_count
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name_prefix}-database-rt-${count.index + 1}"
Type = "database"
}
}
resource "aws_route_table_association" "database" {
count = local.az_count
subnet_id = aws_subnet.database[count.index].id
route_table_id = aws_route_table.database[count.index].id
}
Security Group Patterns
Security groups provide stateful firewall rules at the instance level:
# Web tier security group
resource "aws_security_group" "web" {
name_prefix = "${var.name_prefix}-web-"
vpc_id = aws_vpc.main.id
description = "Security group for web tier"
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
ingress {
description = "HTTPS from ALB"
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-web-sg"
Tier = "web"
}
}
# Application Load Balancer security group
resource "aws_security_group" "alb" {
name_prefix = "${var.name_prefix}-alb-"
vpc_id = aws_vpc.main.id
description = "Security group for Application Load Balancer"
ingress {
description = "HTTP from internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "HTTP to web tier"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
tags = {
Name = "${var.name_prefix}-alb-sg"
Tier = "load-balancer"
}
}
# Database security group
resource "aws_security_group" "database" {
name_prefix = "${var.name_prefix}-db-"
vpc_id = aws_vpc.main.id
description = "Security group for database tier"
ingress {
description = "MySQL/Aurora from application tier"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
tags = {
Name = "${var.name_prefix}-db-sg"
Tier = "database"
}
}
VPC Endpoints for AWS Services
VPC endpoints provide private connectivity to AWS services:
# S3 VPC Endpoint (Gateway endpoint)
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.s3"
route_table_ids = concat(
[aws_route_table.public.id],
aws_route_table.private[*].id
)
tags = {
Name = "${var.name_prefix}-s3-endpoint"
}
}
# EC2 VPC Endpoint (Interface endpoint)
resource "aws_vpc_endpoint" "ec2" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.ec2"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "${var.name_prefix}-ec2-endpoint"
}
}
# Security group for VPC endpoints
resource "aws_security_group" "vpc_endpoints" {
name_prefix = "${var.name_prefix}-vpc-endpoints-"
vpc_id = aws_vpc.main.id
description = "Security group for VPC endpoints"
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
tags = {
Name = "${var.name_prefix}-vpc-endpoints-sg"
}
}
Network ACLs for Additional Security
Network ACLs provide subnet-level security controls:
# Database tier Network ACL
resource "aws_network_acl" "database" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.database[*].id
# Allow inbound MySQL from private subnets
ingress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "10.0.0.0/8"
from_port = 3306
to_port = 3306
}
# Allow return traffic
ingress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# Allow outbound responses
egress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
tags = {
Name = "${var.name_prefix}-database-nacl"
Tier = "database"
}
}
Outputs for Network Resources
Expose network information for use by other configurations:
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = aws_subnet.private[*].id
}
output "database_subnet_ids" {
description = "IDs of the database subnets"
value = aws_subnet.database[*].id
}
output "database_subnet_group_name" {
description = "Name of the database subnet group"
value = aws_db_subnet_group.main.name
}
output "security_group_ids" {
description = "Security group IDs by tier"
value = {
web = aws_security_group.web.id
alb = aws_security_group.alb.id
database = aws_security_group.database.id
}
}
What’s Next
Well-designed networking provides the foundation for secure, scalable AWS architectures. With VPCs, subnets, and security groups properly configured, you’re ready to tackle AWS’s most complex topic: Identity and Access Management.
In the next part, we’ll explore IAM patterns that provide least-privilege access, enable cross-account workflows, and automate security controls across your AWS infrastructure.