In DevOps, setting up a secure network foundation is the absolute first step. On AWS, this means building a Virtual Private Cloud (VPC). Doing this manually through the AWS Console is slow, error-prone, and impossible to track. That's where Infrastructure as Code (IaC) comes in.
In this guide, we will write a production-ready, highly available, multi-Availability Zone (AZ) AWS VPC using Terraform. We'll set up public subnets for our load balancers, private subnets for our application servers, and automate route tables and NAT Gateways.
Note: We will configure this layout across 2 Availability Zones to ensure high availability while keeping cost efficiency in mind by sharing NAT Gateways.
The Architecture
Before writing code, it's vital to understand what we're building. Our VPC will have:
- A Classless Inter-Domain Routing (CIDR) block of
10.0.0.0/16. - 2 Public Subnets (one in each AZ) which route internet traffic through an Internet Gateway.
- 2 Private Subnets (one in each AZ) which route outbound-only traffic through a NAT Gateway.
- Properly associated Route Tables to secure private environments from direct ingress.
Step 1: Provider Setup
Create a file named main.tf. First, we define our AWS provider and specify our working region.
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" }
Step 2: Define the VPC and Gateways
Next, we provision the main VPC container and create the Internet Gateway (IGW) for public routing. We'll also allocate an Elastic IP (EIP) and set up a NAT Gateway so that instances in our private subnets can connect to the internet (for updates, patches, etc.) without exposing themselves to incoming connections.
# 1. Main VPC resource resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "production-vpc" } } # 2. Internet Gateway for public traffic resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id tags = { Name = "production-igw" } } # 3. Elastic IP for the NAT Gateway resource "aws_eip" "nat" { domain = "vpc" depends_on = [aws_internet_gateway.igw] } # 4. NAT Gateway (placed in public subnet 1) resource "aws_nat_gateway" "nat" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public_1.id tags = { Name = "production-nat-gw" } }
Step 3: Creating Subnets Across AZs
We divide our 10.0.0.0/16 network block into smaller chunks using subnet CIDR blocks. We place subnets in both us-east-1a and us-east-1b for high availability.
# Public Subnet 1 (AZ A) resource "aws_subnet" "public_1" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-east-1a" map_public_ip_on_launch = true tags = { Name = "public-subnet-1a" } } # Public Subnet 2 (AZ B) resource "aws_subnet" "public_2" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "us-east-1b" map_public_ip_on_launch = true tags = { Name = "public-subnet-1b" } } # Private Subnet 1 (AZ A) resource "aws_subnet" "private_1" { vpc_id = aws_vpc.main.id cidr_block = "10.0.10.0/24" availability_zone = "us-east-1a" tags = { Name = "private-subnet-1a" } } # Private Subnet 2 (AZ B) resource "aws_subnet" "private_2" { vpc_id = aws_vpc.main.id cidr_block = "10.0.11.0/24" availability_zone = "us-east-1b" tags = { Name = "private-subnet-1b" } }
Step 4: Route Tables and Associations
Now we define how network packages flow. The public route table sends all external traffic (0.0.0.0/0) to the IGW. The private route table routes outgoing traffic to the NAT Gateway.
# 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.igw.id } tags = { Name = "public-rt" } } # Private Route Table resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.nat.id } tags = { Name = "private-rt" } } # Route Table Associations resource "aws_route_table_association" "pub_1" { subnet_id = aws_subnet.public_1.id route_table_id = aws_route_table.public.id } resource "aws_route_table_association" "pub_2" { subnet_id = aws_subnet.public_2.id route_table_id = aws_route_table.public.id } resource "aws_route_table_association" "priv_1" { subnet_id = aws_subnet.private_1.id route_table_id = aws_route_table.private.id } resource "aws_route_table_association" "priv_2" { subnet_id = aws_subnet.private_2.id route_table_id = aws_route_table.private.id }
Step 5: Initialization and Execution
With the code ready, let's run our terminal execution. Fire up your terminal inside your project directory and follow these commands:
Conclusion
Congratulations! You've just codified your network infrastructure using Terraform. Your architecture is highly available, isolated, and secure across multiple AZs. By versioning this code in Git, you now have a clean system audit log of your infrastructure topology.
In the next guide, we'll configure security groups and spin up containerized ECS tasks inside this exact private network.