Creating a Custom VPC (Step by Step)
A VPC (Virtual Private Cloud) is your own private, isolated network inside AWS where your servers, databases, and load balancers live. Before you can launch almost anything useful in AWS, you need a network for it to run in. This page walks you through building a real-world VPC from scratch: two subnets that face the internet, two that stay private, an internet gateway, a NAT for outbound traffic, and the route tables that tie it all together. We will do it twice, once in the AWS Management Console and once with the AWS CLI, so you understand exactly what each click is doing under the hood.
What we are building
We will create a VPC spanning two Availability Zones (AZs, which are physically separate data centers in the same region). Spreading across two AZs means if one data center has an outage, your app keeps running in the other.
| Component | CIDR / detail | Purpose |
|---|---|---|
| VPC | 10.0.0.0/16 | The overall private network (65,536 addresses) |
| Public subnet A | 10.0.0.0/24 in AZ-a | Internet-facing resources (load balancers, NAT) |
| Public subnet B | 10.0.1.0/24 in AZ-b | Same, second AZ for redundancy |
| Private subnet A | 10.0.2.0/24 in AZ-a | App servers and databases, no direct internet |
| Private subnet B | 10.0.3.0/24 in AZ-b | Same, second AZ |
| Internet gateway (IGW) | one per VPC | Lets public subnets reach the internet |
| NAT gateway | in a public subnet | Lets private subnets make outbound calls only |
A “public subnet” is simply a subnet whose route table sends 0.0.0.0/0 (all internet traffic) to an internet gateway. A “private subnet” sends that traffic to a NAT instead. The subnets themselves are identical; the route table is what makes them public or private.
Option 1: the Console “VPC and more” wizard (recommended)
The fastest, safest path is the built-in wizard. It creates everything and, crucially, wires the route tables to the right subnets for you.
- Open the VPC console and click Create VPC.
- Choose VPC and more (not “VPC only”).
- Under Name tag auto-generation, enter a project name like
devcraftly. - Set IPv4 CIDR block to
10.0.0.0/16. - Set Number of Availability Zones to
2. - Set Number of public subnets to
2and Number of private subnets to2. - Under NAT gateways, choose 1 per AZ for production, or In 1 AZ to save money in dev.
- Leave VPC endpoints as None unless you need private S3 access.
- Click Create VPC.
The wizard provisions the VPC, four subnets, an internet gateway, NAT gateway(s), and the route tables, and it associates each route table with the correct subnets automatically. This last step is the one people forget when building by hand.
Cost note: An internet gateway is free. A NAT gateway is not, it costs roughly $0.045 per hour (about $32/month) plus $0.045 per GB of data processed in most regions. Two NAT gateways double that. In dev environments, use a single NAT gateway in one AZ.
Option 2: building it by hand with the AWS CLI
Doing it manually shows you every moving part. Run these with AWS CLI v2.
Step 1: create the VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=devcraftly-vpc}]'
Output:
{
"Vpc": {
"VpcId": "vpc-0a1b2c3d",
"CidrBlock": "10.0.0.0/16",
"State": "pending"
}
}
Step 2: create the subnets
Pick two AZs in your region (for example us-east-1a and us-east-1b).
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d \
--cidr-block 10.0.0.0/24 --availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-a}]'
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d \
--cidr-block 10.0.2.0/24 --availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-a}]'
Repeat for the second AZ with 10.0.1.0/24 (public-b) and 10.0.3.0/24 (private-b).
Output:
{
"Subnet": {
"SubnetId": "subnet-0a1b2c3d",
"CidrBlock": "10.0.0.0/24",
"AvailabilityZone": "us-east-1a",
"State": "available"
}
}
Step 3: create and attach the internet gateway
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=devcraftly-igw}]'
aws ec2 attach-internet-gateway \
--internet-gateway-id igw-0a1b2c3d --vpc-id vpc-0a1b2c3d
Output:
{
"InternetGateway": {
"InternetGatewayId": "igw-0a1b2c3d",
"Attachments": []
}
}
Step 4: create a NAT gateway
A NAT gateway needs an Elastic IP (a permanent public IP address) and lives in a public subnet.
aws ec2 allocate-address --domain vpc
aws ec2 create-nat-gateway \
--subnet-id subnet-0a1b2c3d \
--allocation-id eipalloc-0a1b2c3d \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=devcraftly-nat}]'
Step 5: create route tables and add routes
# Public route table -> internet gateway
aws ec2 create-route-table --vpc-id vpc-0a1b2c3d
aws ec2 create-route --route-table-id rtb-0aaaa111 \
--destination-cidr-block 0.0.0.0/0 --gateway-id igw-0a1b2c3d
# Private route table -> NAT gateway
aws ec2 create-route-table --vpc-id vpc-0a1b2c3d
aws ec2 create-route --route-table-id rtb-0bbbb222 \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-0a1b2c3d
Step 6: associate route tables with subnets (do not skip this)
This is the step hand-built VPCs forget. Creating a route is not enough, you must associate the table with each subnet.
# Public subnets -> public route table
aws ec2 associate-route-table --route-table-id rtb-0aaaa111 --subnet-id subnet-0a1b2c3d
aws ec2 associate-route-table --route-table-id rtb-0aaaa111 --subnet-id subnet-0b2c3d4e
# Private subnets -> private route table
aws ec2 associate-route-table --route-table-id rtb-0bbbb222 --subnet-id subnet-0c3d4e5f
aws ec2 associate-route-table --route-table-id rtb-0bbbb222 --subnet-id subnet-0d4e5f6a
Output:
{
"AssociationId": "rtbassoc-0a1b2c3d",
"AssociationState": {
"State": "associated"
}
}
The #1 gotcha: If an instance “can’t reach the internet,” check route table associations first. A subnet that is not explicitly associated falls back to the VPC’s main route table, which usually has no internet route. The fix is always Step 6, associate the subnet with a route table that has a
0.0.0.0/0route.
Finally, enable auto-assign public IPs on the public subnets so instances launched there get a public address:
aws ec2 modify-subnet-attribute --subnet-id subnet-0a1b2c3d --map-public-ip-on-launch
Console vs CLI vs IaC, when to use which
| Approach | Best for | Trade-off |
|---|---|---|
| Console wizard | First-time setup, learning, one-off VPCs | Not repeatable or version-controlled |
| AWS CLI | Scripting, understanding internals | Easy to miss a step (e.g. associations) |
| CloudFormation / Terraform | Production, repeatable environments | More upfront effort to author |
For real projects, define the VPC as code so it is reproducible:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "devcraftly-vpc" }
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
Terraform makes the association an explicit resource, so you cannot forget it.
Best practices
- Use the Console “VPC and more” wizard or IaC, not hand-built CLI, for anything beyond learning, it wires associations for you.
- Always spread subnets across at least two AZs for high availability.
- Keep databases and app servers in private subnets; only put load balancers and NAT in public subnets.
- Leave room in your CIDR plan (a
/16VPC with/24subnets) so you can add subnets later without overlap. - In dev, use a single NAT gateway to cut costs; in production, run one NAT gateway per AZ to avoid a single point of failure.
- After building, verify every subnet’s route table association before launching workloads.
- Enable VPC Flow Logs early so you can debug connectivity later.