AWS CLI First #5: Production VPC Architecture & Multi-AZ Routing from Scratch
Build an enterprise-grade, highly available cloud network from the command line. Provision custom VPCs, multi-AZ public and private subnets, Internet Gateways, NAT Gateways, and segregated Route Tables using pure AWS CLI scripts.
AWS with CLI: The Terminal-First Mastery Path
Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.
In Part 4, we mastered AWS Global Infrastructure—learning how Regions, Availability Zones, and physical ZoneId mappings form the foundation of cloud reliability.
Now in Part 5, we take those physical foundations and construct our private cloud data center from scratch.
Why Default VPCs are Unfit for Production
When you create an AWS account, AWS provides a Default VPC (172.31.0.0/16) in every region. While convenient for quick experimentation, default VPCs create serious architectural risks:
- No Private Subnet Isolation: In a default VPC, all subnets are public by default. Any EC2 instance launched inside receives a public IPv4 address and direct exposure to Internet scanners.
- Fixed, Overlapping CIDRs: Default VPCs always use
172.31.0.0/16. If you later connect multiple AWS accounts or link your cloud network to your on-premises office via AWS Site-to-Site VPN or AWS Transit Gateway, overlapping IP ranges will cause irreversible routing conflicts. - No Ingress/Egress Segregation: There is no dedicated NAT Gateway architecture separating inbound traffic from outbound-only worker nodes.
In this guide, we will build a custom, production-ready, multi-Availability Zone (AZ) VPC topology purely from the terminal.
💰 Estimated Lab Cost & Resource Architecture
| Component | Usage in Lab | AWS Free Tier | On-Demand Rate |
|---|---|---|---|
| VPC, Subnets & IGW | Base Network Topology | 100% Free | $0.00 |
| NAT Gateway | Outbound Egress (~1 hour) | Paid Service | $0.045 / hour |
| Elastic IP (EIP) | Attached to NAT Gateway (~1 hour) | Included with NAT | $0.005 / hour |
| Total Estimated Cost | 1-Hour Hands-On Session | ~$0.05 Total | ~$0.05 – $0.06 Total |
[!TIP] The NAT Gateway incurs ~$0.05 for a 1-hour lab session. Running the automated
destroy-vpc.shscript in Phase 7 terminates the NAT Gateway and halts all billing immediately!
Core Concept: The Multi-AZ Production VPC Blueprint
Here is the exact network architecture we will build in this guide:
+---------------------------------------------------------------------------------------+
| Production VPC (10.0.0.0/16) - Singapore Region |
+---------------------------------------------------------------------------------------+
| |
| [ Internet ] <=================> [ Internet Gateway (IGW) ] |
| | |
| +---------------------------------------|-----------------------------------------+ |
| | [ Public Route Table ] (0.0.0.0/0 -> igw-xxxx) | |
| +---------------------------------------------------------------------------------+ |
| | | |
| v (ap-southeast-1a) v (ap-southeast-1b) |
| +--------------------------------------+ +--------------------------+ |
| | Public Subnet A (10.0.1.0/24) | | Public Subnet B | |
| | * Auto-assign Public IP: Enabled | | (10.0.2.0/24) | |
| | * NAT Gateway (nat-xxxx + EIP) | | * Public Load Balancers | |
| +--------------------------------------+ +--------------------------+ |
| | |
| | (One-way outbound Internet access) |
| v |
| +---------------------------------------------------------------------------------+ |
| | [ Private Route Table ] (0.0.0.0/0 -> nat-xxxx) | |
| +---------------------------------------------------------------------------------+ |
| | | |
| v (ap-southeast-1a) v (ap-southeast-1b) |
| +--------------------------------------+ +--------------------------+ |
| | Private Subnet A (10.0.10.0/24) | | Private Subnet B | |
| | * Backend Microservices | | (10.0.20.0/24) | |
| | * Relational Database (RDS Primary) | | * Redundant Web Servers | |
| +--------------------------------------+ | * RDS Standby Replica | |
| +--------------------------+ |
| |
+---------------------------------------------------------------------------------------+
Key Architectural Highlights:
- VPC CIDR (
10.0.0.0/16): Provides 65,536 private IPv4 addresses. - Dual-AZ Redundancy: Resources span
ap-southeast-1aandap-southeast-1bso that an entire data center facility failure will not bring down our applications. - Tiered Subnets:
- Public Tier (
10.0.1.0/24,10.0.2.0/24): Hosts Internet-facing Application Load Balancers (ALB) and NAT Gateways. - Private Tier (
10.0.10.0/24,10.0.20.0/24): Hosts databases, backend containers, and business logic with zero direct inbound Internet access.
- Public Tier (
Core Concept: Subnet IP Math & The 5 Reserved AWS IPs
When planning CIDR blocks, a common mistake is assuming that a /24 subnet contains 256 usable IP addresses ($2^{32-24} = 256$).
In every subnet you create, AWS automatically reserves 5 IP addresses:
| IP Offset | Purpose | Description |
|---|---|---|
.0 | Network Address | The base network identifier. |
.1 | VPC Router | The default gateway for all subnets inside the VPC. |
.2 | AWS DNS Resolver | The base address of AmazonProvidedDNS (Amazon Route 53 Resolver). |
.3 | AWS Future Use | Reserved by AWS for future expansion. |
.255 | Network Broadcast | Reserved broadcast address (AWS VPC does not support broadcast, but the IP is reserved). |
[!IMPORTANT] A
/24subnet provides 251 usable IP addresses ($256 - 5 = 251$). A/28subnet (the minimum allowed in AWS) provides only 11 usable IP addresses ($16 - 5 = 11$).
Core Concept: Ingress vs. Egress Architecture (IGW vs. NAT Gateway)
Understanding the distinction between Internet Gateways and NAT Gateways is fundamental to cloud networking:
+-----------------------------------------------------------------------------------+
| Ingress vs Egress Gateway Mechanics |
+-----------------------------------------------------------------------------------+
1. INGRESS & EGRESS (Public Subnet)
[ EC2 Instance (Public IP) ] <==== 1:1 NAT ====> [ Internet Gateway ] <===> [ Internet ]
(Bidirectional: External clients can initiate incoming connections on open ports)
2. EGRESS-ONLY (Private Subnet)
[ Private EC2 Instance ] ======> [ NAT Gateway (Public Subnet) ] ======> [ Internet ]
(Unidirectional: Private instance can download patches; Internet scanners CANNOT enter)
- Internet Gateway (IGW): A fully managed, horizontally scalable software gateway. It performs 1-to-1 NAT translation for instances that possess a public IP. It supports both inbound (ingress) and outbound (egress) traffic.
- NAT Gateway: A managed Network Address Translation service deployed inside a public subnet with a static Elastic IP. It allows instances in private subnets to initiate outbound requests (e.g.
npm install,apt update) while blocking all inbound connections initiated from the Internet.
Phase 1: Provisioning the Custom VPC & DNS Attributes
Let’s begin by assuming our administrator persona (cloud-dev-admin) and creating our 10.0.0.0/16 VPC.
Step 1: Create the VPC
Run the following command to create the VPC and capture its ID directly into a shell variable:
VPC_ID=$(aws ec2 create-vpc \
--cidr-block "10.0.0.0/16" \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=Production-VPC},{Key=Environment,Value=Production}]' \
--profile cloud-dev-admin \
--query "Vpc.VpcId" \
--output text)
🔍 How to Validate:
Inspect the newly created VPC state and CIDR block:
aws ec2 describe-vpcs \
--vpc-ids "$VPC_ID" \
--query "Vpcs[*].[VpcId, CidrBlock, State, Tags[?Key=='Name'].Value | [0]]" \
--output table \
--profile cloud-dev-admin
Expected Output:
------------------------------------------------------------------
| DescribeVpcs |
+-----------------------+---------------+------------+-----------+
| vpc-0123456789abcdef | 10.0.0.0/16 | available | Production-VPC |
+-----------------------+---------------+------------+-----------+
Step 2: Enable DNS Hostnames and DNS Resolution
By default, newly created custom VPCs do not automatically assign human-readable public DNS hostnames to instances. Let’s enable both attributes:
# Enable DNS Resolution (AmazonProvidedDNS at 10.0.0.2)
aws ec2 modify-vpc-attribute \
--vpc-id "$VPC_ID" \
--enable-dns-support '{"Value": true}' \
--profile cloud-dev-admin
# Enable DNS Hostnames (e.g. ec2-xx-xx-xx-xx.compute-1.amazonaws.com)
aws ec2 modify-vpc-attribute \
--vpc-id "$VPC_ID" \
--enable-dns-hostnames '{"Value": true}' \
--profile cloud-dev-admin
🔍 How to Validate:
Query both DNS attributes to verify they returned true:
# 1. Validate DNS Resolution Support:
aws ec2 describe-vpc-attribute \
--vpc-id "$VPC_ID" \
--attribute enableDnsSupport \
--profile cloud-dev-admin
# 2. Validate DNS Hostnames:
aws ec2 describe-vpc-attribute \
--vpc-id "$VPC_ID" \
--attribute enableDnsHostnames \
--profile cloud-dev-admin
Expected JSON Output:
{
"VpcId": "vpc-0123456789abcdef",
"EnableDnsSupport": {
"Value": true
}
}
{
"VpcId": "vpc-0123456789abcdef",
"EnableDnsHostnames": {
"Value": true
}
}
Phase 2: Provisioning Multi-AZ Public and Private Subnets
Let’s query the active Availability Zones in our region (ap-southeast-1 Singapore) and provision 4 dedicated subnets.
Step 1: Query Availability Zones
aws ec2 describe-availability-zones \
--profile cloud-dev-admin \
--query "AvailabilityZones[?State=='available'].ZoneName" \
--output table
We will use ap-southeast-1a (AZ 1) and ap-southeast-1b (AZ 2).
Step 2: Create Public Subnets
# 1. Public Subnet A (AZ-a)
PUB_SUB_A=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.1.0/24" \
--availability-zone "ap-southeast-1a" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-A},{Key=Tier,Value=Public}]' \
--profile cloud-dev-admin \
--query "Subnet.SubnetId" \
--output text)
# 2. Public Subnet B (AZ-b)
PUB_SUB_B=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.2.0/24" \
--availability-zone "ap-southeast-1b" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-B},{Key=Tier,Value=Public}]' \
--profile cloud-dev-admin \
--query "Subnet.SubnetId" \
--output text)
Step 3: Enable Auto-Assign Public IPv4 on Public Subnets
Instances launched in public subnets should receive a public IPv4 address automatically:
aws ec2 modify-subnet-attribute \
--subnet-id "$PUB_SUB_A" \
--map-public-ip-on-launch \
--profile cloud-dev-admin
aws ec2 modify-subnet-attribute \
--subnet-id "$PUB_SUB_B" \
--map-public-ip-on-launch \
--profile cloud-dev-admin
🔍 How to Validate Public Subnets:
Verify both subnets and confirm MapPublicIpOnLaunch == True:
aws ec2 describe-subnets \
--subnet-ids "$PUB_SUB_A" "$PUB_SUB_B" \
--query "Subnets[*].[Tags[?Key=='Name'].Value | [0], SubnetId, CidrBlock, AvailabilityZone, MapPublicIpOnLaunch]" \
--output table \
--profile cloud-dev-admin
Expected Output:
-----------------------------------------------------------------------------------------------
| DescribeSubnets |
+-------------------+-----------------------+----------------+--------------------+-----------+
| Public-Subnet-A | subnet-011111111111 | 10.0.1.0/24 | ap-southeast-1a | True |
| Public-Subnet-B | subnet-022222222222 | 10.0.2.0/24 | ap-southeast-1b | True |
+-------------------+-----------------------+----------------+--------------------+-----------+
Step 4: Create Private Subnets
# 1. Private Subnet A (AZ-a)
PRIV_SUB_A=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.10.0/24" \
--availability-zone "ap-southeast-1a" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-A},{Key=Tier,Value=Private}]' \
--profile cloud-dev-admin \
--query "Subnet.SubnetId" \
--output text)
# 2. Private Subnet B (AZ-b)
PRIV_SUB_B=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.20.0/24" \
--availability-zone "ap-southeast-1b" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-B},{Key=Tier,Value=Private}]' \
--profile cloud-dev-admin \
--query "Subnet.SubnetId" \
--output text)
🔍 How to Validate Private Subnets:
Verify that private subnets have MapPublicIpOnLaunch == False:
aws ec2 describe-subnets \
--subnet-ids "$PRIV_SUB_A" "$PRIV_SUB_B" \
--query "Subnets[*].[Tags[?Key=='Name'].Value | [0], SubnetId, CidrBlock, AvailabilityZone, MapPublicIpOnLaunch]" \
--output table \
--profile cloud-dev-admin
Expected Output:
-----------------------------------------------------------------------------------------------
| DescribeSubnets |
+-------------------+-----------------------+----------------+--------------------+-----------+
| Private-Subnet-A | subnet-033333333333 | 10.0.10.0/24 | ap-southeast-1a | False |
| Private-Subnet-B | subnet-044444444444 | 10.0.20.0/24 | ap-southeast-1b | False |
+-------------------+-----------------------+----------------+--------------------+-----------+
Phase 3: Creating & Attaching the Internet Gateway (IGW)
A newly created VPC is completely isolated. To allow public traffic, we must provision an Internet Gateway and attach it to our VPC.
Step 1: Create the Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=Production-IGW}]' \
--profile cloud-dev-admin \
--query "InternetGateway.InternetGatewayId" \
--output text)
Step 2: Attach the IGW to the VPC
aws ec2 attach-internet-gateway \
--internet-gateway-id "$IGW_ID" \
--vpc-id "$VPC_ID" \
--profile cloud-dev-admin
🔍 How to Validate:
Verify that the IGW attachment state is available inside our VPC:
aws ec2 describe-internet-gateways \
--internet-gateway-ids "$IGW_ID" \
--query "InternetGateways[*].[InternetGatewayId, Attachments[0].VpcId, Attachments[0].State]" \
--output table \
--profile cloud-dev-admin
Expected Output:
-----------------------------------------------------------------
| DescribeInternetGateways |
+-----------------------+-----------------------+---------------+
| igw-0123456789abcdef | vpc-0123456789abcdef | available |
+-----------------------+-----------------------+---------------+
Phase 4: Provisioning Elastic IP & Managed NAT Gateway
To provide outbound Internet access to our private subnets without exposing them to inbound Internet scanners, we will deploy a managed NAT Gateway inside Public Subnet A.
What is an Elastic IP (EIP) and Why Does a NAT Gateway Need One?
An Elastic IP (EIP) is a reserved, permanent public IPv4 address allocated to your AWS account. Unlike standard EC2 public IPs (which change every time a machine stops or restarts), an Elastic IP never changes until you explicitly release it.
+-----------------------------------------------------------------------------------+
| Source NAT (SNAT) Outbound Flow |
+-----------------------------------------------------------------------------------+
[ Private EC2 #1 (10.0.10.25) ] ----\
[ Private EC2 #2 (10.0.10.88) ] -----> [ NAT Gateway (Public Subnet A) ]
[ Private EC2 #3 (10.0.20.14) ] ----/ |
| (Translates private source IPs
| into single Elastic IP)
v
[ Static Elastic IP: 13.250.xx.xx ]
|
v
[ External Payment API / Stripe / GitHub ]
(Whitelists 13.250.xx.xx in corporate firewall)
Why a NAT Gateway REQUIRES an Elastic IP:
- Source Network Address Translation (SNAT): When 100 private backend servers send outbound requests (e.g. downloading software packages, calling Stripe/GitHub APIs), the NAT Gateway translates all internal private IPs to exit through its single, static Elastic IP.
- External IP Whitelisting: External vendors, payment gateways, and SaaS partners often require a fixed, static IP address to whitelist in their corporate firewalls. Because your NAT Gateway holds an Elastic IP, your outbound IP never changes.
- AWS API Requirement: AWS strictly mandates an active Elastic IP (
--allocation-id) when provisioning a public NAT Gateway.
[!TIP] The AWS Elastic IP Anti-Hoarding Pricing Rule: Public IPv4 addresses are a scarce global commodity.
- While attached to an active resource (NAT Gateway or running EC2): The Elastic IP is 100% Free (1 per resource).
- If unattached / left idle in your account: AWS charges $0.005/hour to discourage hoarding unused public IPv4 addresses!
[!NOTE] Single-AZ vs. Multi-AZ NAT Gateway Trade-off:
- Production Standard: Deploy 1 NAT Gateway per AZ for complete fault tolerance (if
ap-southeast-1afails, AZ-b continues routing egress traffic).- Sandbox / Dev / Learning Standard: Deploy 1 NAT Gateway in Public Subnet A shared by both private subnets. This saves ~$32.40/month per additional NAT Gateway while teaching identical routing principles.
Step 1: Allocate a Static Elastic IP (EIP)
EIP_ALLOC_ID=$(aws ec2 allocate-address \
--domain vpc \
--tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=NAT-Gateway-EIP}]' \
--profile cloud-dev-admin \
--query "AllocationId" \
--output text)
Step 2: Create the NAT Gateway in Public Subnet A
NAT_GW_ID=$(aws ec2 create-nat-gateway \
--subnet-id "$PUB_SUB_A" \
--allocation-id "$EIP_ALLOC_ID" \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=Production-NAT-GW}]' \
--profile cloud-dev-admin \
--query "NatGateway.NatGatewayId" \
--output text)
Step 3: Wait for the NAT Gateway to Become Available
NAT Gateway provisioning takes approximately 60–90 seconds. Let’s poll its state using the CLI:
aws ec2 wait nat-gateway-available \
--nat-gateway-ids "$NAT_GW_ID" \
--profile cloud-dev-admin
🔍 How to Validate:
Inspect the NAT Gateway state and public IP binding:
aws ec2 describe-nat-gateways \
--nat-gateway-ids "$NAT_GW_ID" \
--query "NatGateways[*].[NatGatewayId, State, SubnetId, NatGatewayAddresses[0].AllocationId, NatGatewayAddresses[0].PublicIp]" \
--output table \
--profile cloud-dev-admin
Expected Output:
--------------------------------------------------------------------------------------------------------------
| DescribeNatGateways |
+-----------------------+------------+-----------------------+--------------------------+--------------------+
| nat-0123456789abcdef | available | subnet-011111111111 | eipalloc-0123456789abc | 13.250.xx.xx |
+-----------------------+------------+-----------------------+--------------------------+--------------------+
Phase 5: Configuring Public and Private Route Tables
Subnets do not route traffic automatically—they follow the directions inside their associated Route Table.
We will create two route tables:
- Public Route Table: Routes
0.0.0.0/0→IGW_ID. - Private Route Table: Routes
0.0.0.0/0→NAT_GW_ID.
Step 1: Create and Configure the Public Route Table
# 1. Create Public Route Table
PUB_RTB_ID=$(aws ec2 create-route-table \
--vpc-id "$VPC_ID" \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Public-Route-Table}]' \
--profile cloud-dev-admin \
--query "RouteTable.RouteTableId" \
--output text)
# 2. Add Default Route (0.0.0.0/0 -> IGW)
aws ec2 create-route \
--route-table-id "$PUB_RTB_ID" \
--destination-cidr-block "0.0.0.0/0" \
--gateway-id "$IGW_ID" \
--profile cloud-dev-admin
# 3. Associate Public Subnet A
aws ec2 associate-route-table \
--route-table-id "$PUB_RTB_ID" \
--subnet-id "$PUB_SUB_A" \
--profile cloud-dev-admin
# 4. Associate Public Subnet B
aws ec2 associate-route-table \
--route-table-id "$PUB_RTB_ID" \
--subnet-id "$PUB_SUB_B" \
--profile cloud-dev-admin
🔍 How to Validate Public Route Table:
Verify that 0.0.0.0/0 points to IGW_ID and both public subnets are associated:
aws ec2 describe-route-tables \
--route-table-ids "$PUB_RTB_ID" \
--query "RouteTables[*].[RouteTableId, Routes[*].[DestinationCidrBlock, GatewayId, State], Associations[*].SubnetId]" \
--output json \
--profile cloud-dev-admin
Step 2: Create and Configure the Private Route Table
# 1. Create Private Route Table
PRIV_RTB_ID=$(aws ec2 create-route-table \
--vpc-id "$VPC_ID" \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Private-Route-Table}]' \
--profile cloud-dev-admin \
--query "RouteTable.RouteTableId" \
--output text)
# 2. Add Default Route (0.0.0.0/0 -> NAT Gateway)
aws ec2 create-route \
--route-table-id "$PRIV_RTB_ID" \
--destination-cidr-block "0.0.0.0/0" \
--nat-gateway-id "$NAT_GW_ID" \
--profile cloud-dev-admin
# 3. Associate Private Subnet A
aws ec2 associate-route-table \
--route-table-id "$PRIV_RTB_ID" \
--subnet-id "$PRIV_SUB_A" \
--profile cloud-dev-admin
# 4. Associate Private Subnet B
aws ec2 associate-route-table \
--route-table-id "$PRIV_RTB_ID" \
--subnet-id "$PRIV_SUB_B" \
--profile cloud-dev-admin
🔍 How to Validate Private Route Table:
Verify that 0.0.0.0/0 points to NAT_GW_ID and both private subnets are associated:
aws ec2 describe-route-tables \
--route-table-ids "$PRIV_RTB_ID" \
--query "RouteTables[*].[RouteTableId, Routes[*].[DestinationCidrBlock, NatGatewayId, State], Associations[*].SubnetId]" \
--output json \
--profile cloud-dev-admin
Phase 6: Verifying Complete Network Topology via Structured CLI Queries
Let’s audit our entire VPC topology to ensure every subnet, gateway, and route is accurately bound:
# 1. List Subnet Allocation & CIDRs
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query "Subnets[*].[Tags[?Key=='Name'].Value | [0], SubnetId, CidrBlock, AvailabilityZone, MapPublicIpOnLaunch]" \
--output table \
--profile cloud-dev-admin
Subnet Verification Table:
-----------------------------------------------------------------------------------------------
| DescribeSubnets |
+-------------------+-----------------------+----------------+--------------------+-----------+
| Public-Subnet-A | subnet-011111111111 | 10.0.1.0/24 | ap-southeast-1a | True |
| Public-Subnet-B | subnet-022222222222 | 10.0.2.0/24 | ap-southeast-1b | True |
| Private-Subnet-A | subnet-033333333333 | 10.0.10.0/24 | ap-southeast-1a | False |
| Private-Subnet-B | subnet-044444444444 | 10.0.20.0/24 | ap-southeast-1b | False |
+-------------------+-----------------------+----------------+--------------------+-----------+
# 2. Inspect Routes inside Route Tables
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query "RouteTables[*].[Tags[?Key=='Name'].Value | [0], RouteTableId, Routes[*].[DestinationCidrBlock, GatewayId, NatGatewayId, State]]" \
--output json \
--profile cloud-dev-admin
How Engineers Validate Ingress & Egress in Live Environments
Now that the structural network topology is verified, how do cloud engineers prove that packets actually flow as designed in a real production VPC?
In live production, engineers run the Bastion Jump & Egress Test:
+-----------------------------------------------------------------------------------+
| Live Validation Architecture Flow |
+-----------------------------------------------------------------------------------+
1. TEST INTERNET GATEWAY (INGRESS)
[ Your Local Terminal ] ---- SSH: 22 ---> [ Public EC2 (10.0.1.50 + Public IP) ]
* Result: Connects immediately through the Internet Gateway (IGW)!
2. TEST PRIVATE ISOLATION
[ Your Local Terminal ] ---- SSH: 22 -X-> [ Private EC2 (10.0.10.88 - No Public IP) ]
* Result: Connection times out / blocked (No public IP, no IGW route).
3. TEST NAT GATEWAY (EGRESS)
[ Public EC2 (Bastion Jump Host) ] ---- SSH ---> [ Private EC2 (10.0.10.88) ]
|
| Run: `curl ifconfig.me`
v
[ NAT Gateway (Elastic IP) ]
|
v
[ Output: 13.250.xx.xx ]
What this proves:
- Public Subnet Ingress: Your local laptop can reach the Public EC2 because the Internet Gateway performs bidirectional NAT.
- Private Subnet Isolation: Outsiders on the Internet cannot reach the Private EC2 directly.
- NAT Gateway Egress: When the private server queries
curl ifconfig.me, the Internet returns the Elastic IP of your NAT Gateway, proving that outbound traffic leaves through the NAT Gateway while inbound traffic remains blocked!
[!TIP] What’s Coming in Parts 6 & 7:
Phase 7: Automated Teardown Script (Cost Hygiene)
[!CAUTION] NAT Gateway Hourly Billing: AWS charges $0.045 per hour (~$32.40/month) for active NAT Gateways, even if no traffic passes through them. When practicing in a personal sandbox, run the teardown script below when you finish your lab session to eliminate all ongoing costs.
Save the following script as destroy-vpc.sh:
cat <<'EOF' > destroy-vpc.sh
#!/usr/bin/env bash
set -e
# Accept VPC ID from 1st argument, or exported environment variable, or prompt interactively
VPC_ID="${1:-$VPC_ID}"
if [ -z "$VPC_ID" ]; then
read -p "Enter VPC ID to destroy (e.g. vpc-xxxx): " VPC_ID
fi
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
echo "=== Starting Complete Teardown for VPC: $VPC_ID ==="
# 1. Delete NAT Gateways
NAT_GWS=$(aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=$VPC_ID" "Name=state,Values=available,pending" --query "NatGateways[*].NatGatewayId" --output text --region "$REGION" --profile "$PROFILE")
for NAT_ID in $NAT_GWS; do
echo "Deleting NAT Gateway: $NAT_ID..."
aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_ID" --region "$REGION" --profile "$PROFILE"
echo "Waiting for NAT Gateway $NAT_ID to delete..."
aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$NAT_ID" --region "$REGION" --profile "$PROFILE"
done
# 2. Release Elastic IPs
EIPS=$(aws ec2 describe-addresses --filter "Name=tag:Name,Values=NAT-Gateway-EIP" --query "Addresses[*].AllocationId" --output text --region "$REGION" --profile "$PROFILE")
for EIP in $EIPS; do
echo "Releasing Elastic IP: $EIP..."
aws ec2 release-address --allocation-id "$EIP" --region "$REGION" --profile "$PROFILE" || true
done
# 3. Delete Subnets first (Releases all route table associations)
SUBS=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query "Subnets[*].SubnetId" --output text --region "$REGION" --profile "$PROFILE")
for SUB in $SUBS; do
echo "Deleting Subnet: $SUB..."
aws ec2 delete-subnet --subnet-id "$SUB" --region "$REGION" --profile "$PROFILE"
done
# 4. Delete Custom Route Tables (Excludes the Default Main Route Table)
RTBS=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Route-Table,Private-Route-Table" --query "RouteTables[*].RouteTableId" --output text --region "$REGION" --profile "$PROFILE")
for RTB in $RTBS; do
echo "Deleting Custom Route Table: $RTB..."
aws ec2 delete-route-table --route-table-id "$RTB" --region "$REGION" --profile "$PROFILE" || true
done
# 5. Detach & Delete Internet Gateways
IGWS=$(aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[*].InternetGatewayId" --output text --region "$REGION" --profile "$PROFILE")
for IGW in $IGWS; do
echo "Detaching & deleting IGW: $IGW..."
aws ec2 detach-internet-gateway --internet-gateway-id "$IGW" --vpc-id "$VPC_ID" --region "$REGION" --profile "$PROFILE"
aws ec2 delete-internet-gateway --internet-gateway-id "$IGW" --region "$REGION" --profile "$PROFILE"
done
# 6. Delete VPC
echo "Deleting VPC: $VPC_ID..."
aws ec2 delete-vpc --vpc-id "$VPC_ID" --region "$REGION" --profile "$PROFILE"
echo "=== Teardown Complete! Zero Resources Remaining. ==="
EOF
chmod +x destroy-vpc.sh
Execute the Teardown Script:
You can pass the variable directly as an argument, or let it prompt you:
# Option A: Pass the VPC ID directly as an argument (Recommended)
./destroy-vpc.sh "$VPC_ID"
# Option B: Run interactively and enter the VPC ID when prompted
./destroy-vpc.sh
Quick Reference: Essential VPC CLI Commands
| Action | CLI Command |
|---|---|
| Create VPC | aws ec2 create-vpc --cidr-block <cidr> |
| Enable DNS Hostnames | aws ec2 modify-vpc-attribute --vpc-id <id> --enable-dns-hostnames '{"Value": true}' |
| Create Subnet | aws ec2 create-subnet --vpc-id <id> --cidr-block <cidr> --availability-zone <az> |
| Auto-Assign Public IP | aws ec2 modify-subnet-attribute --subnet-id <id> --map-public-ip-on-launch |
| Create Internet Gateway | aws ec2 create-internet-gateway |
| Attach IGW to VPC | aws ec2 attach-internet-gateway --internet-gateway-id <igw> --vpc-id <vpc> |
| Allocate Elastic IP | aws ec2 allocate-address --domain vpc |
| Create NAT Gateway | aws ec2 create-nat-gateway --subnet-id <pub-sub> --allocation-id <eip> |
| Create Route Table | aws ec2 create-route-table --vpc-id <id> |
| Add Route (0.0.0.0/0) | aws ec2 create-route --route-table-id <rtb> --destination-cidr-block 0.0.0.0/0 --gateway-id <gw> |
| Associate Subnet | aws ec2 associate-route-table --route-table-id <rtb> --subnet-id <sub> |
Summary & What’s Next
In this fifth installment of AWS with CLI, we constructed our core multi-AZ network architecture:
- We designed an enterprise multi-AZ network topology spanning
ap-southeast-1aandap-southeast-1b. - We mastered Subnet IP Math and accounted for the 5 Reserved AWS IPs.
- We provisioned an Internet Gateway for public ingress and an Elastic IP + Managed NAT Gateway for private egress.
- We created segregated Public & Private Route Tables to maintain strict isolation.
- We validated every infrastructure layer and authored an automated Teardown Script.
In Part 6, we will explore Advanced VPC: S3 Gateway Endpoints, VPC Flow Logs & Network Reachability Simulator, learning how to eliminate NAT Gateway data processing fees with zero-cost internal routing and mathematically audit network packet paths!