Cloud 16 min read

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.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide Part 5 of 17

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:

  1. 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.
  2. 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.
  3. 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

ComponentUsage in LabAWS Free TierOn-Demand Rate
VPC, Subnets & IGWBase Network Topology100% Free$0.00
NAT GatewayOutbound 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 Cost1-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.sh script 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-1a and ap-southeast-1b so 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.

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 OffsetPurposeDescription
.0Network AddressThe base network identifier.
.1VPC RouterThe default gateway for all subnets inside the VPC.
.2AWS DNS ResolverThe base address of AmazonProvidedDNS (Amazon Route 53 Resolver).
.3AWS Future UseReserved by AWS for future expansion.
.255Network BroadcastReserved broadcast address (AWS VPC does not support broadcast, but the IP is reserved).

[!IMPORTANT] A /24 subnet provides 251 usable IP addresses ($256 - 5 = 251$). A /28 subnet (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:

  1. 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.
  2. 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.
  3. 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-1a fails, 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:

  1. Public Route Table: Routes 0.0.0.0/0IGW_ID.
  2. Private Route Table: Routes 0.0.0.0/0NAT_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:

  1. Public Subnet Ingress: Your local laptop can reach the Public EC2 because the Internet Gateway performs bidirectional NAT.
  2. Private Subnet Isolation: Outsiders on the Internet cannot reach the Private EC2 directly.
  3. 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:

  • In Part 6, we will run the AWS VPC Reachability Analyzer CLI to mathematically simulate and verify these packet paths without launching virtual machines.
  • In Part 7, we will generate SSH Key Pairs, configure Security Groups, and launch live EC2 fleets to execute this live Bastion jump test hands-on!

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

ActionCLI Command
Create VPCaws ec2 create-vpc --cidr-block <cidr>
Enable DNS Hostnamesaws ec2 modify-vpc-attribute --vpc-id <id> --enable-dns-hostnames '{"Value": true}'
Create Subnetaws ec2 create-subnet --vpc-id <id> --cidr-block <cidr> --availability-zone <az>
Auto-Assign Public IPaws ec2 modify-subnet-attribute --subnet-id <id> --map-public-ip-on-launch
Create Internet Gatewayaws ec2 create-internet-gateway
Attach IGW to VPCaws ec2 attach-internet-gateway --internet-gateway-id <igw> --vpc-id <vpc>
Allocate Elastic IPaws ec2 allocate-address --domain vpc
Create NAT Gatewayaws ec2 create-nat-gateway --subnet-id <pub-sub> --allocation-id <eip>
Create Route Tableaws 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 Subnetaws 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:

  1. We designed an enterprise multi-AZ network topology spanning ap-southeast-1a and ap-southeast-1b.
  2. We mastered Subnet IP Math and accounted for the 5 Reserved AWS IPs.
  3. We provisioned an Internet Gateway for public ingress and an Elastic IP + Managed NAT Gateway for private egress.
  4. We created segregated Public & Private Route Tables to maintain strict isolation.
  5. 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!

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.