Home / AWS

AWSHow-toBeginner

Build your first AWS VPC with the CLI: subnets, route tables and an internet gateway

Create a VPC one step at a time so you understand each part. Add a public and a private subnet, an internet gateway and a route table, check what makes a subnet public, then clean it up in the right order.

Deepak Bhardwaj5 min read

Applies toAmazon VPC, AWS CLI v2

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.