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
| Object | What it is | Azure |
|---|---|---|
| VPC | your address range in one Region | VNet |
| Subnet | a slice of it, in one AZ | subnet (but Azure subnets span zones) |
| Route table | where 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 IP | implicit in Azure |
| NAT gateway | outbound-only internet for private subnets | NAT gateway |
| Security group | stateful firewall on a network interface | NSG on a NIC |
| Network ACL | stateless firewall on a subnet | NSG 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:
- No overlaps with anything you will connect: other VPCs (peering, Transit Gateway), the office or data centre (VPN, Direct Connect), the Kubernetes service range.
10.0.0.0/16in every account is the classic mistake that blocks a merger of networks two years later. - Each subnet loses 5 addresses: the network address,
.1(the VPC router),.2(the DNS resolver),.3(reserved) and the broadcast address. A/24gives 251 usable. - Size subnets for what runs in them. EKS pods get VPC addresses (the EKS lesson), so a node group's subnets need room for every pod, not just every node:
/20or/19for those.
$ 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 main route table (created with the VPC,
Main = True): local only. Every subnet that is not explicitly associated with another table uses it. Keep it boring - then a forgotten subnet is private, not public. try-public: local +0.0.0.0/0 -> igw-.... The public subnets are associated with it.try-private: local only for now; the next lesson gives it a route to a NAT gateway.
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:
| Tier | Subnets | Route to the internet | What lives there |
|---|---|---|---|
| public | public-a, public-b | IGW | load balancers, NAT gateways - nothing else |
| private (app) | private-a, private-b | NAT gateway | instances, EKS nodes and pods |
| data (isolated) | data-a, data-b | none | databases, 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
- read any VPC: its CIDR, subnets per AZ, which route table each subnet uses and why that makes it public or private;
- plan addresses that will not collide (a
/16per VPC, subnets sized for pods, five addresses per subnet gone); - explain why production does not use the default VPC.
The lab builds the same layout from scratch with the CLI, then again in Terraform.