Skip to content
AWS aws networking 6 min read

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.

ComponentCIDR / detailPurpose
VPC10.0.0.0/16The overall private network (65,536 addresses)
Public subnet A10.0.0.0/24 in AZ-aInternet-facing resources (load balancers, NAT)
Public subnet B10.0.1.0/24 in AZ-bSame, second AZ for redundancy
Private subnet A10.0.2.0/24 in AZ-aApp servers and databases, no direct internet
Private subnet B10.0.3.0/24 in AZ-bSame, second AZ
Internet gateway (IGW)one per VPCLets public subnets reach the internet
NAT gatewayin a public subnetLets 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.

The fastest, safest path is the built-in wizard. It creates everything and, crucially, wires the route tables to the right subnets for you.

  1. Open the VPC console and click Create VPC.
  2. Choose VPC and more (not “VPC only”).
  3. Under Name tag auto-generation, enter a project name like devcraftly.
  4. Set IPv4 CIDR block to 10.0.0.0/16.
  5. Set Number of Availability Zones to 2.
  6. Set Number of public subnets to 2 and Number of private subnets to 2.
  7. Under NAT gateways, choose 1 per AZ for production, or In 1 AZ to save money in dev.
  8. Leave VPC endpoints as None unless you need private S3 access.
  9. 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/0 route.

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

ApproachBest forTrade-off
Console wizardFirst-time setup, learning, one-off VPCsNot repeatable or version-controlled
AWS CLIScripting, understanding internalsEasy to miss a step (e.g. associations)
CloudFormation / TerraformProduction, repeatable environmentsMore 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 /16 VPC with /24 subnets) 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.
Last updated June 15, 2026
Was this helpful?