AWS · 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
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 IGW | Public IP | Reachable from internet | Can reach internet | |
|---|---|---|---|---|
| Public subnet | yes | yes | yes (if SG allows) | yes |
| Private subnet | no (routes via NAT) | no | no | yes, outbound only |
| Isolated subnet | no | no | no | no |
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.