Networking basics · Lesson 7 of 12 · about 30 minutes

Cloud networks: VPCs, subnets and gateways

How the ideas from Lessons 2 to 6 become a network in the cloud: what a VPC is, how to pick its range and split it into public and private subnets, what route tables, internet gateways and NAT gateways do, and how to plan one yourself with a short Python script.

By the end you'll know

  • What a VPC is, and how it relates to your home network
  • How to choose a VPC's range, and split it into subnets across zones
  • What makes a subnet public or private: its route table
  • What an internet gateway and a NAT gateway do, and what they cost
  • How to plan a VPC's subnets before you build anything

Where you are

Lesson 6 ended at your home network: devices with private addresses, a router with a routing table, a default route to the internet, and NAT so that private devices can reach out. Every one of those pieces was a physical box, or ran on one.

A cloud network is exactly the same set of ideas, built out of software. You don't plug in a router; you create one by clicking a button or writing a few lines of Terraform. This lesson uses AWS (Amazon Web Services), because it's the most common and its pieces are spelled out clearly, but every major cloud has the same parts under slightly different names.

A VPC is a private network you rent

Picture an office building rented by one company. The building has its own walls, so other tenants nearby can't wander in. Inside are floors, each used for a different team. There's a front door with a reception desk for visitors, and a mailroom at the back that sends post out but never lets strangers in.

A VPC (virtual private cloud) is that building: a private network inside a cloud provider, walled off from every other customer. You give it one range of addresses, written in CIDR from Lesson 3, and everything you put in it, like servers, databases and Kubernetes nodes, gets a private address from that range.

Most people choose a /16 from the private ranges in Lesson 2, like 10.0.0.0/16: 65,536 addresses, plenty of room to grow. AWS lets a VPC be anything from /16 down to /28. Every AWS region even comes with a default VPC already made, using 172.31.0.0/16, a piece of the 172.16.0.0/12 private range that ended at 31 in Lesson 3.

Choosing the range matters more than it looks, for the reason from Lesson 3's VPN case: ranges that might ever need to connect must not overlap. If your VPC is 10.0.0.0/16, your office network, your other VPCs and any partner you'll link to should all use something else. Changing a VPC's range later means rebuilding nearly everything inside it, so this is a choice to make once and carefully.

Subnets, spread across zones

The floors of the building are subnets: smaller ranges carved out of the VPC's range, exactly like the /16 split into /24s in Lesson 3. Every server goes in one subnet, and subnets are how you give different groups of servers different rules.

Each AWS region is split into several availability zones, or AZs: separate data centres, far enough apart that a flood or power cut in one shouldn't touch the others. In AWS, each subnet lives in exactly one zone. So the usual pattern is to make matching subnets in at least two zones, and run copies of each server in both: if one zone has a bad day, the other keeps going.

Try sizing a VPC with the slider from Lesson 3. This one knows AWS keeps 5 addresses in every subnet: the first and last, as in Lesson 3, plus three more for its router, its DNS server and future use.

Try it: drag the prefix

network bits: 16, fixedhost bits: 16, free to change
Addresses in the range
65,536
Usable for devices
65,531
First address
10.0.0.0
Last address
10.0.255.255
Subnet mask
255.255.0.0
The range, written out
10.0.0.0/16

Big enough to split into many /24 subnets, like a whole cloud network.

"Usable" here leaves out 5 addresses per subnet, which this cloud keeps for itself.

At /16, the whole VPC, there are 65,531 addresses to use. Drag to /24, a typical subnet: 251. Drag to /28, the smallest AWS allows: only 11, which is the "too small" case from Lesson 3.

Public and private subnets

Here's the most important idea in cloud networking. Subnets come in two kinds:

  • A public subnet can reach the internet directly, and be reached from it. It's for the few things that must face the world, like load balancers (Lesson 10).
  • A private subnet has no direct route in from the internet. It's for everything else: application servers, databases, Kubernetes nodes.

What makes the difference isn't a setting called "public". It's the subnet's route table, the same kind of table you read on your Mac in Lesson 6. Every subnet is attached to one, and its rules decide where data goes.

A public subnet's route table:

DestinationTargetMeans
10.0.0.0/16localAnything in the VPC: deliver it directly
0.0.0.0/0internet gatewayEverything else: out to the internet

And a private subnet's:

DestinationTargetMeans
10.0.0.0/16localAnything in the VPC: deliver it directly
0.0.0.0/0NAT gatewayEverything else: out through NAT, replies only

0.0.0.0/0 is CIDR for "every address": a /0 fixes no bits at all. It's the default route from Lesson 6, written as a range. The local route is added by AWS automatically, so everything inside the VPC can always reach everything else inside it, just like 192.168.1 on your Mac. The only difference between the two tables is the last line, and that one line is what makes a subnet public or private.

Gateways: the front door and the mailroom

The two targets in those tables are the building's front door and its mailroom.

An internet gateway is the front door. Attach one to a VPC, point a route table at it, and the subnets using that table are connected to the internet. A server there still needs a public address of its own to be reachable, which AWS can give it automatically when it starts. There's no charge for the gateway itself.

A NAT gateway is the mailroom, and it's exactly your home router's NAT from Lesson 6. It sits in a public subnet with a public address. Servers in private subnets send their outgoing traffic to it, through that 0.0.0.0/0 route, and it swaps their private addresses for its public one, keeps track of the swap, and passes replies back. They can download updates or call other APIs, but nothing on the internet can start a conversation with them, because, as in Lesson 6, there's nothing in the NAT table for it.

Unlike the internet gateway, a NAT gateway costs money: an hourly charge just for existing, plus a charge for every gigabyte through it. It's one of the most common surprise lines on a first AWS bill, so delete it when you tear down a practice VPC, and check the provider's current pricing before you build one.

A VPC with public and private subnetsA VPC with the range 10.0.0.0/16 spans two availability zones, a and b. Each zone has a public subnet, 10.0.0.0/24 and 10.0.1.0/24, and a private subnet, 10.0.10.0/24 and 10.0.11.0/24. An internet gateway at the top connects the public subnets to the internet. A NAT gateway sits in public subnet a. The application servers in the private subnets send outgoing traffic to the NAT gateway, which sends it out through the internet gateway. Nothing from the internet can reach the private subnets directly.The internetVPC 10.0.0.0/16Internet gatewayzone azone bPublic subnet10.0.0.0/24NAT gatewayPublic subnet10.0.1.0/24Private subnet10.0.10.0/24app servers, databasePrivate subnet10.0.11.0/24copies of the app serversPrivate subnets reach out through the NAT gateway. Nothing reaches in.
A common layout: a public and a private subnet in each of two zones. Only the public subnets route to the internet gateway; the private ones send everything outward through the NAT gateway. Real setups often add a second NAT gateway in zone b, so one zone's trouble can't cut off the other.

Plan it before you build it

Picking subnet ranges by hand is where mistakes creep in, so plan them in code first. This extends cidr.py from Lesson 3. Create plan_vpc.py:

PYTHON
import sys
import ipaddress

vpc = ipaddress.ip_network(sys.argv[1])
size = int(sys.argv[2])
pieces = list(vpc.subnets(new_prefix=size))

plan = {
    "public-a": pieces[0],
    "public-b": pieces[1],
    "private-a": pieces[10],
    "private-b": pieces[11],
}

print("VPC", vpc, "has", len(pieces), f"/{size} subnets to choose from")
for name, subnet in plan.items():
    usable = subnet.num_addresses - 5
    print(f"{name:10} {str(subnet):16} {usable} usable on AWS")

vpc.subnets(new_prefix=size) splits the VPC into pieces of the size you ask for, like the /24s in Lesson 3, and list turns them into a list you can pick from by position. The plan uses the first two pieces for public subnets, and skips ahead to the eleventh and twelfth for private ones, so there's room to add more public subnets later without the numbers getting tangled. {name:10} in the f-string pads the name to 10 characters, so the columns line up. Run it:

BASH
python3 plan_vpc.py 10.0.0.0/16 24
OUTPUT
VPC 10.0.0.0/16 has 256 /24 subnets to choose from
public-a   10.0.0.0/24      251 usable on AWS
public-b   10.0.1.0/24      251 usable on AWS
private-a  10.0.10.0/24     251 usable on AWS
private-b  10.0.11.0/24     251 usable on AWS

That's the layout in the diagram. Now suppose you'll run Kubernetes, where every pod can take an address from the subnet on AWS. 251 might not be enough, so try bigger subnets:

BASH
python3 plan_vpc.py 10.0.0.0/16 20
OUTPUT
VPC 10.0.0.0/16 has 16 /20 subnets to choose from
public-a   10.0.0.0/20      4091 usable on AWS
public-b   10.0.16.0/20     4091 usable on AWS
private-a  10.0.160.0/20    4091 usable on AWS
private-b  10.0.176.0/20    4091 usable on AWS

Sixteen times the room per subnet, but only 16 subnets in total. That trade-off, between many small subnets and a few big ones, is the main decision in planning a VPC.

The same thing, in Terraform

If you did the Terraform track, here's how the first public subnet looks as code. Read it rather than run it: a real apply would create resources in an AWS account, and with a NAT gateway, cost money.

HCL
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public_a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
}

resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a.id
  route_table_id = aws_route_table.public.id
}

Every idea from this lesson is there: the VPC's range, a subnet carved from it in one zone, map_public_ip_on_launch to give its servers public addresses, the internet gateway, a route table with the 0.0.0.0/0 default route, and the association that attaches the table to the subnet. The references between them, like aws_vpc.main.id, are the dependencies from Terraform Lesson 2, and building the other three subnets is a job for for_each from Terraform Lesson 6.

Other clouds

The names change, the ideas don't:

  • Google Cloud also calls it a VPC, but one VPC can span every region, with subnets inside each region.
  • Azure calls it a VNet (virtual network), with subnets inside.
  • DigitalOcean keeps it simpler: a VPC is one private range in one region, and servers and Kubernetes nodes created there get private addresses from it automatically.

In my real setup

CivicGrid runs on DigitalOcean's Kubernetes service, and the cluster's servers sit in a DigitalOcean VPC, talking to each other on private addresses. Nothing in the cluster is reachable from the internet at all, not even the front door. Instead, a small pod inside opens a connection outward to Cloudflare, called a Cloudflare Tunnel, and visitors' requests come back in along it, the way replies come back through NAT in Lesson 6. The database is Supabase, which lives outside my VPC entirely, so the API reaches it across the internet, encrypted, which is Lesson 9. The pattern is the one in this lesson: as little as possible public, and everything else private.

Break it on purpose

Three cases. Work out the cause before you read the explanation.

A plan that doesn't fit. Run the planner with a VPC that's too small for it:

BASH
python3 plan_vpc.py 10.0.0.0/24 26
OUTPUT
Traceback (most recent call last):
  ...
IndexError: list index out of range

A /24 split into /26s makes only four pieces, numbered 0 to 3, and the plan asked for piece 10. The script is telling you the VPC is too small for the plan. Real tools give you the cloud version of this: "the subnet range is not within the VPC range" or "it conflicts with another subnet". Check the numbers before you build, and leave room.

A private server that can't get updates. You create a server in a private subnet, and its software update times out. It can reach other servers in the VPC fine. What's missing? Its route table has the local route but no 0.0.0.0/0 route, so anything outside the VPC has nowhere to go. That's Lesson 6's computer with no default route. The fix is a NAT gateway in a public subnet, and a 0.0.0.0/0 route to it in the private subnet's table. And if it reaches the internet but still fails, it's probably a firewall rule, which is Lesson 8.

Two VPCs that can't be joined. Your team has two VPCs, both created from the same Terraform module with its default range, 10.0.0.0/16. Now they need to talk, so you try to peer them: connect two VPCs directly, so they can reach each other on private addresses. The cloud refuses. Check why:

BASH
python3 -c "import ipaddress as i; print(i.ip_network('10.0.0.0/16').overlaps(i.ip_network('10.0.0.0/16')))"
OUTPUT
True

The ranges overlap, so an address like 10.0.5.20 could mean a server in either VPC, and routers can't tell which. It's Lesson 3's VPN problem again, in the cloud. The fix is to rebuild one VPC with a different range, like 10.1.0.0/16. Which is why the range is worth choosing carefully, and why a module's default range should be something you override, not something you accept.

Where you are now

You can design a cloud network: a VPC with a range that won't clash, public and private subnets across zones, route tables that decide which is which, an internet gateway for the front door and a NAT gateway for outgoing traffic. What you haven't done yet is control who's allowed in. A public subnet with a route to the internet will carry any traffic that's sent to it. Lesson 8 is firewalls and security groups: the rules that decide which of that traffic gets through.

Check yourself

  1. What is a VPC?

  2. What makes a subnet public?

  3. What does 0.0.0.0/0 mean in a route table?

  4. Why put servers in a private subnet behind a NAT gateway?

  5. Why spread subnets across two availability zones?

  6. Two VPCs are both 10.0.0.0/16. What goes wrong?

Key terms

  • VPC (virtual private cloud): a private network inside a cloud provider, with one address range.
  • Availability zone (AZ): a separate data centre within a region. Each AWS subnet lives in one.
  • Public subnet: a subnet whose route table sends 0.0.0.0/0 to an internet gateway.
  • Private subnet: a subnet with no direct route from the internet.
  • Route table: the rules a subnet follows to decide where data goes.
  • Internet gateway: the VPC's front door to the internet.
  • NAT gateway: lets private subnets reach out, without letting anything in. Charged by the hour and by the gigabyte.
  • Peering: connecting two VPCs directly. Their ranges can't overlap.

Go deeper