Skip to main content

Module 11, AWS Networking with Terraform

The 3-Tier VPC Every Engineer Must Know

VPC (10.0.0.0/16)
├── Public Subnet A (10.0.1.0/24), AZ-A → Internet Gateway
├── Public Subnet B (10.0.2.0/24), AZ-B → Internet Gateway
├── Private App Subnet A (10.0.11.0/24) → NAT Gateway
├── Private App Subnet B (10.0.12.0/24) → NAT Gateway
├── Private Data Subnet A (10.0.21.0/24) → No internet
└── Private Data Subnet B (10.0.22.0/24) → No internet

Full VPC in Terraform

resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, { Name = "${local.name_prefix}-vpc" })
}

resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, { Name = "${local.name_prefix}-igw" })
}

# Public subnets, dynamic CIDRs using cidrsubnet()
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 1)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${var.availability_zones[count.index]}"
Tier = "public"
})
}

# NAT Gateways, one per AZ for high availability
resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"
}

resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
depends_on = [aws_internet_gateway.main]
}

cidrsubnet, Dynamic CIDR Calculation

# cidrsubnet(prefix, newbits, netnum)
# cidrsubnet("10.0.0.0/16", 8, 1) = "10.0.1.0/24"
# cidrsubnet("10.0.0.0/16", 8, 2) = "10.0.2.0/24"
# cidrsubnet("10.0.0.0/16", 8, 11) = "10.0.11.0/24"

resource "aws_subnet" "private_app" {
count = length(var.availability_zones)
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 11)
}

Security Groups

resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb"
vpc_id = aws_vpc.main.id

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

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

# App security group, only ALB can reach it
resource "aws_security_group" "app" {
name = "${local.name_prefix}-app"
vpc_id = aws_vpc.main.id

ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.alb.id] # ALB only
}
}

Knowledge Check

  1. Why do we create one NAT Gateway per AZ rather than one shared NAT Gateway?
  2. What does cidrsubnet("10.0.0.0/16", 8, 1) return?
  3. What is the difference between an Internet Gateway and a NAT Gateway?
  4. Why should the database security group only accept traffic from the app security group?
  5. What does depends_on = [aws_internet_gateway.main] do on the NAT Gateway?
Exam Mapping
  • AWS DevOps Pro Domain 6: High Availability and Fault Tolerance