Build a Virtual Private Cloud from scratch: define an IP range with CIDR, carve it into public and private subnets, route traffic with route tables and gateways, and control access with security groups.
A VPC (Virtual Private Cloud) is your own isolated network inside AWS — like having a private office building where you decide the room layout and who gets in. Why build one: every server, database, and load balancer lives inside a VPC, and the network design decides what can talk to what.
Create a VPC with the IP range 10.0.0.0/16 (~65,000 addresses). We capture the new VPC's id into a shell variable to reuse below.
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--query 'Vpc.VpcId' --output text)echo "Created $VPC_ID"Tag it with a friendly name so you can find it in the console
aws ec2 create-tags --resources $VPC_ID \
--tags Key=Name,Value=my-vpcCIDR (e.g. 10.0.0.0/16) is shorthand for a block of IP addresses. The "/16" is how many bits are fixed: a smaller number = a bigger block. /16 ≈ 65k addresses, /24 ≈ 256. Why: pick a private range (10.x, 172.16–31.x, or 192.168.x) big enough to split into subnets but not so big it overlaps networks you might later connect.
Inspect the VPC you just made
aws ec2 describe-vpcs --vpc-ids $VPC_ID \
--query 'Vpcs[0].CidrBlock'A handy way to see how many addresses a block holds:
/16 -> 65,536 addresses (whole VPC)
/24 -> 256 addresses (a typical subnet)
/28 -> 16 addresses (smallest AWS allows)
AWS reserves 5 addresses in every subnet, so a /24 gives you 251 usable.A subnet is a slice of your VPC's IP range that lives in one Availability Zone (a physical data center). The split that matters: a "public" subnet can reach the internet; a "private" one cannot. Why: put web servers in public subnets and databases in private ones, so the database is unreachable from outside.
A subnet for public-facing things (e.g. load balancers, web servers)
PUBLIC_SUBNET=$(aws ec2 create-subnet \
--vpc-id $VPC_ID --cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--query 'Subnet.SubnetId' --output text)A subnet for private things (e.g. databases)
PRIVATE_SUBNET=$(aws ec2 create-subnet \
--vpc-id $VPC_ID --cidr-block 10.0.2.0/24 \
--availability-zone us-east-1b \
--query 'Subnet.SubnetId' --output text)Auto-assign public IPs to anything launched in the public subnet
aws ec2 modify-subnet-attribute \
--subnet-id $PUBLIC_SUBNET --map-public-ip-on-launchA fresh VPC is sealed off. An Internet Gateway (IGW) is the door that lets traffic flow to and from the public internet. Why: without one attached, even a server with a public IP cannot be reached. You create it, then attach it to the VPC.
Create the gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--query 'InternetGateway.InternetGatewayId' --output text)Attach it to your VPC
aws ec2 attach-internet-gateway \
--internet-gateway-id $IGW_ID --vpc-id $VPC_IDA route table is a list of rules saying "traffic for this destination goes that way." Each subnet uses one. Why public vs private: a public subnet's table has a route sending internet-bound traffic (0.0.0.0/0) to the Internet Gateway; a private subnet's does not.
Create a route table for the public subnet
PUBLIC_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID \
--query 'RouteTable.RouteTableId' --output text)Rule: send all internet-bound traffic to the Internet Gateway
aws ec2 create-route --route-table-id $PUBLIC_RT \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW_IDAttach the table to the public subnet — now it's truly "public"
aws ec2 associate-route-table \
--route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNETSometimes a private server needs to reach OUT (to download updates) without being reachable from outside. A NAT Gateway allows that one-way flow. Note: it lives in a PUBLIC subnet and needs an Elastic IP. It costs money per hour and per GB — delete it when experimenting.
Allocate a fixed public IP for the NAT gateway
EIP_ALLOC=$(aws ec2 allocate-address --domain vpc \
--query 'AllocationId' --output text)Create the NAT gateway in the PUBLIC subnet
NAT_ID=$(aws ec2 create-nat-gateway \
--subnet-id $PUBLIC_SUBNET \
--allocation-id $EIP_ALLOC \
--query 'NatGateway.NatGatewayId' --output text)In the PRIVATE route table, send internet-bound traffic to the NAT
(assumes you created PRIVATE_RT like the public one above)
aws ec2 create-route --route-table-id $PRIVATE_RT \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_IDA security group is a firewall wrapped around a resource (like a server). It is "stateful": if you allow traffic in, the reply is automatically allowed out. Why: this is your main access control — e.g. allow web traffic on port 443 from anywhere, but SSH only from your office IP.
Create a security group inside the VPC
SG_ID=$(aws ec2 create-security-group \
--group-name web-sg --description "Web servers" \
--vpc-id $VPC_ID --query 'GroupId' --output text)Allow HTTPS (port 443) from anywhere
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 443 --cidr 0.0.0.0/0Allow SSH (port 22) only from one office IP — never open 22 to the world
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 22 --cidr 203.0.113.25/32