Real AWS workloads start with the network, not the server. Where a workload lives — which VPC, which subnet, behind which gateway, allowed by which security rules — decides far more about its security posture, cost, and failure behaviour than the choice of instance type. This post is the deep dive on VPC, the virtual data centre that hosts almost everything else in the series.

Series map: 00 Foundations → 01 Network First (this post) → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data → 05 Serverless & Events → 06 Workflow Orchestration → 07 Observability & Security → 08 Infrastructure as Code.


Why network before compute

A tempting shortcut is to "just launch an EC2" and figure out the network later. The cost: every later change (adding a database, fronting with a load balancer, going multi-AZ, locking down for security review) becomes a re-architecture. Network-first means picking a small set of decisions once and reusing them for the next decade.

The decisions:

  • Which region.
  • VPC CIDR range (which private IPs you'll own).
  • Subnet layout (how many AZs, public/private/isolated).
  • Internet egress path (IGW + NAT or VPC endpoints).
  • Security group baseline (who can talk to whom).

Learning Outcome

  • Design a VPC CIDR that won't collide with other accounts you might peer with.
  • Lay out public / private / isolated subnets across 3 AZs.
  • Configure route tables, internet gateways, NAT gateways, and VPC endpoints.
  • Write security groups and (rarely) NACLs that are tight without being painful.
  • Recognise the cost levers: NAT gateway, cross-AZ traffic, data egress.

Chapter 1 — VPC anatomy

A VPC is a software-defined private network inside one AWS region. Each AWS account gets a default VPC per region (172.31.0.0/16); production accounts almost always create custom ones.

Construct What
VPC the boundary; one or more CIDR ranges (IPv4 + optional IPv6)
Subnet a CIDR slice of the VPC, lives in exactly one AZ
Route table per-subnet (or default) rules: "for destination X, next hop is Y"
Internet Gateway (IGW) one per VPC; enables inbound + outbound public internet
NAT Gateway enables outbound-only public internet from private subnets
VPC endpoint reach AWS services privately, no IGW/NAT needed
Security group stateful per-ENI firewall
NACL stateless per-subnet firewall

CIDR sizing

A /16 (e.g., 10.0.0.0/16) gives 65,536 addresses — enough for a serious production environment. A /24 subnet (e.g., 10.0.1.0/24) gives 256 addresses minus 5 reserved by AWS → 251 usable.

Trick: pick non-overlapping ranges across all your AWS accounts and on-prem networks. Once you peer two VPCs or set up a Transit Gateway / Direct Connect, overlapping CIDRs become an immediate migration project. Conventional starting layouts:

  • Account prod — 10.0.0.0/16
  • Account staging — 10.1.0.0/16
  • Account dev — 10.2.0.0/16
  • Account tools / shared services — 10.10.0.0/16
  • On-prem — 192.168.0.0/16 or 172.16.0.0/12

Chapter 2 — The 3-AZ subnet layout

1
2
3
4
5
6
7
8
9
10
11
VPC 10.0.0.0/16  (region: ap-east-1)
├── Public subnets (one per AZ): 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
│ route 0.0.0.0/0 → IGW
│ holds: ALB, NAT gateways, bastions
├── Private subnets (one per AZ): 10.0.11.0/24, 10.0.12.0/24, 10.0.13.0/24
│ route 0.0.0.0/0 → NAT gateway (per AZ)
│ holds: EC2 app servers, ECS tasks, Lambda-in-VPC
└── Isolated subnets (one per AZ): 10.0.21.0/24, 10.0.22.0/24, 10.0.23.0/24
no default route
holds: RDS, ElastiCache, sensitive workloads

Three reasons this shape is the industry default:

  1. Failure isolation. An AZ outage takes down at most a third of capacity. Combined with a load balancer (Post 03), the service stays up.
  2. Blast radius for security. Public subnets only host things designed to face the internet. Application servers and data live one or two hops inward.
  3. Cost vs availability tradeoff. One NAT gateway is cheaper but a single point of failure; one per AZ doubles or triples cost but removes the SPoF. Production prefers per-AZ.

A subnet is "public" because its route table sends 0.0.0.0/0 to an Internet Gateway. There is no checkbox — it is the routing that defines the type.

A three-AZ VPC with public, private and isolated subnets, a NAT gateway per AZ, and the three route tables


Chapter 3 — Route tables

A route table is an ordered list of (destination CIDR, target) rules. Each subnet associates with one route table.

Example public-subnet route table:

Destination Target
10.0.0.0/16 local (intra-VPC; implicit)
0.0.0.0/0 igw-XXXXXXXX (Internet Gateway)

Example private-subnet route table:

Destination Target
10.0.0.0/16 local
0.0.0.0/0 nat-XXXXXXXX (NAT Gateway)

Example isolated-subnet route table:

Destination Target
10.0.0.0/16 local

(No 0.0.0.0/0 rule → no internet access at all.)


Chapter 4 — Internet egress: IGW vs NAT vs VPC endpoints

Path Where allowed Inbound from internet? Outbound to internet?
IGW public subnets only yes (with public IP) yes
NAT Gateway private subnets no yes
VPC endpoint (Gateway) S3, DynamoDB n/a private to those services
VPC endpoint (Interface) most AWS services n/a private to those services

VPC endpoints — the cost saver

By default, an EC2 in a private subnet calling s3.ap-east-1.amazonaws.com routes out via the NAT gateway — and you pay NAT data charges (~USD 0.045 / GB). With a Gateway VPC endpoint for S3, the traffic stays inside AWS, free.

Watch out: Interface endpoints cost about USD 7 / endpoint / AZ / month plus per-GB. Useful when bandwidth is high; not always cheaper than NAT for occasional access. Always estimate before adding.


Chapter 5 — Security groups (your everyday firewall)

A security group is stateful: if a request matches an outbound rule, the reply is automatically allowed (and vice versa). You only configure one direction, not both.

Default behaviour: no inbound, all outbound.

Rules contain:

  • Protocol (TCP / UDP / ICMP / all)
  • Port range
  • Source / destination (CIDR, another security group, or a prefix list)

Reference SGs by ID, not by IP

When both ends are AWS resources, reference the other side's security group rather than its IP range. As IPs change (autoscaling, recreation), the SG reference stays correct.

1
2
3
4
5
6
7
8
9
10
11
12
sg-alb        (load balancer)
Inbound 443 from 0.0.0.0/0
Outbound all to 0.0.0.0/0

sg-app (app tier)
Inbound 8080 from sg-alb ← reference by SG
Outbound all to 0.0.0.0/0

sg-db (database tier)
Inbound 5432 from sg-app ← reference by SG
Outbound: none required

Chapter 6 — NACLs (the backup layer)

A Network ACL is stateless and applied per subnet. Default rules allow all inbound + outbound. Custom NACLs default to deny.

NACLs are evaluated in order (rule numbers); the first match wins.

In practice most teams don't touch NACLs — security groups do all the real work. Reserve NACLs for blanket per-subnet rules (e.g., "isolated subnets reject any outbound traffic at all") or defence-in-depth against compromised SG state.


Chapter 7 — Naming + tagging conventions

A VPC project survives for years; the names you pick now are the names every dashboard, alert, and IaC variable inherits.

Recommended tags on every resource:

Tag Example
Name prod-vpc-public-1a
Environment prod / staging / dev
Owner team-platform
CostCenter cc-1234
ManagedBy terraform

Standardised tags drive Cost Explorer breakdowns, automated cleanups, and access policies.


Chapter 8 — Reference build (CLI walkthrough)

A minimal 3-AZ network you can recreate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# create VPC
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query Vpc.VpcId --output text)

# attach IGW
IGW=$(aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC --internet-gateway-id $IGW

# public subnet in AZ a
PUB_A=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.1.0/24 \
--availability-zone ap-east-1a --query Subnet.SubnetId --output text)

# private subnet in AZ a
PRIV_A=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.11.0/24 \
--availability-zone ap-east-1a --query Subnet.SubnetId --output text)

# public route table
PUB_RT=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $PUB_RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW
aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_A

# allocate Elastic IP for NAT
EIP=$(aws ec2 allocate-address --domain vpc --query AllocationId --output text)

# NAT gateway in public-A
NAT=$(aws ec2 create-nat-gateway --subnet-id $PUB_A --allocation-id $EIP \
--query NatGateway.NatGatewayId --output text)

# private route table
PRIV_RT=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $PRIV_RT --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT
aws ec2 associate-route-table --route-table-id $PRIV_RT --subnet-id $PRIV_A

Repeat for AZ b and c. In Post 08 the same network appears as a 30-line Terraform module.


Chapter 9 — Cost levers in a VPC

Item Approx (us-east-1) Note
VPC itself free the construct costs nothing
Subnet free also free
Route table, IGW free
NAT Gateway USD 32 / month + USD 0.045 / GB processed the main cost driver
Elastic IP attached free
Elastic IP idle (unattached) USD 3.60 / month always release unused
VPC Interface endpoint ~USD 7 / month / AZ + per-GB
VPC Gateway endpoint (S3, DynamoDB) free
Cross-AZ traffic USD 0.01 / GB each direction adds up at scale
Inter-region traffic USD 0.02 / GB
Internet egress USD 0.09 / GB (after free)

Trick: when in doubt about NAT cost, look at the BytesOutToDestination CloudWatch metric on the NAT gateway. Multiply by 0.045 to get the monthly USD bill. If it's painful, add a Gateway endpoint for S3 / DynamoDB first — usually a 30 – 60% NAT bill reduction with zero risk.


Practice Exercises

Exercise 1 — Pick a CIDR

Design CIDR blocks for these accounts so they can all peer without overlap:

  • prod (large)
  • staging (medium)
  • dev (small)
  • shared-tools (medium)
Solution
  • prod: 10.0.0.0/16
  • staging: 10.1.0.0/16
  • dev: 10.2.0.0/16
  • shared-tools: 10.10.0.0/16

Or use /20 slices of a single /12 (10.0.0.0/12) if you want larger headroom.

Exercise 2 — Build the 3-AZ VPC

Recreate Chapter 8's CLI build for all three AZs of your default region. Verify by launching a t3.micro into a private subnet and confirming curl https://example.com works (proves NAT egress).

Exercise 3 — Cut the NAT bill

Add an S3 Gateway endpoint to the private route tables of the VPC from Exercise 2. Run aws s3 cp from the private instance. Confirm in CloudWatch that the NAT gateway's outbound bytes did not increase.

Exercise 4 — Security-group chain

Build three SGs:

  • sg-web → 443 from 0.0.0.0/0
  • sg-app → 8080 from sg-web
  • sg-db → 5432 from sg-app

Verify the chain works end-to-end and that nothing else can reach the DB.


Cheat-sheet

What How
VPC CIDR private range like 10.0.0.0/16, non-overlapping
Subnet AZ each subnet is in one AZ
Public subnet route 0.0.0.0/0 → IGW
Private subnet route 0.0.0.0/0 → NAT
Isolated subnet no default route
NAT cost per-hour + per-GB; biggest VPC cost
Free egress to S3/DynamoDB use Gateway VPC endpoints
Security group stateful, default deny inbound
NACL stateless, default allow; rarely modified
Cross-AZ costs $0.01/GB each direction
Tag everything Name, Env, Owner, CostCenter, ManagedBy
Multi-AZ rule always ≥ 2 AZs for production

What's next

Post 02 covers compute and storage — EC2 instances, EBS volumes, and S3 object storage — the workhorse trio that runs on top of the network from this post.


References

Note: this post is in progress. Planned additions: IPv6 dual-stack, VPC peering vs Transit Gateway decision, PrivateLink for SaaS, flow-log analytics.