A working mental model for AWS VPCs — what each piece does, how they connect, and why "VPC" is the wrong mental model if you came from physical networks.
By the end of this post you'll understand what an AWS VPC is, what each component does, and how packets actually flow when an EC2 instance talks to the internet (or doesn't). We'll also build a minimal VPC end-to-end so the parts stop feeling abstract.
About 25 minutes. You'll need an AWS account and the AWS CLI installed and configured. We'll stay well within the free tier.
If you came from physical networks, the temptation is to map AWS networking onto VLANs, switches, and routers. Resist it. AWS networking is not switches and routers — it's configuration that defines connectivity. There are no wires. Two EC2 instances can be in adjacent IP ranges and totally unable to reach each other if the route tables and security groups don't allow it.
That's the core insight. Once it lands, everything else slots in.
A VPC has five parts you need to understand:
10.0.0.0/16).That's the whole vocabulary you need to read most VPC diagrams.
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications \
'ResourceType=vpc,Tags=[{Key=Name,Value=tutorial-vpc}]'
You'll get back a VPC object. Note the VpcId (e.g. vpc-0abc123...). Save it:
export VPC=vpc-0abc123... # paste your VPC ID
That's a /16 — 65,536 addresses. Way more than you need, but /16 is the standard default. You can always split it into smaller subnets.
A "public" subnet (where instances can reach the internet) and a "private" subnet (where they can't):
aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a --tag-specifications \
'ResourceType=subnet,Tags=[{Key=Name,Value=tutorial-public}]'
aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.2.0/24 \
--availability-zone us-east-1a --tag-specifications \
'ResourceType=subnet,Tags=[{Key=Name,Value=tutorial-private}]'
Save both subnet IDs:
export PUBLIC_SUBNET=subnet-0...
export PRIVATE_SUBNET=subnet-0...
Important: there is no flag on a subnet that makes it "public" or "private." That distinction comes from the route table, which we'll get to. The names are descriptive, not functional.
aws ec2 create-internet-gateway --tag-specifications \
'ResourceType=internet-gateway,Tags=[{Key=Name,Value=tutorial-igw}]'
Save the InternetGatewayId:
export IGW=igw-0...
Attach it to the VPC:
aws ec2 attach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
The IGW is now part of the VPC, but no traffic flows yet. We need a route table that points at it.
aws ec2 create-route-table --vpc-id $VPC --tag-specifications \
'ResourceType=route-table,Tags=[{Key=Name,Value=tutorial-public-rt}]'
Save it:
export PUBLIC_RT=rtb-0...
Add a route: "for any destination (0.0.0.0/0), send traffic to the IGW."
aws ec2 create-route --route-table-id $PUBLIC_RT --destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW
That's the line that makes a route table "public" — a default route to an IGW. Same VPC's other route table, no such route, becomes "private."
Now associate the public subnet with the public route table:
aws ec2 associate-route-table --route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNET
The simplest test: launch a small EC2 instance into the public subnet and try to reach the internet.
# Create a security group that allows outbound everywhere
aws ec2 create-security-group --vpc-id $VPC --group-name tutorial-sg \
--description "tutorial"
export SG=sg-0...
# Allow SSH from your IP
MYIP=$(curl -s https://ifconfig.me)
aws ec2 authorize-security-group-ingress --group-id $SG \
--protocol tcp --port 22 --cidr ${MYIP}/32
Launch an Amazon Linux EC2 in the public subnet. (Use the AWS console for this if you don't have key pairs configured for the CLI; quicker.) When it's running, SSH into it and:
curl https://example.com
You should get HTML back. The packets traveled: instance → public subnet → public route table → internet gateway → internet.
If you launched the same instance into the private subnet instead, with no route table changes, the same curl would hang. Same instance, same security group — but the route table doesn't know how to reach 0.0.0.0/0, so the packet is dropped at the boundary.
That's the whole "configuration defines connectivity" lesson in one experiment.
When done, delete in reverse order so AWS doesn't complain about dependencies:
aws ec2 terminate-instances --instance-ids <your-instance>
aws ec2 delete-security-group --group-id $SG
aws ec2 disassociate-route-table ...
aws ec2 delete-route-table --route-table-id $PUBLIC_RT
aws ec2 detach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
aws ec2 delete-internet-gateway --internet-gateway-id $IGW
aws ec2 delete-subnet --subnet-id $PUBLIC_SUBNET
aws ec2 delete-subnet --subnet-id $PRIVATE_SUBNET
aws ec2 delete-vpc --vpc-id $VPC
Forgetting that subnets are zonal, not regional. A subnet lives in exactly one AZ. For HA, you need multiple subnets — typically one per AZ.
Confusing security groups with route tables. Route tables decide where traffic goes. Security groups decide whether it's allowed when it arrives. Both have to permit a packet for it to flow. Most "I can't connect" debugging fails to check both.
CIDR blocks too small. A /27 (32 IPs) seems fine until your EKS cluster eats them all in pod IPs. For new VPCs, /16 is a safe default. For subnets, /22 for container workloads, /24 for VM-only.
Building a private subnet without a NAT gateway and being surprised when apt-get update hangs. Private subnets can't reach the internet by default. If they need to (for package updates, S3 access, etc.) you either need a NAT gateway (for general internet) or VPC endpoints (for AWS services specifically).
You've got the core pieces. The next levels:
VPCs are not switches and routers. They're configuration. Once that lands, the AWS networking docs stop being terrifying and start being useful.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Create your first S3 bucket, upload and download files, and set up the right access controls — without accidentally making everything public.
Build a real disk-cleanup script step by step. Learn variables, conditionals, loops, error handling, and the safety preamble that prevents foot-guns.
Explore more articles in this category
Create your first S3 bucket, upload and download files, and set up the right access controls — without accidentally making everything public.
Write, package, and deploy a Lambda function using only the AWS CLI. Trigger it via a public URL. Understand what serverless actually means.
We deployed the same edge function on both platforms and measured for a quarter. Where each wins, where each loses, and the surprises along the way.
Evergreen posts worth revisiting.