The goal
The VPC wizard creates everything for you, but you don't learn what each part does. Here you'll build a small VPC by hand. It has one public subnet and one private subnet, and when you finish you'll know what really makes a subnet "public".
You need the AWS CLI with a working profile. If not, read AWS CLI first steps. Check where you are first:
aws sts get-caller-identity
aws configure get region
What you're building
VPC 10.0.0.0/16
├─ Public subnet 10.0.1.0/24 → route 0.0.0.0/0 to the internet gateway
└─ Private subnet 10.0.2.0/24 → local routes only
| Part | What it does |
|---|---|
| VPC | Your private network, with an address range |
| Subnet | A slice of that range, in one Availability Zone |
| Route table | Decides where a subnet's traffic goes |
| Internet gateway | The VPC's door to the internet |
Step 1: create the VPC
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=lab-vpc}]' \
--query Vpc.VpcId --output text)
echo "$VPC_ID"
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames '{"Value":true}'
The second command gives instances with public IPs a public DNS name.
Choose your range with care. It can't overlap with any network you may want to connect to later, such as your office or another VPC. You also can't change a VPC's primary range afterward, although you can add secondary ones.
Step 2: create two subnets
AZ=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[0].ZoneName' --output text)
PUB_SUBNET=$(aws ec2 create-subnet --vpc-id "$VPC_ID" \
--cidr-block 10.0.1.0/24 --availability-zone "$AZ" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=lab-public}]' \
--query Subnet.SubnetId --output text)
PRIV_SUBNET=$(aws ec2 create-subnet --vpc-id "$VPC_ID" \
--cidr-block 10.0.2.0/24 --availability-zone "$AZ" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=lab-private}]' \
--query Subnet.SubnetId --output text)
AWS reserves 5 addresses in every subnet, so a /24 gives you 251 usable ones.
At this point, both subnets are identical. Neither can reach the internet.
Step 3: add an internet gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=lab-igw}]' \
--query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
Attaching it doesn't change any traffic. No subnet has a route to it yet.
Step 4: make one subnet public
A subnet is public because its route table has a route to an internet gateway. Nothing else defines it. There's no "public" setting.
PUB_RT=$(aws ec2 create-route-table --vpc-id "$VPC_ID" \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=lab-public-rt}]' \
--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_ID"
aws ec2 associate-route-table --route-table-id "$PUB_RT" --subnet-id "$PUB_SUBNET"
Have new instances in that subnet receive a public IP automatically:
aws ec2 modify-subnet-attribute --subnet-id "$PUB_SUBNET" --map-public-ip-on-launch
The private subnet has no route table of its own, so it uses the VPC's main route table, which has only the local route. To reach the internet from there, it would need a NAT gateway, which costs money, so we won't add one in this lab.
Step 5: check your work
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[].{id:SubnetId, cidr:CidrBlock, az:AvailabilityZone, free:AvailableIpAddressCount, publicIp:MapPublicIpOnLaunch}' \
--output table
aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'RouteTables[].{id:RouteTableId, main:Associations[0].Main, routes:Routes[].[DestinationCidrBlock,GatewayId]}'
You should see a local route in each table, and 0.0.0.0/0 pointing to igw-... in the public one only.
To test it, launch a small instance in the public subnet, using your first EC2 instance as a guide.
Why can't my instance reach the internet?
Internet access needs all of these:
| Check | How to see it |
|---|---|
| Internet gateway attached to the VPC | aws ec2 describe-internet-gateways |
| Route to the gateway in the subnet's route table | aws ec2 describe-route-tables |
| Public IP or Elastic IP on the instance | aws ec2 describe-instances |
| Security group allows the traffic | See security groups vs NACLs |
| Network ACL allows both directions | Same post |
To find out which one is failing, use Reachability Analyzer.
Step 6: clean up
Dependencies have to go first, or you'll get a DependencyViolation error. Terminate any instances, then:
aws ec2 delete-subnet --subnet-id "$PUB_SUBNET"
aws ec2 delete-subnet --subnet-id "$PRIV_SUBNET"
aws ec2 delete-route-table --route-table-id "$PUB_RT"
aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID"
aws ec2 delete-vpc --vpc-id "$VPC_ID"
Shell variables don't survive a new terminal. If you've lost them, look up the IDs by the tags you set. If delete-vpc still fails, see what's left:
aws ec2 describe-network-interfaces --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'NetworkInterfaces[].{id:NetworkInterfaceId, what:Description}' --output table
Caution
Check the IDs before running these commands. Deleting a route table or internet gateway in a live VPC takes down traffic right away.
Prevent it next time
- Plan address ranges across all your VPCs before you build, and leave room to grow.
- Use at least two Availability Zones for anything real, as a subnet lives in just one. See Multi-AZ design.
- Tag everything with a name and an owner.
- Build production networks with infrastructure as code, not by hand. See ClickOps to IaC.