DEV Community

Cover image for Building Your First VPC: AWS Networking with Terraform

Building Your First VPC: AWS Networking with Terraform

πŸ‘‹ Hey there, tech enthusiasts!

I'm Sarvar, a Cloud Architect with a passion for transforming complex technological challenges into elegant solutions. With extensive experience spanning Cloud Operations (AWS & Azure), Data Operations, Analytics, DevOps, and Generative AI, I've had the privilege of architecting solutions for global enterprises that drive real business impact. Through this article series, I'm excited to share practical insights, best practices, and hands-on experiences from my journey in the tech world. Whether you're a seasoned professional or just starting out, I aim to break down complex concepts into digestible pieces that you can apply in your projects.


🎯 Welcome Back!

Remember in Article 5 when you learned about variables and outputs? You created flexible, reusable S3 bucket configurations. That's great for storage, but what about networking?

Here's the reality: S3 buckets are public services. But what about:

  • EC2 instances that need network isolation?
  • Databases that should never be directly accessible from the internet?
  • Applications that need both public and private components?

That's where VPC (Virtual Private Cloud) comes in.

By the end of this article, you'll:

  • βœ… Understand VPC fundamentals (CIDR, subnets, routing)
  • βœ… Create a production-ready VPC with Terraform
  • βœ… Configure public and private subnets
  • βœ… Set up Internet Gateway and NAT Gateway
  • βœ… Master route tables and associations
  • βœ… Use data sources to fetch AWS information
  • βœ… Build cost-optimized VPC architecture

Time Required: 45 minutes (20 min read + 25 min practice)

Cost: ~$0.05 for testing (destroy immediately!)

Difficulty: Intermediate

⚠️ IMPORTANT: NAT Gateway costs $0.045/hour (~$32/month). We'll create it for learning, but destroy it immediately after testing!

Let's build real infrastructure! πŸš€


πŸ’” The Problem: Networking Confusion

The Manual Nightmare

Imagine this scenario (maybe you've lived it):

Your Task: "Set up a VPC for our new application"

You open AWS Console:

Step 1: Create VPC... wait, what's a CIDR block?
Step 2: Create subnets... public or private? What's the difference?
Step 3: Internet Gateway... where does this go?
Step 4: NAT Gateway... why do I need this?
Step 5: Route tables... which subnet goes where?
Step 6: *2 hours later* ... did I configure this correctly?
Step 7: *Application doesn't work* ... something's wrong with routing
Step 8: *Starts over* 😰
Enter fullscreen mode Exit fullscreen mode

Common Problems:

❌ Forgot route table associations - Subnets can't reach internet

❌ Wrong CIDR blocks - IP addresses overlap

❌ NAT Gateway in wrong subnet - Private instances can't update

❌ Inconsistent configuration - Resources don't work together

❌ Can't remember configuration - No documentation

❌ Takes hours to set up - Clicking through endless screens

Sound familiar? Let's fix this with Terraform.


🌐 What is a VPC? (Quick Theory)

Simple Definition

VPC (Virtual Private Cloud) is your own private network in AWS. Think of it like this:

  • Traditional Data Center: Physical building with servers, switches, routers
  • VPC: Virtual data center with virtual servers, virtual switches, virtual routers

Key Point: Your VPC is isolated from other AWS accounts. It's YOUR private network.

VPC Components (The Building Blocks)

VPC (Your Private Network)
β”œβ”€β”€ CIDR Block (IP Address Range)
β”œβ”€β”€ Subnets (Subdivisions of your network)
β”‚   β”œβ”€β”€ Public Subnets (Internet-accessible)
β”‚   └── Private Subnets (Internal only)
β”œβ”€β”€ Internet Gateway (Door to the internet)
β”œβ”€β”€ NAT Gateway (Private subnet's internet access)
└── Route Tables (GPS for network traffic)
Enter fullscreen mode Exit fullscreen mode

Understanding CIDR Blocks

CIDR = Classless Inter-Domain Routing (fancy name for IP address ranges)

Examples:

  • 10.0.0.0/16 = 65,536 IP addresses (10.0.0.0 to 10.0.255.255)
  • 10.0.1.0/24 = 256 IP addresses (10.0.1.0 to 10.0.1.255)
  • 10.0.2.0/24 = 256 IP addresses (10.0.2.0 to 10.0.2.255)

Simple Rule: Smaller number after / = More IP addresses

  • /16 = 65,536 IPs (big network)
  • /24 = 256 IPs (small network)
  • /32 = 1 IP (single host)

Don't worry! You don't need to be a networking expert. Just follow the patterns.

Public vs Private Subnets

Public Subnet:

  • βœ… Has route to Internet Gateway
  • βœ… Resources get public IP addresses
  • βœ… Directly accessible from internet
  • πŸ“ Use for: Web servers, load balancers, bastion hosts

Private Subnet:

  • βœ… Has route to NAT Gateway (not Internet Gateway)
  • ❌ No public IP addresses
  • ❌ Not directly accessible from internet
  • πŸ“ Use for: Databases, application servers, internal services

Why both? Security! Keep sensitive resources (databases) in private subnets.


πŸ—οΈ What We'll Build Today

Architecture Overview

VPC: 10.0.0.0/16 (65,536 IPs)
β”‚
β”œβ”€β”€ Public Subnet 1: 10.0.1.0/24 (256 IPs) - us-east-1a
β”‚   β”œβ”€β”€ Internet Gateway attached
β”‚   └── Route: 0.0.0.0/0 β†’ Internet Gateway
β”‚
β”œβ”€β”€ Private Subnet 1: 10.0.11.0/24 (256 IPs) - us-east-1a
β”‚   └── Route: 0.0.0.0/0 β†’ NAT Gateway
Enter fullscreen mode Exit fullscreen mode

Why this design?

  • βœ… Single-AZ
  • βœ… Public subnets = For load balancers, web servers
  • βœ… Private subnets = For databases, application servers
  • βœ… NAT Gateway = Private subnets can download updates
  • βœ… Production-ready = This is how real companies do it

πŸ“ Step-by-Step: Building the VPC

Step 1: Create Project Directory

mkdir -p ~/terraform-vpc-demo
cd ~/terraform-vpc-demo
Enter fullscreen mode Exit fullscreen mode

Step 2: Create variables.tf

Let's start with variables for flexibility:

nano variables.tf
Enter fullscreen mode Exit fullscreen mode

Copy this code:

# AWS Region
variable "aws_region" {
  description = "AWS region where resources will be created"
  type        = string
  default     = "us-east-1"
}

# Project Name
variable "project_name" {
  description = "Project name to be used in resource naming"
  type        = string
  default     = "terraform-vpc"
}

# Environment
variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"
}

# VPC CIDR Block
variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

# Public Subnet CIDR
variable "public_subnet_cidr" {
  description = "CIDR block for public subnet"
  type        = string
  default     = "10.0.1.0/24"
}

# Private Subnet CIDR
variable "private_subnet_cidr" {
  description = "CIDR block for private subnet"
  type        = string
  default     = "10.0.11.0/24"
}
Enter fullscreen mode Exit fullscreen mode

What's happening here?

  • All CIDR blocks are variables (easy to change)
  • Project name and environment for tagging
  • Default values provided (but can be overridden)
  • Cost-optimized: 1 public + 1 private subnet

Save the file (Ctrl+X, then Y, then Enter)

Step 3: Create terraform.tfvars

nano terraform.tfvars
Enter fullscreen mode Exit fullscreen mode

Copy this code:

# Default configuration for development environment
aws_region = "us-east-1"
project_name = "terraform-vpc"
environment = "dev"

# VPC Configuration
vpc_cidr = "10.0.0.0/16"

# Subnets
public_subnet_cidr = "10.0.1.0/24"
private_subnet_cidr = "10.0.11.0/24"
Enter fullscreen mode Exit fullscreen mode

Save the file

Step 4: Create main.tf (The Main Infrastructure)

This is where the magic happens! We'll build it piece by piece.

nano main.tf
Enter fullscreen mode Exit fullscreen mode

Copy this code:

# Terraform configuration
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider configuration
provider "aws" {
  region = var.aws_region
}

# Data source: Get available availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.project_name}-vpc"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name        = "${var.project_name}-igw"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Public Subnet 1
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_1_cidr
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.project_name}-public-subnet"
    Environment = var.environment
    Type        = "Public"
    ManagedBy   = "Terraform"
  }
}

# Private Subnet
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidr
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name        = "${var.project_name}-private-subnet"
    Environment = var.environment
    Type        = "Private"
    ManagedBy   = "Terraform"
  }
}

# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
    Name        = "${var.project_name}-nat-eip"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  depends_on = [aws_internet_gateway.main]
}

# NAT Gateway
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public_1.id

  tags = {
    Name        = "${var.project_name}-nat-gateway"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  depends_on = [aws_internet_gateway.main]
}

# 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.project_name}-public-rt"
    Environment = var.environment
    Type        = "Public"
    ManagedBy   = "Terraform"
  }
}

# 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.main.id
  }

  tags = {
    Name        = "${var.project_name}-private-rt"
    Environment = var.environment
    Type        = "Private"
    ManagedBy   = "Terraform"
  }
}

# Public Subnet Route Table Association
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Private Subnet Route Table Association
resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private.id
}

# Private Subnet Route Table Association
resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}
Enter fullscreen mode Exit fullscreen mode

Save the file

That's the complete VPC infrastructure! Let's break it down...


πŸ” Understanding the Code

1. Data Source (NEW CONCEPT!)

data "aws_availability_zones" "available" {
  state = "available"
}
Enter fullscreen mode Exit fullscreen mode

What's a data source?

  • Resources = Things Terraform creates (VPC, subnets, etc.)
  • Data Sources = Things Terraform reads from AWS (AZs, AMIs, etc.)

Why use it?

  • Don't hardcode availability zones (they differ by region)
  • Automatically get available AZs
  • Makes code portable across regions

How to use it:

availability_zone = data.aws_availability_zones.available.names[0]  # First AZ
availability_zone = data.aws_availability_zones.available.names[1]  # Second AZ
Enter fullscreen mode Exit fullscreen mode

2. VPC Resource

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true  # Important for EC2 instances
  enable_dns_support   = true  # Important for DNS resolution
}
Enter fullscreen mode Exit fullscreen mode

Key settings:

  • enable_dns_hostnames = EC2 instances get DNS names
  • enable_dns_support = DNS resolution works in VPC

Without these: Your instances can't resolve domain names!

3. Internet Gateway

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id  # Attach to our VPC
}
Enter fullscreen mode Exit fullscreen mode

What it does: Allows public subnets to access the internet

Think of it as: The front door of your VPC

4. Public Subnets

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_1_cidr
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true  # Auto-assign public IPs
}
Enter fullscreen mode Exit fullscreen mode

Key setting: map_public_ip_on_launch = true

  • EC2 instances in this subnet automatically get public IPs
  • Makes them accessible from internet

5. Private Subnets

resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_1_cidr
  availability_zone = data.aws_availability_zones.available.names[0]
  # NO map_public_ip_on_launch = Private!
}
Enter fullscreen mode Exit fullscreen mode

Notice: No map_public_ip_on_launch

  • Instances in private subnets don't get public IPs
  • More secure for databases and internal services

6. Elastic IP and NAT Gateway

# Elastic IP (static public IP)
resource "aws_eip" "nat" {
  domain = "vpc"
  depends_on = [aws_internet_gateway.main]
}

# NAT Gateway (in public subnet!)
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public_1.id  # Must be in public subnet
  depends_on = [aws_internet_gateway.main]
}
Enter fullscreen mode Exit fullscreen mode

What's NAT Gateway?

  • Allows private subnets to access internet (outbound only)
  • Private instances can download updates, call APIs
  • But internet can't initiate connections to private instances

Why Elastic IP?

  • NAT Gateway needs a static public IP
  • Elastic IP provides that

⚠️ COST WARNING: NAT Gateway = $0.045/hour (~$32/month)

7. Route Tables (The GPS)

Public Route Table:

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"  # All traffic
    gateway_id = aws_internet_gateway.main.id  # Goes to Internet Gateway
  }
}
Enter fullscreen mode Exit fullscreen mode

Translation: "Send all internet traffic (0.0.0.0/0) to the Internet Gateway"

Private Route Table:

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"  # All traffic
    nat_gateway_id = aws_nat_gateway.main.id  # Goes to NAT Gateway
  }
}
Enter fullscreen mode Exit fullscreen mode

Translation: "Send all internet traffic (0.0.0.0/0) to the NAT Gateway"

8. Route Table Associations

resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}
Enter fullscreen mode Exit fullscreen mode

What this does: Links subnet to route table

Think of it as: "Public Subnet 1, use the Public Route Table for routing"

Without this: Subnet doesn't know how to route traffic!


πŸ“€ Step 5: Create outputs.tf

nano outputs.tf
Enter fullscreen mode Exit fullscreen mode

Copy this code:

# VPC Outputs
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "vpc_cidr" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.main.cidr_block
}

# Subnet Outputs
output "public_subnet_1_id" {
  description = "ID of public subnet 1"
  value       = aws_subnet.public_1.id
}

output "public_subnet_2_id" {
  description = "ID of public subnet 2"
  value       = aws_subnet.public_2.id
}

output "private_subnet_1_id" {
  description = "ID of private subnet 1"
  value       = aws_subnet.private_1.id
}

output "private_subnet_2_id" {
  description = "ID of private subnet 2"
  value       = aws_subnet.private_2.id
}

# Gateway Outputs
output "internet_gateway_id" {
  description = "ID of the Internet Gateway"
  value       = aws_internet_gateway.main.id
}

output "nat_gateway_id" {
  description = "ID of the NAT Gateway"
  value       = aws_nat_gateway.main.id
}

output "nat_gateway_public_ip" {
  description = "Public IP of the NAT Gateway"
  value       = aws_eip.nat.public_ip
}

# Route Table Outputs
output "public_route_table_id" {
  description = "ID of the public route table"
  value       = aws_route_table.public.id
}

output "private_route_table_id" {
  description = "ID of the private route table"
  value       = aws_route_table.private.id
}

# Availability Zone
output "availability_zone" {
  description = "Availability zone used"
  value       = data.aws_availability_zones.available.names[0]
}
Enter fullscreen mode Exit fullscreen mode

Save the file

Why so many outputs?

  • You'll need these IDs in Article 7 (EC2 + Load Balancer)
  • Good documentation of what was created
  • Easy to reference in other Terraform modules

πŸš€ Let's Deploy the VPC!

Step 6: Initialize Terraform

terraform init
Enter fullscreen mode Exit fullscreen mode

Expected output:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.x.x...

Terraform has been successfully initialized!
Enter fullscreen mode Exit fullscreen mode

Step 7: Plan the Infrastructure

terraform plan
Enter fullscreen mode Exit fullscreen mode

What you'll see:

Terraform will perform the following actions:

  # aws_eip.nat will be created
  # aws_internet_gateway.main will be created
  # aws_nat_gateway.main will be created
  # aws_route_table.private will be created
  # aws_route_table.public will be created
  # aws_route_table_association.private_1 will be created
  # aws_route_table_association.private_2 will be created
  # aws_route_table_association.public_1 will be created
  # aws_route_table_association.public_2 will be created
  # aws_subnet.private_1 will be created
  # aws_subnet.private_2 will be created
  # aws_subnet.public_1 will be created
  # aws_subnet.public_2 will be created
  # aws_vpc.main will be created

Plan: 10 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode

10 resources! That's a complete VPC infrastructure from just one command.

Review the plan carefully:

  • βœ… VPC with correct CIDR (10.0.0.0/16)
  • βœ… 4 subnets with correct CIDRs
  • βœ… Subnets in different availability zones
  • βœ… Internet Gateway attached to VPC
  • βœ… NAT Gateway in public subnet
  • βœ… Route tables with correct routes

Step 8: Apply the Configuration

terraform apply
Enter fullscreen mode Exit fullscreen mode

Type yes when prompted.

Expected output:

aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-xxxxx]
aws_internet_gateway.main: Creating...
aws_subnet.public_1: Creating...
aws_subnet.public_2: Creating...
aws_subnet.private_1: Creating...
aws_subnet.private_2: Creating...
...
aws_nat_gateway.main: Creating...
aws_nat_gateway.main: Still creating... [10s elapsed]
aws_nat_gateway.main: Still creating... [20s elapsed]
...
aws_nat_gateway.main: Creation complete after 1m45s

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Outputs:

availability_zones = [
  "us-east-1a"
internet_gateway_id = "igw-xxxxx"
nat_gateway_id = "nat-xxxxx"
nat_gateway_public_ip = "54.xxx.xxx.xxx"
private_route_table_id = "rtb-xxxxx"
private_subnet_id = "subnet-xxxxx"
public_route_table_id = "rtb-xxxxx"
public_subnet_id = "subnet-xxxxx"
vpc_cidr = "10.0.0.0/16"
vpc_id = "vpc-xxxxx"
Enter fullscreen mode Exit fullscreen mode

⏱️ Time: Takes about 2-3 minutes (NAT Gateway is slow to create)

πŸŽ‰ Success! You just created production-grade networking infrastructure!

Step 9: Verify in AWS Console

Option 1: AWS CLI

# Get VPC ID
VPC_ID=$(terraform output -raw vpc_id)

# List all subnets
aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=$VPC_ID" \
  --query 'Subnets[*].[SubnetId,CidrBlock,AvailabilityZone,Tags[?Key==`Name`].Value|[0]]' \
  --output table
Enter fullscreen mode Exit fullscreen mode


# Check NAT Gateway
aws ec2 describe-nat-gateways \
  --filter "Name=vpc-id,Values=$VPC_ID" \
  --query 'NatGateways[*].[NatGatewayId,State,SubnetId]' \
  --output table
Enter fullscreen mode Exit fullscreen mode


# Check route tables
aws ec2 describe-route-tables \
  --filters "Name=vpc-id,Values=$VPC_ID" \
  --query 'RouteTables[*].[RouteTableId,Tags[?Key==`Name`].Value|[0]]' \
  --output table
Enter fullscreen mode Exit fullscreen mode

Option 2: AWS Console

  1. Go to AWS Console β†’ VPC
  2. Find your VPC: terraform-vpc-vpc
  3. Check:
    • βœ… 2 subnets (1 public, 1 private)
    • βœ… Internet Gateway attached
    • βœ… NAT Gateway in public subnet
    • βœ… 2 route tables (public and private)


🧠 Understanding Resource Dependencies

Implicit Dependencies

Terraform automatically understands dependencies when you reference resources:

resource "aws_subnet" "public_1" {
  vpc_id = aws_vpc.main.id  # References VPC
}
Enter fullscreen mode Exit fullscreen mode

Terraform knows: "Create VPC first, then create subnet"

Explicit Dependencies

Sometimes you need to force an order:

resource "aws_eip" "nat" {
  domain = "vpc"
  depends_on = [aws_internet_gateway.main]  # Explicit dependency
}
Enter fullscreen mode Exit fullscreen mode

Why? Elastic IP in VPC requires Internet Gateway to exist first.

Dependency Graph

Terraform builds a graph of dependencies:

VPC
β”œβ”€β”€ Internet Gateway
β”‚   β”œβ”€β”€ Elastic IP
β”‚   β”‚   └── NAT Gateway
β”‚   └── Public Route Table
β”‚       └── Route Table Associations
└── Subnets
    └── Route Table Associations
Enter fullscreen mode Exit fullscreen mode

Terraform creates resources in the correct order automatically!


πŸ’° Cost Breakdown

Let's talk about costs (important!):

Resource Cost Notes
VPC FREE No charge for VPCs
Subnets FREE No charge for subnets
Internet Gateway FREE No charge for IGW
Elastic IP FREE* *When attached to running instance
NAT Gateway $0.045/hour ~$32/month ⚠️
Data Transfer $0.045/GB Through NAT Gateway

Total for this setup: ~$32/month (just for NAT Gateway)

⚠️ IMPORTANT: Always destroy test infrastructure!

terraform destroy
Enter fullscreen mode Exit fullscreen mode

Production Note: In production, you'd have one NAT Gateway per AZ (2 NAT Gateways = $64/month) for high availability.


🎯 Best Practices

1. CIDR Planning

Do:

  • βœ… Plan CIDR blocks before creating VPC
  • βœ… Leave room for growth (use /16 for VPC)
  • βœ… Use non-overlapping ranges

Don't:

  • ❌ Use overlapping CIDR blocks
  • ❌ Make VPC too small (/24 = only 256 IPs)
  • ❌ Forget about VPC peering requirements

Tool: Use CIDR Calculator for planning

2. Multi-AZ Architecture

Always use multiple availability zones:

  • βœ… High availability
  • βœ… Fault tolerance
  • βœ… Better for production

Our setup: 2 AZs (us-east-1a, us-east-1b)

3. Tagging Strategy

Always tag resources:

tags = {
  Name        = "${var.project_name}-vpc"
  Environment = var.environment
  ManagedBy   = "Terraform"
  Project     = "Learning"
}
Enter fullscreen mode Exit fullscreen mode

Why?

  • Easy to identify resources
  • Cost allocation
  • Automation and filtering

4. Separate Public and Private

Security principle:

  • Public subnets: Load balancers, bastion hosts
  • Private subnets: Databases, application servers

Never put databases in public subnets!

5. Use Variables

Make everything configurable:

  • CIDR blocks
  • Project name
  • Environment
  • Region

Benefit: Same code for dev, staging, prod


πŸ› Common Issues & Solutions

Issue 1: NAT Gateway Creation Timeout

Problem:

Error: timeout while waiting for state to become 'available'
Enter fullscreen mode Exit fullscreen mode

Solution:

  • NAT Gateway takes 2-3 minutes to create
  • This is normal, just wait
  • If it fails, run terraform apply again

Issue 2: Elastic IP Limit Reached

Problem:

Error: AddressLimitExceeded: The maximum number of addresses has been reached
Enter fullscreen mode Exit fullscreen mode

Solution:

# Check current EIPs
aws ec2 describe-addresses

# Release unused EIPs
aws ec2 release-address --allocation-id eipalloc-xxxxx

# Or request limit increase in AWS Console
Enter fullscreen mode Exit fullscreen mode

Issue 3: Route Table Not Associated

Problem: Subnet can't reach internet

Solution: Check route table associations:

aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID"
Enter fullscreen mode Exit fullscreen mode

Ensure each subnet has a route table association.

Issue 4: CIDR Block Overlap

Problem:

Error: InvalidSubnet.Conflict: The CIDR 'x.x.x.x/x' conflicts with another subnet
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Check your CIDR blocks don't overlap
  • Public: 10.0.1.0/24, 10.0.2.0/24
  • Private: 10.0.11.0/24, 10.0.12.0/24
  • No overlap!

Issue 5: Forgot to Destroy NAT Gateway

Problem: Unexpected AWS bill

Solution:

# Check for NAT Gateways
aws ec2 describe-nat-gateways --filter "Name=state,Values=available"

# Destroy with Terraform
terraform destroy

# Or manually delete in AWS Console
Enter fullscreen mode Exit fullscreen mode

🧹 Cleanup (IMPORTANT!)

⚠️ Don't forget this step! NAT Gateway costs money.

# Destroy all resources
terraform destroy
Enter fullscreen mode Exit fullscreen mode

Type yes when prompted.

Expected output:

aws_route_table_association.private_2: Destroying...
aws_route_table_association.private_1: Destroying...
aws_route_table_association.public_2: Destroying...
aws_route_table_association.public_1: Destroying...
...
aws_nat_gateway.main: Destroying...
aws_nat_gateway.main: Still destroying... [10s elapsed]
aws_nat_gateway.main: Still destroying... [20s elapsed]
...
aws_nat_gateway.main: Destruction complete after 1m30s
...
Destroy complete! Resources: 10 destroyed.
Enter fullscreen mode Exit fullscreen mode

Verify deletion:

# Should return empty
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=terraform-vpc-vpc"
Enter fullscreen mode Exit fullscreen mode

Clean up directory:

cd ~
rm -rf ~/terraform-vpc-demo
Enter fullscreen mode Exit fullscreen mode


πŸ“š What You Learned

Concepts:

  • βœ… VPC fundamentals (CIDR, subnets, routing)
  • βœ… Public vs Private subnets
  • βœ… Internet Gateway and NAT Gateway
  • βœ… Route tables and associations
  • βœ… Data sources (aws_availability_zones)
  • βœ… Resource dependencies (implicit and explicit)
  • βœ… Cost-optimized architecture

Skills:

  • βœ… Created production-ready VPC
  • βœ… Configured networking components
  • βœ… Used data sources
  • βœ… Managed complex infrastructure
  • βœ… Understood AWS networking costs

Infrastructure:

  • βœ… 1 VPC
  • βœ… 2 Subnets (1 public, 1 private)
  • βœ… 1 Internet Gateway
  • βœ… 1 NAT Gateway
  • βœ… 2 Route Tables
  • βœ… Cost-optimized setup

πŸŽ“ Challenge: Extend Your VPC

Try these modifications:

Easy:

  1. Change CIDR blocks to 172.16.0.0/16
  2. Add a third availability zone
  3. Change project name and environment

Medium:

  1. Add more private subnets (10.0.21.0/24, 10.0.22.0/24)
  2. Create separate route tables for each private subnet
  3. Add Network ACLs for additional security

Hard:

  1. Add VPC Flow Logs
  2. Create a second NAT Gateway for high availability
  3. Add VPC endpoints for S3 and DynamoDB

πŸ”— Useful Resources


πŸš€ What's Next?

In Article 7, we'll deploy EC2 instances and an Application Load Balancer in this VPC!

You'll learn:

  • Launch EC2 instances in public and private subnets
  • Configure security groups
  • Create an Application Load Balancer
  • Set up health checks
  • Test the complete architecture

Time: 45 minutes

Difficulty: Intermediate

Outcome: Full web application infrastructure


πŸ“Œ Wrapping Up

Thank you for reading. I hope this article provided practical insights and a clearer understanding of the topic.

If you found this useful:

  • ❀️ Like if it added value
  • πŸ¦„ Unicorn if you’re applying it today
  • πŸ’Ύ Save it for your next optimization session
  • πŸ”„ Share it with your team

πŸ’‘ What’s Next

More deep dives are coming soon on:

  • Cloud Operations
  • GenAI & Agentic AI
  • DevOps Automation
  • Data & Platform Engineering

Follow along for weekly insights and hands-on guides.


🌐 Portfolio & Work

You can explore my full body of work, certifications, architecture projects, and technical articles here:

πŸ‘‰ Visit My Website


πŸ› οΈ Services I Offer

If you're looking for hands-on guidance or collaboration, I provide:

  • Cloud Architecture Consulting (AWS / Azure)
  • DevSecOps & Automation Design
  • FinOps Optimization Reviews
  • Technical Writing (Cloud, DevOps, GenAI)
  • Product & Architecture Reviews
  • Mentorship & 1:1 Technical Guidance

🀝 Let’s Connect

I’d love to hear your thoughts. Feel free to drop a comment or connect with me on:

πŸ”— LinkedIn

For collaborations, consulting, or technical discussions, reach out at:

πŸ“§ simplynadaf@gmail.com


Found this helpful? Share it with your team.

⭐ Star the repo β€’ πŸ“– Follow the series β€’ πŸ’¬ Ask questions

Made by Sarvar Nadaf

🌐 https://sarvarnadaf.com


Top comments (0)