# AWS VPC Explained — Subnets, Route Tables, and the Internet Gateway

URL: https://www.devopsness.com/blog/aws-vpc-explained-beginner-guide
Author: Kiril Urbonas
Published: 2026-04-29
Updated: 2026-04-29
Categories: Cloud
Tags: AWS, Networking, Cloud, VPC, Tutorial, Beginner

Summary: 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.

---
# AWS VPC Explained: Subnets, Route Tables, and the Internet Gateway

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.

## The mental shift

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.

## The five pieces

A VPC has five parts you need to understand:

1. **VPC** — a private network in AWS. Has a CIDR block (e.g. `10.0.0.0/16`).
2. **Subnet** — a slice of the VPC's CIDR, bound to a single Availability Zone (AZ).
3. **Route table** — rules saying "for this destination CIDR, send traffic to this target."
4. **Internet gateway (IGW)** — the thing that lets traffic in/out of the public internet.
5. **NAT gateway** — lets private instances reach the internet *out* without being reachable *in*.

That's the whole vocabulary you need to read most VPC diagrams.

## Step 1: Create a VPC

```bash
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:

```bash
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.

## Step 2: Create two subnets

A "public" subnet (where instances can reach the internet) and a "private" subnet (where they can't):

```bash
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:

```bash
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.

## Step 3: Create an internet gateway and attach it

```bash
aws ec2 create-internet-gateway --tag-specifications \
  'ResourceType=internet-gateway,Tags=[{Key=Name,Value=tutorial-igw}]'
```

Save the `InternetGatewayId`:

```bash
export IGW=igw-0...
```

Attach it to the VPC:

```bash
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.

## Step 4: Create the public route table

```bash
aws ec2 create-route-table --vpc-id $VPC --tag-specifications \
  'ResourceType=route-table,Tags=[{Key=Name,Value=tutorial-public-rt}]'
```

Save it:

```bash
export PUBLIC_RT=rtb-0...
```

Add a route: "for any destination (`0.0.0.0/0`), send traffic to the IGW."

```bash
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:

```bash
aws ec2 associate-route-table --route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNET
```

## Step 5: Verify connectivity

The simplest test: launch a small EC2 instance into the public subnet and try to reach the internet.

```bash
# 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:

```bash
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.

## Step 6: Cleanup

When done, delete in reverse order so AWS doesn't complain about dependencies:

```bash
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
```

## Common mistakes

**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).

## What to read next

You've got the core pieces. The next levels:

- [Cloud networking fundamentals: VPCs, subnets, routing](/blog/cloud-networking-fundamentals-vpcs-subnets-routing) — the deeper mental model and the mistakes that compound at scale
- [Best practices: cloud networking segmentation patterns](/blog/best-practices-cloud-networking-segmentation-patterns) — how to design VPCs that survive a year
- [Deep dive: multi-cluster traffic routing strategies](/blog/deep-dive-multi-cluster-traffic-routing-strategies) — when one VPC isn't enough

VPCs are not switches and routers. They're configuration. Once that lands, the AWS networking docs stop being terrifying and start being useful.

