AWS for DevOps — Project 1: Production VPC
Project Overview
Build a production-grade VPC with public subnets, private subnets, database subnets, NAT Gateways, and proper security group configurations across two Availability Zones.
Real-World Analogy: You’re designing the office building layout — where the reception area is (public subnets), where employees work (private subnets), and where the vault is (database subnets). The NAT Gateway is like a secure mail room that lets employees send letters out without exposing their location.
| Detail | Value |
|---|---|
| Difficulty | ⭐⭐ (Intermediate) |
| Time | 1-2 hours |
| Cost | ~$0.045/hr for NAT Gateway |
| Services | VPC, Subnets, IGW, NAT GW, Route Tables, NACLs, Security Groups |
Architecture
┌───────────────────────────────────────────────────────────────┐
│ VPC: 10.0.0.0/16 │
│ │
│ ┌─── AZ-a ────────────────┐ ┌─── AZ-b ────────────────┐ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ Public Subnet │ │ │ │ Public Subnet │ │ │
│ │ │ 10.0.1.0/24 │ │ │ │ 10.0.2.0/24 │ │ │
│ │ │ ┌────────────┐ │ │ │ │ ┌────────────┐ │ │ │
│ │ │ │ NAT GW │ │ │ │ │ │ NAT GW │ │ │ │
│ │ │ └────────────┘ │ │ │ │ └────────────┘ │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ │ ↑ │ │ ↑ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ Private Subnet │ │ │ │ Private Subnet │ │ │
│ │ │ 10.0.11.0/24 │ │ │ │ 10.0.12.0/24 │ │ │
│ │ │ (App Servers) │ │ │ │ (App Servers) │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ Database Subnet │ │ │ │ Database Subnet │ │ │
│ │ │ 10.0.21.0/24 │ │ │ │ 10.0.22.0/24 │ │ │
│ │ │ (RDS, ElastiC.) │ │ │ │ (RDS, ElastiC.) │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Internet GW │ ←── Route to 0.0.0.0/0 from public subs │
│ └──────────────┘ │
└───────────────────────────────────────────────────────────────┘
Step 1 — Create the VPC
# Create the VPC
$ aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=prod-vpc},{Key=Project,Value=practice-1}]'
{
"Vpc": {
"VpcId": "vpc-0abc123def456789",
"CidrBlock": "10.0.0.0/16",
"State": "available"
}
}
# Enable DNS hostnames (required for many AWS services)
$ aws ec2 modify-vpc-attribute \
--vpc-id vpc-0abc123def456789 \
--enable-dns-hostnames '{"Value": true}'
Why /16? A
/16CIDR gives you 65,536 IP addresses — plenty of room for growth. Production VPCs often use/16to avoid running out of IPs.
Step 2 — Create Subnets
Public Subnets (for load balancers, bastion hosts)
# Public Subnet in AZ-a
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-public-1a},{Key=Tier,Value=public}]'
{
"Subnet": {
"SubnetId": "subnet-0pub1a",
"CidrBlock": "10.0.1.0/24",
"AvailabilityZone": "us-east-1a"
}
}
# Public Subnet in AZ-b
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-public-1b},{Key=Tier,Value=public}]'
Private Subnets (for application servers)
# Private Subnet in AZ-a
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.11.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-private-1a},{Key=Tier,Value=private}]'
# Private Subnet in AZ-b
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.12.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-private-1b},{Key=Tier,Value=private}]'
Database Subnets (for RDS, ElastiCache)
# Database Subnet in AZ-a
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.21.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-db-1a},{Key=Tier,Value=database}]'
# Database Subnet in AZ-b
$ aws ec2 create-subnet \
--vpc-id vpc-0abc123def456789 \
--cidr-block 10.0.22.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-db-1b},{Key=Tier,Value=database}]'
Subnet Summary
| Subnet | CIDR | AZ | Purpose |
|---|---|---|---|
| prod-public-1a | 10.0.1.0/24 | us-east-1a | ALB, Bastion |
| prod-public-1b | 10.0.2.0/24 | us-east-1b | ALB, Bastion |
| prod-private-1a | 10.0.11.0/24 | us-east-1a | App servers |
| prod-private-1b | 10.0.12.0/24 | us-east-1b | App servers |
| prod-db-1a | 10.0.21.0/24 | us-east-1a | RDS, ElastiCache |
| prod-db-1b | 10.0.22.0/24 | us-east-1b | RDS, ElastiCache |
Step 3 — Create Internet Gateway
# Create Internet Gateway
$ aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=prod-igw}]'
{
"InternetGateway": {
"InternetGatewayId": "igw-0abc123"
}
}
# Attach to VPC
$ aws ec2 attach-internet-gateway \
--internet-gateway-id igw-0abc123 \
--vpc-id vpc-0abc123def456789
Step 4 — Create NAT Gateways
# Allocate Elastic IPs for NAT Gateways
$ aws ec2 allocate-address --domain vpc
{
"AllocationId": "eipalloc-0nat1a",
"PublicIp": "52.1.2.3"
}
$ aws ec2 allocate-address --domain vpc
{
"AllocationId": "eipalloc-0nat1b",
"PublicIp": "52.4.5.6"
}
# Create NAT Gateway in Public Subnet AZ-a
$ aws ec2 create-nat-gateway \
--subnet-id subnet-0pub1a \
--allocation-id eipalloc-0nat1a \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=prod-nat-1a}]'
{
"NatGateway": {
"NatGatewayId": "nat-0abc1a",
"State": "pending"
}
}
# Create NAT Gateway in Public Subnet AZ-b
$ aws ec2 create-nat-gateway \
--subnet-id subnet-0pub1b \
--allocation-id eipalloc-0nat1b \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=prod-nat-1b}]'
# Wait for NAT Gateways to become available
$ aws ec2 wait nat-gateway-available --nat-gateway-ids nat-0abc1a nat-0abc1b
Why two NAT Gateways? One per AZ provides high availability. If AZ-a goes down, instances in AZ-b still have internet access through their own NAT Gateway.
Step 5 — Configure Route Tables
Public Route Table
# Create public route table
$ aws ec2 create-route-table \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=prod-public-rt}]'
{
"RouteTable": {
"RouteTableId": "rtb-0public"
}
}
# Add route to Internet Gateway
$ aws ec2 create-route \
--route-table-id rtb-0public \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-0abc123
# Associate with public subnets
$ aws ec2 associate-route-table \
--route-table-id rtb-0public \
--subnet-id subnet-0pub1a
$ aws ec2 associate-route-table \
--route-table-id rtb-0public \
--subnet-id subnet-0pub1b
Private Route Tables (one per AZ)
# Private route table for AZ-a
$ aws ec2 create-route-table \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=prod-private-rt-1a}]'
{
"RouteTable": {
"RouteTableId": "rtb-0priv1a"
}
}
$ aws ec2 create-route \
--route-table-id rtb-0priv1a \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-0abc1a
$ aws ec2 associate-route-table --route-table-id rtb-0priv1a --subnet-id subnet-0priv1a
$ aws ec2 associate-route-table --route-table-id rtb-0priv1a --subnet-id subnet-0db1a
# Private route table for AZ-b
$ aws ec2 create-route-table \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=prod-private-rt-1b}]'
{
"RouteTable": {
"RouteTableId": "rtb-0priv1b"
}
}
$ aws ec2 create-route \
--route-table-id rtb-0priv1b \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-0abc1b
$ aws ec2 associate-route-table --route-table-id rtb-0priv1b --subnet-id subnet-0priv1b
$ aws ec2 associate-route-table --route-table-id rtb-0priv1b --subnet-id subnet-0db1b
Route Table Summary
| Route Table | Destination | Target | Subnets |
|---|---|---|---|
| prod-public-rt | 0.0.0.0/0 | igw-0abc123 | public-1a, public-1b |
| prod-private-rt-1a | 0.0.0.0/0 | nat-0abc1a | private-1a, db-1a |
| prod-private-rt-1b | 0.0.0.0/0 | nat-0abc1b | private-1b, db-1b |
Step 6 — Create Security Groups
# ALB Security Group — allows HTTP/HTTPS from internet
$ aws ec2 create-security-group \
--group-name prod-alb-sg \
--description "ALB Security Group" \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=prod-alb-sg}]'
{
"GroupId": "sg-0alb"
}
$ aws ec2 authorize-security-group-ingress \
--group-id sg-0alb \
--protocol tcp --port 80 --cidr 0.0.0.0/0
$ aws ec2 authorize-security-group-ingress \
--group-id sg-0alb \
--protocol tcp --port 443 --cidr 0.0.0.0/0
# App Security Group — allows traffic ONLY from ALB
$ aws ec2 create-security-group \
--group-name prod-app-sg \
--description "App Server Security Group" \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=prod-app-sg}]'
{
"GroupId": "sg-0app"
}
$ aws ec2 authorize-security-group-ingress \
--group-id sg-0app \
--protocol tcp --port 8080 \
--source-group sg-0alb
# Database Security Group — allows traffic ONLY from app servers
$ aws ec2 create-security-group \
--group-name prod-db-sg \
--description "Database Security Group" \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=prod-db-sg}]'
{
"GroupId": "sg-0db"
}
$ aws ec2 authorize-security-group-ingress \
--group-id sg-0db \
--protocol tcp --port 5432 \
--source-group sg-0app
Security Group Chain
Internet → (80/443) → ALB SG → (8080) → App SG → (5432) → DB SG
Key Principle: Each tier only accepts traffic from the tier above it. The database never talks directly to the internet.
Step 7 — Create Network ACLs
# Create NACL for database subnets (extra layer of security)
$ aws ec2 create-network-acl \
--vpc-id vpc-0abc123def456789 \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=prod-db-nacl}]'
{
"NetworkAcl": {
"NetworkAclId": "acl-0db"
}
}
# Allow inbound PostgreSQL from private subnets only
$ aws ec2 create-network-acl-entry \
--network-acl-id acl-0db \
--rule-number 100 \
--protocol tcp \
--port-range From=5432,To=5432 \
--cidr-block 10.0.11.0/24 \
--rule-action allow \
--ingress
$ aws ec2 create-network-acl-entry \
--network-acl-id acl-0db \
--rule-number 110 \
--protocol tcp \
--port-range From=5432,To=5432 \
--cidr-block 10.0.12.0/24 \
--rule-action allow \
--ingress
# Allow ephemeral ports outbound (for responses)
$ aws ec2 create-network-acl-entry \
--network-acl-id acl-0db \
--rule-number 100 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 10.0.0.0/16 \
--rule-action allow \
--egress
# Associate with database subnets
$ aws ec2 replace-network-acl-association \
--association-id aclassoc-0db1a \
--network-acl-id acl-0db
Step 8 — Verify the Setup
# Verify VPC
$ aws ec2 describe-vpcs \
--filters "Name=tag:Name,Values=prod-vpc" \
--query 'Vpcs[0].{VpcId:VpcId,CIDR:CidrBlock,State:State}'
{
"VpcId": "vpc-0abc123def456789",
"CIDR": "10.0.0.0/16",
"State": "available"
}
# Verify all subnets
$ aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=vpc-0abc123def456789" \
--query 'Subnets[].{Name:Tags[?Key==`Name`].Value|[0],CIDR:CidrBlock,AZ:AvailabilityZone}' \
--output table
------------------------------------------------------
| DescribeSubnets |
+--------------+------------------+-------------------+
| AZ | CIDR | Name |
+--------------+------------------+-------------------+
| us-east-1a | 10.0.1.0/24 | prod-public-1a |
| us-east-1b | 10.0.2.0/24 | prod-public-1b |
| us-east-1a | 10.0.11.0/24 | prod-private-1a |
| us-east-1b | 10.0.12.0/24 | prod-private-1b |
| us-east-1a | 10.0.21.0/24 | prod-db-1a |
| us-east-1b | 10.0.22.0/24 | prod-db-1b |
+--------------+------------------+-------------------+
# Verify NAT Gateways
$ aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=vpc-0abc123def456789" \
--query 'NatGateways[].{Id:NatGatewayId,State:State,SubnetId:SubnetId}'
Step 9 — Test Connectivity
# Launch a test instance in the private subnet
$ aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t2.micro \
--subnet-id subnet-0priv1a \
--security-group-ids sg-0app \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=prod-test-instance}]' \
--key-name my-key-pair
# Use SSM Session Manager to connect (no SSH needed)
$ aws ssm start-session --target i-0abc123
# From inside the instance — test outbound internet via NAT
$ curl -s ifconfig.me
52.1.2.3 # This is the NAT Gateway's EIP
Step 10 — Clean Up
⚠️ Always clean up to avoid charges, especially the NAT Gateways.
# Delete in reverse order of creation
# 1. Terminate test instances
$ aws ec2 terminate-instances --instance-ids i-0abc123
# 2. Delete NAT Gateways
$ aws ec2 delete-nat-gateway --nat-gateway-id nat-0abc1a
$ aws ec2 delete-nat-gateway --nat-gateway-id nat-0abc1b
# Wait for NAT gateways to be deleted
$ sleep 60
# 3. Release Elastic IPs
$ aws ec2 release-address --allocation-id eipalloc-0nat1a
$ aws ec2 release-address --allocation-id eipalloc-0nat1b
# 4. Delete subnets
$ for subnet in subnet-0pub1a subnet-0pub1b subnet-0priv1a subnet-0priv1b subnet-0db1a subnet-0db1b; do
aws ec2 delete-subnet --subnet-id $subnet
done
# 5. Delete security groups
$ aws ec2 delete-security-group --group-id sg-0db
$ aws ec2 delete-security-group --group-id sg-0app
$ aws ec2 delete-security-group --group-id sg-0alb
# 6. Detach and delete Internet Gateway
$ aws ec2 detach-internet-gateway --internet-gateway-id igw-0abc123 --vpc-id vpc-0abc123def456789
$ aws ec2 delete-internet-gateway --internet-gateway-id igw-0abc123
# 7. Delete route tables (non-main)
$ aws ec2 delete-route-table --route-table-id rtb-0public
$ aws ec2 delete-route-table --route-table-id rtb-0priv1a
$ aws ec2 delete-route-table --route-table-id rtb-0priv1b
# 8. Delete VPC
$ aws ec2 delete-vpc --vpc-id vpc-0abc123def456789
What You Built
| Component | Count | Purpose |
|---|---|---|
| VPC | 1 | Network boundary |
| Public Subnets | 2 | Load balancers, bastion |
| Private Subnets | 2 | Application servers |
| Database Subnets | 2 | RDS, ElastiCache |
| Internet Gateway | 1 | Public internet access |
| NAT Gateways | 2 | Private subnet outbound |
| Route Tables | 3 | Traffic routing |
| Security Groups | 3 | Layered firewall |
| NACLs | 1 | Subnet-level firewall |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Forgetting DNS hostnames | Services can’t resolve VPC endpoints | Enable enableDnsHostnames |
| Single NAT Gateway | Single point of failure | One NAT per AZ |
| Overlapping CIDRs | Can’t peer VPCs later | Plan CIDR ranges upfront |
| Wide-open security groups | Security vulnerability | Use source security groups |
| Forgetting ephemeral ports in NACLs | Broken responses | Allow 1024-65535 outbound |