OnCallReady

Lesson 30.1 · AWS II: VPC, EC2, ELB & EKS · 17 min read

VPC design: CIDRs, subnets per AZ, route tables

In plain words

A VPC is a private plot of land in one AWS city, with an address range you choose. You split the plot into lots (subnets), and every lot sits in one of the city's separate districts (Availability Zones). Each lot gets a signpost (a route table) that says where roads lead. A lot whose signpost points the main road to the city gate (an internet gateway) is public; a lot without that sign is private.

Nothing about the lot itself says "public": only its signpost does.

VPC design: CIDRs, subnets per AZ, route tables

Everything in this chapter sits on a network: the servers, the load balancers, the Kubernetes nodes, even the pods. On AWS that network is a VPC (virtual private cloud), and most "the app cannot reach X" pages are a VPC design question in disguise. You know Azure's VNet; the ideas carry over, the details do not.

Need to know: a VPC is a private address range (a CIDR such as 10.20.0.0/16) in one Region. You cut it into subnets, each in exactly one Availability Zone. Every subnet uses a route table; a subnet is public only because its route table sends 0.0.0.0/0 to an internet gateway - there is no "public" switch on the subnet itself. Plan the CIDRs once: they cannot overlap with anything you will ever connect to, and a VPC's range can only be extended, never shrunk.

The pieces

ObjectWhat it isAzure
VPCyour address range in one RegionVNet
Subneta slice of it, in one AZsubnet (but Azure subnets span zones)
Route tablewhere traffic for each destination goes; one per subnet (the main one by default)route table / UDR
Internet gateway (IGW)the VPC's door to the internet, for addresses that have a public IPimplicit in Azure
NAT gatewayoutbound-only internet for private subnetsNAT gateway
Security groupstateful firewall on a network interfaceNSG on a NIC
Network ACLstateless firewall on a subnetNSG on a subnet (kind of)

The lab account has the default VPC every Region comes with (172.31.0.0/16, a public subnet in every AZ, an IGW already attached) and this lesson's own try-vpc:

$ aws ec2 describe-vpcs --query 'Vpcs[].[VpcId,CidrBlock,IsDefault,Tags[?Key==`Name`]|[0].Value]' --output table
----------------------------------------------------------------
|                         DescribeVpcs                         |
+------------------------+-----------------+--------+----------+
|  vpc-03f40e92832b5247c |  172.31.0.0/16  |  True  |  None    |
|  vpc-0fab6173e91281414 |  10.99.0.0/16   |  False |  try-vpc |
+------------------------+-----------------+--------+----------+

The default VPC is handy for experiments and wrong for production: every subnet is public, every instance gets a public IP by default, and its range overlaps with every other account's default VPC, so it can never be peered with them. Real workloads get their own VPC.

Planning the addresses

A VPC is between a /16 (65,536 addresses) and a /28 (16). Take a /16 per VPC unless you have a reason not to: addresses are free, re-addressing later is not. The rules that bite:

$ aws ec2 describe-subnets --filters Name=tag:Name,Values='try-*' --query 'sort_by(Subnets, &CidrBlock)[].[Tags[?Key==`Name`]|[0].Value,CidrBlock,AvailabilityZone,AvailableIpAddressCount,MapPublicIpOnLaunch]' --output table
---------------------------------------------------------------------
|                          DescribeSubnets                          |
+---------------+-----------------+-----------------+------+--------+
|  try-public-a |  10.99.0.0/24   |  eu-central-1a  |  251 |  True  |
|  try-public-b |  10.99.1.0/24   |  eu-central-1b  |  251 |  True  |
|  try-private-a|  10.99.10.0/24  |  eu-central-1a  |  251 |  False |
|  try-private-b|  10.99.11.0/24  |  eu-central-1b  |  251 |  False |
+---------------+-----------------+-----------------+------+--------+

AvailableIpAddressCount is 251 for each /24: the five reserved addresses are already gone.

A subnet cannot overlap another one in the VPC, and it must fit inside the VPC's range - the API says so directly:

$ VPC=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=try-vpc --query 'Vpcs[0].VpcId' --output text)
$ aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.99.0.128/25 --availability-zone eu-central-1a
aws: [ERROR]: An error occurred (InvalidSubnet.Conflict) when calling the CreateSubnet operation: The CIDR '10.99.0.128/25' conflicts with another subnet
$ aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.42.0.0/24 --availability-zone eu-central-1a
aws: [ERROR]: An error occurred (InvalidSubnet.Range) when calling the CreateSubnet operation: The CIDR '10.42.0.0/24' is invalid.

Route tables decide "public" and "private"

Every route table starts with the local route: the whole VPC CIDR goes to local, so every subnet can reach every other subnet (security groups and ACLs permitting). That route cannot be removed. Everything else you add:

$ aws ec2 describe-route-tables --filters Name=vpc-id,Values=$VPC --query 'RouteTables[].[Tags[?Key==`Name`]|[0].Value,Associations[0].Main,join(`, `, Routes[].DestinationCidrBlock)]' --output table
-----------------------------------------------------
|                DescribeRouteTables                |
+--------------+--------+---------------------------+
|  None        |  True  |  10.99.0.0/16             |
|  try-public  |  False |  10.99.0.0/16, 0.0.0.0/0  |
|  try-private |  False |  10.99.0.0/16             |
+--------------+--------+---------------------------+

Three route tables in try-vpc:

The longest prefix wins: a packet to 10.99.10.7 matches both 0.0.0.0/0 and 10.99.0.0/16, and the /16 (local) is more specific, so it stays inside the VPC.

$ aws ec2 describe-route-tables --filters Name=tag:Name,Values=try-public --query 'RouteTables[0].Routes[].[DestinationCidrBlock,GatewayId,State]' --output text
10.99.0.0/16	local	active
0.0.0.0/0	igw-0342e2f2941a7c782	active

A public subnet needs two things for an instance to be on the internet: the IGW route and a public IPv4 address on the instance (MapPublicIpOnLaunch on the subnet, or --associate-public-ip-address at launch). The IGW only translates addresses that have a public one. Since February 2024 every public IPv4 address costs money ($0.005 per hour), another reason to keep instances private and put a load balancer in front.

The usual layout

Three tiers, two (or three) AZs, a subnet per tier per AZ:

TierSubnetsRoute to the internetWhat lives there
publicpublic-a, public-bIGWload balancers, NAT gateways - nothing else
private (app)private-a, private-bNAT gatewayinstances, EKS nodes and pods
data (isolated)data-a, data-bnonedatabases, caches

Two AZs because one AZ can fail (it happens: a power or network event in one zone, the others fine), and every tier must survive that. The load balancer spans both public subnets, the app instances or nodes spread over both private subnets, the database is Multi-AZ across the data subnets.

In an interview: "What makes a subnet public in AWS?" - its route table has a 0.0.0.0/0 route to an internet gateway, and instances in it still need a public IP; private subnets reach out through a NAT gateway in a public subnet, or not at all.

DNS inside the VPC

The VPC router answers DNS at the base address +2 (10.99.0.2 here, also 169.254.169.253). Two attributes control it: enableDnsSupport (on by default) and enableDnsHostnames (off on a new VPC, on in the default one). Without hostnames instances get no ip-10-99-... names and interface endpoints cannot use private DNS - a lab later in this chapter trips over exactly that.

$ aws ec2 describe-vpc-attribute --vpc-id $VPC --attribute enableDnsHostnames
{
    "VpcId": "vpc-0fab6173e91281414",
    "EnableDnsHostnames": {
        "Value": true
    }
}

What you can do now

The lab builds the same layout from scratch with the CLI, then again in Terraform.

Why it helps

Most connectivity incidents on AWS come down to a route table nobody looked at, or an address plan that clashes with another network and cannot be fixed later. Knowing that the route table decides public or private, that each subnet loses five addresses, and why every tier needs two AZs means you can read any VPC in minutes and design one that survives a zone failure and a merger of networks.

Commands in this lesson

aws

FAQ

How big should a VPC be?

A /16 (65,536 addresses) is the usual choice: addresses cost nothing and re-addressing later costs a lot. The rule that matters is no overlaps with anything you will connect - other VPCs, offices, VPNs, the Kubernetes service range. Subnet sizes follow what runs inside: EKS pods take real VPC IPs, so node subnets are often /20 or larger.

Why does a /24 show 251 free addresses, not 256?

AWS keeps five in every subnet: the network address, the .1 router, the .2 DNS resolver, the .3 reserved address and the broadcast address. AvailableIpAddressCount already subtracts them. Small subnets feel this most: a /28 has 16 addresses but only 11 usable.

What is the main route table?

The route table created with the VPC. Every subnet not explicitly associated with another table uses it. Keep it local-only, with no route to the internet, so that a subnet someone forgets to associate ends up private, not public. Make separate tables for public and private subnets and associate them explicitly.

Why not just use the default VPC?

It is built for quick experiments: every subnet is public, instances get public IPs by default, and its 172.31.0.0/16 range is the same in every account, so it can never be peered with other default VPCs. Production workloads get their own VPC with private subnets and a deliberate address plan.

What is the difference from an Azure VNet?

The ideas are close, but subnets in AWS live in a single Availability Zone, while Azure subnets span the zones. So on AWS you create one subnet per tier per AZ. Routing is explicit through route tables, and an instance needs a public IP plus an internet gateway route to be reachable from outside.

In an interview Mid

What makes a subnet public in AWS?

Only its route table: a 0.0.0.0/0 route to an internet gateway. There is no public flag on the subnet itself. An instance there also needs a public IPv4 address, because the internet gateway only translates addresses that have one. Private subnets have no such route; they reach out through a NAT gateway in a public subnet, or not at all. Subnets that are not associated explicitly use the main route table, which should stay local-only.

Also asked: How do you plan the CIDR ranges for a new VPC? · Why should every tier have a subnet in at least two AZs? · What happens if two VPCs you want to connect have overlapping CIDRs?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.