Skip to main content
AWS · lesson 5 of 10
AWS intermediate Lesson 5 of 10

Also part of: Networking

VPC and Networking Basics

Build a subnet, route table, and internet gateway, then walk through the exact debugging sequence for an EC2 instance that can't reach the internet.

By SkillByExample · Published

aws vpc networking

A VPC (Virtual Private Cloud) is an isolated network you define inside a region: an IP address range, subnets carved out of it, and route tables that decide where traffic can go. Almost every “my EC2 instance won’t connect to X” problem traces back to one of the three pieces built in this lesson.

This lesson uses a real AWS free-tier account — VPC objects can be created against LocalStack, but simulating real routing failures (and fixing them) needs an actual network path, which LocalStack’s Community edition does not provide.

Building the network

aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
  --query 'Vpc.{VpcId:VpcId,Cidr:CidrBlock}'
$ aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.{VpcId:VpcId,Cidr:CidrBlock}'
{
    "VpcId": "vpc-0a1b2c3d4e5f60789",
    "Cidr": "10.0.0.0/16"
}
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d4e5f60789 \
  --cidr-block 10.0.1.0/24 --availability-zone us-east-1a \
  --query 'Subnet.{SubnetId:SubnetId,Cidr:CidrBlock}'
$ aws ec2 create-subnet --vpc-id vpc-0a1b2c3d4e5f60789 --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --query 'Subnet.{SubnetId:SubnetId,Cidr:CidrBlock}'
{
    "SubnetId": "subnet-0f1e2d3c4b5a67890",
    "Cidr": "10.0.1.0/24"
}

A brand-new subnet has no route out yet — it is neither public nor private, just isolated. “Public” is not a property you set on a subnet directly; it’s a consequence of its route table.

Step 1: launch an instance — and watch it fail to reach the internet

aws ec2 run-instances --image-id ami-0c101f26f147fa7fd \
  --instance-type t3.micro --subnet-id subnet-0f1e2d3c4b5a67890 \
  --associate-public-ip-address \
  --query 'Instances[0].{InstanceId:InstanceId,PublicIp:PublicIpAddress}'
$ aws ec2 run-instances ...
{
    "InstanceId": "i-0b7c4a91f2e8d5063",
    "PublicIp": "54.198.31.207"
}
ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
$ ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
curl: (28) Failed to connect to example.com port 443 after 5001 ms: Connection timed out

SSH itself worked (it uses a security group rule, not routing, and the instance has a public IP), but outbound HTTPS from inside the instance times out. This is the classic symptom: the instance is reachable, but it can’t reach out.

Step 2: check the route table

aws ec2 describe-route-tables \
  --filters "Name=association.subnet-id,Values=subnet-0f1e2d3c4b5a67890" \
  --query 'RouteTables[0].Routes'
$ aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=subnet-0f1e2d3c4b5a67890" --query 'RouteTables[0].Routes'
[
    {
        "DestinationCidrBlock": "10.0.0.0/16",
        "GatewayId": "local",
        "State": "active"
    }
]

Only the implicit local route exists — nothing sends 0.0.0.0/0 (everything else) anywhere. This is the actual cause: a subnet with a public IP but no route to an internet gateway can be reached from the internet on rules the security group allows, but cannot reach the internet.

Step 3: create and attach an internet gateway

aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId'
"igw-0d4e8f2a1c9b73065"
aws ec2 attach-internet-gateway \
  --internet-gateway-id igw-0d4e8f2a1c9b73065 \
  --vpc-id vpc-0a1b2c3d4e5f60789
aws ec2 create-route \
  --route-table-id rtb-05f2a8b1c9d3e7460 \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id igw-0d4e8f2a1c9b73065
$ aws ec2 create-route --route-table-id rtb-05f2a8b1c9d3e7460 --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0d4e8f2a1c9b73065
{
    "Return": true
}

Step 4: confirm the fix

aws ec2 describe-route-tables --route-table-ids rtb-05f2a8b1c9d3e7460 \
  --query 'RouteTables[0].Routes'
[
    {
        "DestinationCidrBlock": "10.0.0.0/16",
        "GatewayId": "local",
        "State": "active"
    },
    {
        "DestinationCidrBlock": "0.0.0.0/0",
        "GatewayId": "igw-0d4e8f2a1c9b73065",
        "State": "active"
    }
]
ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
$ ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
HTTP/2 200
content-type: text/html; charset=UTF-8

Same instance, same security group, only the route table changed — proof that this class of failure lives entirely at the network layer, one level below anything IAM or security groups control.

Public vs. private subnet, in one table

Route to IGWPublic IPReachable from internetCan reach internet
Public subnetyesyesyes (if SG allows)yes
Private subnetno (routes via NAT)nonoyes, outbound only
Isolated subnetnononono

A private subnet’s 0.0.0.0/0 route points at a NAT gateway instead of an IGW — outbound works, nothing can initiate a connection in.

Practice

1. Remove the 0.0.0.0/0 route and confirm the timeout returns.
aws ec2 delete-route --route-table-id rtb-05f2a8b1c9d3e7460 \
  --destination-cidr-block 0.0.0.0/0
ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
curl: (28) Failed to connect to example.com port 443 after 5001 ms: Connection timed out

Same symptom as step 1 — confirming the route, not the security group or the instance itself, was the entire cause.

2. Add the route back but point it at a bogus gateway ID and see what error that produces instead.
aws ec2 create-route --route-table-id rtb-05f2a8b1c9d3e7460 \
  --destination-cidr-block 0.0.0.0/0 --gateway-id igw-00000000000000000
An error occurred (InvalidInternetGatewayID.NotFound) when calling the CreateRoute
operation: The gateway ID 'igw-00000000000000000' does not exist

A clean, immediate CLI error rather than a runtime timeout — validating the gateway reference is at least one class of mistake AWS catches before it ever reaches the network.

3. Create a private subnet with no internet gateway route and confirm an instance in it has no public IP.
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d4e5f60789 \
  --cidr-block 10.0.2.0/24 --availability-zone us-east-1b \
  --query 'Subnet.SubnetId'
aws ec2 run-instances --image-id ami-0c101f26f147fa7fd \
  --instance-type t3.micro --subnet-id subnet-0a9b8c7d6e5f43210 \
  --query 'Instances[0].PublicIpAddress'
null

No --associate-public-ip-address, and the subnet’s own default is off too — null is correct and expected for a private subnet.

4. Use a network ACL to explicitly block port 443 outbound and reproduce the timeout a third way.
aws ec2 create-network-acl-entry --network-acl-id acl-0f3e8d2c1b9a7654f \
  --rule-number 90 --protocol tcp --port-range From=443,To=443 \
  --egress --cidr-block 0.0.0.0/0 --rule-action deny
$ ssh -i my-key.pem ubuntu@54.198.31.207 'curl -m 5 -sI https://example.com'
curl: (28) Failed to connect to example.com port 443 after 5001 ms: Connection timed out

Identical symptom to the missing-route case — this is why the debugging order matters: route table, then security group, then NACL, because they all produce the same visible timeout from inside the instance.

5. Find every subnet in the VPC and whether each one is effectively public.
aws ec2 describe-route-tables --filters "Name=vpc-id,Values=vpc-0a1b2c3d4e5f60789" \
  --query 'RouteTables[].{Subnets:Associations[].SubnetId,HasIGW:Routes[?GatewayId!=`local`] | [0].GatewayId}'
[
    {
        "Subnets": ["subnet-0f1e2d3c4b5a67890"],
        "HasIGW": "igw-0d4e8f2a1c9b73065"
    },
    {
        "Subnets": ["subnet-0a9b8c7d6e5f43210"],
        "HasIGW": null
    }
]

The first subnet is public, the second is not — read directly off route tables rather than any label, since “public subnet” is not a real AWS attribute, just a description of this exact condition.

Next: RDS — launching a managed Postgres instance, connecting to it, and taking a snapshot.

Frequently Asked Questions

Why does this lesson need a real AWS account rather than LocalStack?
LocalStack Community accepts and stores VPC, subnet, and route table objects, but it does not simulate actual packet routing — an instance there can't demonstrate a real connectivity failure or fix. The debugging sequence in this lesson only means something against a real network path, so it uses a real AWS free-tier account.
What is the difference between a route table and a security group?
A route table decides where traffic is allowed to go at the network layer — it is what makes a subnet public or private, based on whether it has a route to an internet gateway. A security group decides what traffic is allowed to reach a specific instance, evaluated independently. A subnet can have a perfect route to the internet and an instance in it can still be completely unreachable if its security group denies everything — you need both correct.
Why do private subnets still need a route, just not to an internet gateway?
A private subnet's route table sends 0.0.0.0/0 to a NAT gateway instead of an internet gateway. This lets instances inside it initiate outbound connections (to download packages, call an API) while making it impossible for anything on the internet to initiate a connection inward — NAT only translates and forwards traffic that was requested from inside.
What is the most common reason an EC2 instance can't reach the internet?
In order of frequency: the subnet's route table has no route to an internet gateway, the instance doesn't have a public IP (or the subnet doesn't auto-assign one), and the security group or network ACL blocks the traffic. The debugging sequence in this lesson checks all three in that order because a missing route makes the other two irrelevant.