Lab: AWS VPC with public and private subnets and an EC2 instance
In short: A VPC is your own private network in AWS. You build one with a public subnet that can reach the internet and a private subnet that cannot, launch an instance in each, prove the difference with routes and SSH, and then delete everything in the right order.
Aim and what you need
The aim is to understand what really makes a subnet "public": a route to an internet gateway (IGW) plus a public IP address. A VPC (virtual private cloud) is an isolated network you define with a CIDR block. EC2 (Elastic Compute Cloud) instances are the virtual machines that sit in it.
- An AWS account with root MFA, an admin IAM user and a budget (see the IAM and MFA lab).
- The AWS CLI v2 and an SSH client. Your public IP address, written as
<your-ip>/32. - About 60 minutes. Small instances may be covered by your free offer; public IPv4 addresses are charged hourly. This lab deliberately avoids a NAT gateway, which is billed by the hour.
Steps
- Create the VPC and two subnets in the same availability zone, and save the IDs in shell variables.
- Create and attach an internet gateway, then a route table for the public subnet with a default route to it.
- Turn on automatic public IPs for the public subnet only.
- Create two security groups: one for the web host allowing SSH only from your IP, one for the app host allowing SSH only from the web host's group.
- Create a key pair and launch one instance in each subnet using the latest Amazon Linux 2023 image.
export AWS_REGION=eu-west-2
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=lab-vpc},{Key=project,Value=lab}]' --query Vpc.VpcId --output text)
PUB=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.1.0/24 --availability-zone eu-west-2a --query Subnet.SubnetId --output text)
PRIV=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.2.0/24 --availability-zone eu-west-2a --query Subnet.SubnetId --output text)
IGW=$(aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
RT=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW
aws ec2 associate-route-table --route-table-id $RT --subnet-id $PUB
aws ec2 modify-subnet-attribute --subnet-id $PUB --map-public-ip-on-launch
SGW=$(aws ec2 create-security-group --group-name lab-web-sg --description "SSH from my IP" --vpc-id $VPC --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SGW --protocol tcp --port 22 --cidr <your-ip>/32
SGA=$(aws ec2 create-security-group --group-name lab-app-sg --description "SSH from web tier" --vpc-id $VPC --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SGA --protocol tcp --port 22 --source-group $SGW
aws ec2 create-key-pair --key-name lab-key --key-type ed25519 --query KeyMaterial --output text > lab-key.pem
chmod 400 lab-key.pem
AMI=$(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query Parameter.Value --output text)
aws ec2 run-instances --image-id $AMI --instance-type t3.micro --subnet-id $PUB --security-group-ids $SGW --key-name lab-key --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=lab-web},{Key=project,Value=lab}]'
aws ec2 run-instances --image-id $AMI --instance-type t3.micro --subnet-id $PRIV --security-group-ids $SGA --key-name lab-key --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=lab-app},{Key=project,Value=lab}]'
How to verify
List the instances: the web instance should have a public IP and the app instance only a private 10.0.2.x address. SSH to the web instance with ssh -i lab-key.pem ec2-user@<web-public-ip>, then use it as a jump host: ssh -i lab-key.pem -J ec2-user@<web-public-ip> ec2-user@<app-private-ip>. From the app host, curl -I https://example.com should fail, which proves the private subnet has no internet route. Compare the two route tables with aws ec2 describe-route-tables --filters Name=vpc-id,Values=$VPC: only the public one has 0.0.0.0/0 to the IGW. An SSH attempt from any other IP should time out because of the security group.
Troubleshooting and clean-up
- SSH times out to the web host: check the route table association, the public IP, and that your current IP matches the security group rule.
- Permission denied (publickey): wrong user name (Amazon Linux uses
ec2-user) or key file permissions too open. - Private host needs updates: in real designs you add a NAT gateway or VPC endpoints, but both cost money, so skip them here.
Delete in reverse order, because AWS will not delete a VPC that still contains resources. Then check the console in this region shows no instances, volumes, Elastic IPs or VPC called lab-vpc.
aws ec2 terminate-instances --instance-ids <web-id> <app-id>
aws ec2 wait instance-terminated --instance-ids <web-id> <app-id>
aws ec2 delete-security-group --group-id $SGA
aws ec2 delete-security-group --group-id $SGW
aws ec2 delete-key-pair --key-name lab-key
aws ec2 disassociate-route-table --association-id <association-id>
aws ec2 delete-route-table --route-table-id $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 $PUB
aws ec2 delete-subnet --subnet-id $PRIV
aws ec2 delete-vpc --vpc-id $VPC
Practice
What makes a subnet public in AWS?
Why does the app security group reference the web security group instead of an IP?
Why does this lab avoid a NAT gateway?
Key takeaways
- A public subnet is defined by its route to an internet gateway.
- Security groups are stateful and can reference other groups.
- Reach private instances through a jump host or Session Manager.
- NAT gateways and public IPv4 addresses cost money in labs.
- Delete in reverse order and check the region is empty.
Original content written by Mohamed Fayas for FM TecHub. © 2026 FM TecHub. You're welcome to share a link to this page; please don't copy the text elsewhere without permission.