VPC networking

Subnets, routing, NAT, security groups.

Intermediate35 min · lesson 7 of 15

A VPC (Virtual Private Cloud) is a fenced plot of land you lease inside the city that AWS (Amazon Web Services) runs. The Region supplies the roads, the power and the water. Inside your fence you draw the floor plan yourself: rooms (*subnets*), doors (*gateways*), signposts telling traffic which door to use (*route tables*), and guards checking IDs at every doorway (*security groups* and *network ACLs*, short for access control lists). Nothing walks in or out unless you drew a path for it. That is why the network comes first. Every service you deploy later in this course sits inside the plot you are about to draw.

Every VPC starts with a CIDR block (Classless Inter-Domain Routing), which is the street-number range for your plot. It looks like 10.0.0.0/16. The /16 means the first 16 bits are fixed and the rest are yours to hand out, which works out to 65,536 IP (Internet Protocol) addresses. Pick from the RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), the blocks the internet agrees never to route publicly. Then check your choice overlaps neither your office network nor any VPC you might connect to later. You can bolt on secondary CIDRs afterwards. You can never renumber, and two VPCs whose ranges overlap can never be peered, so a careless 10.0.0.0/16 today turns into a migration project in two years. One piece of geography to hold on to: a VPC stretches across every Availability Zone (AZ, a separate cluster of data centers inside the Region), while each subnet you carve out of it sits in exactly one AZ.

Subnets: slicing the plot into rooms

A subnet is one room in the plot. It is a slice of the VPC's range, anywhere from /28 (16 addresses) up to /16 (the whole thing), pinned to a single AZ. Whether that room counts as "public" or "private" is not a checkbox you tick. It falls out of routing, which you wire up in the next section. One sizing detail the exam likes to poke at: AWS keeps five addresses in every subnet for itself (the network address, the VPC router, DNS (Domain Name System), one held in reserve, and broadcast). So a /24 gives you 251 usable IPs, not 254. Build the skeleton:

create the VPC and subnets
# Create the VPC (us-east-1) and tag it
aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=lab-vpc}]' \
--query 'Vpc.{Id:VpcId,Cidr:CidrBlock,State:State}'
{
"Id": "vpc-0f3a9c2d41b7e8a15",
"Cidr": "10.0.0.0/16",
"State": "pending"
}
# One public and one private subnet in us-east-1a
aws ec2 create-subnet --vpc-id vpc-0f3a9c2d41b7e8a15 \
--cidr-block 10.0.1.0/24 --availability-zone us-east-1a \
--query 'Subnet.{Id:SubnetId,Cidr:CidrBlock,Usable:AvailableIpAddressCount}'
{
"Id": "subnet-0b1de2f3a4c5d6e70",
"Cidr": "10.0.1.0/24",
"Usable": 251
}
aws ec2 create-subnet --vpc-id vpc-0f3a9c2d41b7e8a15 \
--cidr-block 10.0.11.0/24 --availability-zone us-east-1a
# → subnet-0c9a8b7f6e5d4c3b2 (repeat the pair in us-east-1b for HA)

Routing is what makes a subnet public

Each subnet is attached to exactly one route table, the signpost at the end of the road. It is a short list of destination → target entries, nothing more. Every table is born with a local route you cannot delete, and that route is what lets anything inside the VPC's range reach anything else inside it. An internet gateway (IGW) is the front door in the fence. AWS runs it, it scales sideways on its own, and it does one-to-one address translation between an instance's private IP and its public or Elastic IP. Here is the whole trick. A subnet turns *public* the moment its route table sends 0.0.0.0/0 (meaning everywhere no other route covers) to that gateway. Nothing else about the subnet changes.

internet gateway and public route
aws ec2 create-internet-gateway \
--query InternetGateway.InternetGatewayId --output text
igw-0a72c1b9e8f3d4a56
aws ec2 attach-internet-gateway --internet-gateway-id igw-0a72c1b9e8f3d4a56 \
--vpc-id vpc-0f3a9c2d41b7e8a15
aws ec2 create-route-table --vpc-id vpc-0f3a9c2d41b7e8a15 \
--query RouteTable.RouteTableId --output text
rtb-0e4f5a6b7c8d9e0f1
aws ec2 create-route --route-table-id rtb-0e4f5a6b7c8d9e0f1 \
--destination-cidr-block 0.0.0.0/0 --gateway-id igw-0a72c1b9e8f3d4a56
{
"Return": true
}
aws ec2 associate-route-table --route-table-id rtb-0e4f5a6b7c8d9e0f1 \
--subnet-id subnet-0b1de2f3a4c5d6e70
# Verify: the local route came free; your 0.0.0.0/0 makes it public
aws ec2 describe-route-tables --route-table-ids rtb-0e4f5a6b7c8d9e0f1 \
--query 'RouteTables[0].Routes[].{Dest:DestinationCidrBlock,Target:GatewayId,State:State}' \
--output table
---------------------------------------------------------
| DescribeRouteTables |
+--------------+--------------------------+-------------+
| Dest | Target | State |
+--------------+--------------------------+-------------+
| 10.0.0.0/16 | local | active |
| 0.0.0.0/0 | igw-0a72c1b9e8f3d4a56 | active |
+--------------+--------------------------+-------------+

Flip that around and you get the other half. A subnet with no route to an internet gateway is private, by definition. That is where app servers and databases belong. Someone scanning the whole internet has nowhere to land, because no path exists that ends on those machines. Putting a database in a public subnet *and* handing it a public IP is the classic first mistake. Scanners sweep the entire address space around the clock, so an exposed listener gets found and probed within minutes of coming up.

The NAT gateway: a one-way door out

Private instances still need to pull package updates and call outside APIs. A NAT gateway (Network Address Translation) does that job, and it works like a hotel switchboard. Calls go out under the hotel's number, the operator writes down who placed each one, and the replies come back to the right room. Nobody outside can dial a room directly. The gateway's outside number is an Elastic IP, a fixed public IPv4 address you allocate and keep. AWS manages the box, it scales on its own from 5 to 100 Gbps (gigabits per second), and it has to sit in a *public* subnet so it has its own way out. Your private route table then points 0.0.0.0/0 at it:

NAT gateway for the private tier
# Elastic IP for the NAT gateway
aws ec2 allocate-address --query AllocationId --output text
eipalloc-07c3d2e1f0a9b8c7d
# The NAT gateway goes in the PUBLIC subnet
aws ec2 create-nat-gateway --subnet-id subnet-0b1de2f3a4c5d6e70 \
--allocation-id eipalloc-07c3d2e1f0a9b8c7d --connectivity-type public \
--query 'NatGateway.{Id:NatGatewayId,State:State}'
{
"Id": "nat-054e8c1b2a3f4d5e6",
"State": "pending"
}
aws ec2 wait nat-gateway-available --nat-gateway-ids nat-054e8c1b2a3f4d5e6 # ~2 min
# Private route table: default route -> NAT, and no IGW route at all
aws ec2 create-route-table --vpc-id vpc-0f3a9c2d41b7e8a15 \
--query RouteTable.RouteTableId --output text
rtb-03b6d9e8f7a6c5d40
aws ec2 create-route --route-table-id rtb-03b6d9e8f7a6c5d40 \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-054e8c1b2a3f4d5e6
aws ec2 associate-route-table --route-table-id rtb-03b6d9e8f7a6c5d40 \
--subnet-id subnet-0c9a8b7f6e5d4c3b2
The VPC you just built, layer by layer
VPC edge (Region)
Internet gateway
One-to-one address translation at the fence; a subnet is public only once its route table sends 0.0.0.0/0 here
Route tables
The free local route wires the VPC together; whatever 0.0.0.0/0 points at decides public or private
Public subnets (per AZ)
Load balancers / entry points
The only doors you opened on purpose, forwarding traffic inward
NAT gateway + Elastic IP
Outbound only for the private tier; run one per AZ so one bad zone cannot cut off the rest
Private subnets (per AZ)
App servers
No internet gateway route, so scanners have nothing that terminates on them
Databases
Never a public subnet, never a public IP. This is the classic first mistake.
Private rails to AWS
Gateway endpoint
S3 and DynamoDB only; a free route-table entry that keeps traffic off NAT and on the AWS backbone
Interface endpoint (PrivateLink)
A private-IP network card covering nearly every other service; bills per hour plus per GB
Two guards run across every layer: security groups (stateful, one per instance, able to point at other security groups) on each server, and network ACLs (stateless, one per subnet, allow and deny) at each subnet edge.
One NAT gateway hides two problems: a single point of failure and a bill
A standard NAT gateway lives in one AZ. Send every private subnet in the VPC through a single NAT sitting in AZ-a, and the afternoon AZ-a has trouble, outbound traffic dies in *all* your zones at once, healthy ones included. The production pattern, and the answer the exam wants, is one NAT gateway per AZ with each zone's private route table pointing at its own. (AWS now also sells a *regional* NAT gateway, created with --availability-mode regional, that spreads across AZs by itself. The zonal kind shown above is still the default.) Watch the meter as well. In us-east-1 you pay $0.045 per hour plus $0.045 for every GB (gigabyte) processed. Push terabytes of S3 traffic through NAT when a free gateway endpoint would have carried it, and you have written one of the most common surprise lines on an AWS bill.

Two guards: security groups and network ACLs

A security group is a bouncer standing at the door of one machine, and this bouncer has a memory. Allow inbound tcp/5432, the port PostgreSQL listens on, and the replies flow back out by themselves, because the group remembers every connection it let in. That memory is what *stateful* means. Anything you did not allow is denied, and there are no deny rules and no rule numbers to reason about. The habit that marks an experienced engineer: point a security group at *another security group* rather than at an IP range. "The database accepts Postgres from the app tier" then stays true no matter how many app servers come and go or which addresses they land on. A network ACL, usually shortened to NACL, is the mirror image on every axis the exam likes to test. It is *stateless*, so it remembers nothing and return traffic needs a rule of its own, including the ephemeral port range 1024–65535 that client machines pick from. It guards a whole subnet instead of one machine, it reads rules in number order and stops at the first match, and it can deny as well as allow. The default NACL waves everything through. Leave it coarse and save it for the one job security groups cannot do, like shutting the door on a hostile IP range.

security groups that reference each other
# One SG per tier; the db SG admits Postgres only from the app SG
aws ec2 create-security-group --group-name app-sg --description "App tier" \
--vpc-id vpc-0f3a9c2d41b7e8a15 --query GroupId --output text
sg-0d1c2b3a4f5e6d7c8
aws ec2 create-security-group --group-name db-sg --description "Data tier" \
--vpc-id vpc-0f3a9c2d41b7e8a15 --query GroupId --output text
sg-0aa11b22c33d44e55
aws ec2 authorize-security-group-ingress --group-id sg-0aa11b22c33d44e55 \
--protocol tcp --port 5432 --source-group sg-0d1c2b3a4f5e6d7c8
{
"Return": true,
"SecurityGroupRules": [
{
"SecurityGroupRuleId": "sgr-0f9e8d7c6b5a43210",
"GroupId": "sg-0aa11b22c33d44e55",
"GroupOwnerId": "111122223333",
"IsEgress": false,
"IpProtocol": "tcp",
"FromPort": 5432,
"ToPort": 5432,
"ReferencedGroupInfo": {
"GroupId": "sg-0d1c2b3a4f5e6d7c8",
"UserId": "111122223333"
}
}
]
}

Staying off the internet: endpoints, peering, Transit Gateway

Call S3 (Simple Storage Service, the object store) from an instance and the traffic takes the internet-facing path by default, even though both ends sit inside AWS. It is like posting a letter to the office next door by walking it to a public mailbox. A VPC endpoint is the internal mail chute instead. Two kinds exist, and the difference shows up on the exam and on the bill. Gateway endpoints cover S3 and DynamoDB only, cost nothing, and appear as one entry in a route table. Interface endpoints (AWS PrivateLink) drop a network card (an ENI, elastic network interface) with a private IP into your subnet, reach nearly every other AWS service, and charge per hour plus per GB. For VPC-to-VPC traffic, VPC peering is a cheap direct link between two of them, but it is non-transitive: peer A↔B and B↔C, and A still cannot reach C. Past a handful of VPCs that becomes a mesh nobody wants to maintain, so you move up to a Transit Gateway, the regional hub every VPC plugs into. One last switch to flip: VPC Flow Logs (aws ec2 create-flow-logs) record which traffic was accepted and which was rejected. That log is how you fix a security group at 2 a.m. with evidence instead of guesses.

free S3 gateway endpoint
aws ec2 create-vpc-endpoint --vpc-id vpc-0f3a9c2d41b7e8a15 \
--service-name com.amazonaws.us-east-1.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-03b6d9e8f7a6c5d40 \
--query 'VpcEndpoint.{Id:VpcEndpointId,Type:VpcEndpointType,State:State}'
{
"Id": "vpce-01a2b3c4d5e6f7a8b",
"Type": "Gateway",
"State": "available"
}
# S3 traffic from the private tier now rides the AWS backbone — $0, no NAT

That is the network every later lesson deploys into: public subnets holding only the doors you opened on purpose, private subnets holding everything worth stealing, a stateful guard on each instance, and private rails to the AWS services you call all day. Next comes the public face that sits on top of it. Route 53 answers DNS queries, CloudFront caches content at the edge, and Elastic Load Balancers stand in exactly the public subnets you built here, passing traffic inward to targets that never touch the internet themselves.

Public and private are facts about routing, not name tags. Default route to an internet gateway means public. Default route to a NAT gateway, or no way out at all, means private. A subnet labeled "private" that still carries an IGW route is how databases end up being scanned.

Security groups are stateful allow-lists bolted to an instance's network interface (ENI). NACLs are stateless fences around a subnet, carrying both allow and deny. Wire security groups to reference each other so tiers scale without you chasing IP ranges, and keep NACLs blunt. Block a bad prefix with them. Do not rebuild your whole security-group matrix as numbered rules.

Plan your address ranges as though you will peer later, because you will. Handing every account the same 10.0.0.0/16 turns peering and Transit Gateway into a renumbering exercise. Reserve a block per account and per environment up front, even if today you only need two subnets.

Flow Logs turn "the connection timed out" into evidence. Switch them on before the incident, not during it. They are boring right up until the night they save you. Pair them with VPC endpoints so private workloads reach S3 and SSM (Systems Manager) without paying the NAT toll.

One NAT gateway per AZ is the boring production answer for the zonal kind. Regional NAT exists now, but the exam and most existing designs still expect you to understand AZ scope. Keep an eye on S3 traffic crossing NAT, too. A gateway endpoint is free and is usually the right call.

Write down the failure you actually lose sleep over, not the one on the vendor slide. Then check that the architecture, the alarm and the runbook all name that failure in the same words.

Try this

Point the AWS CLI (command line interface) at a lab VPC you already have. List its subnets and route tables, then work out for yourself which subnets carry a default route to an internet gateway. Every command here is read-only, and doing it once makes the public/private rule stick far better than reading about it.

terminal
aws ec2 describe-vpcs --filters Name=tag:Name,Values=lab-vpc \
--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock}' --output table
aws ec2 describe-route-tables --filters Name=vpc-id,Values=vpc-0f3a9c2d41b7e8a15 \
--query 'RouteTables[].{Rtb:RouteTableId,Routes:Routes[].{D:DestinationCidrBlock,G:GatewayId,N:NatGatewayId}}' --output json
aws ec2 describe-security-groups --filters Name=vpc-id,Values=vpc-0f3a9c2d41b7e8a15 \
--query 'SecurityGroups[].{Name:GroupName,Id:GroupId}' --output table
output
-----------------------------
| DescribeVpcs |
+----------+----------------+
| Cidr | Id |
+----------+----------------+
| 10.0.0.0/16 | vpc-0f3a9c...|
+----------+----------------+
# Routes showing 0.0.0.0/0 -> igw-... mark public subnets
# Routes showing 0.0.0.0/0 -> nat-... mark private egress path
app-sg | sg-0d1c2b3a4f5e6d7c8
db-sg | sg-0aa11b22c33d44e55

Takeaway

Routing decides public or private. Security groups decide who is allowed to talk to whom. NAT gateways and endpoints decide how the private tier reaches the outside world and the AWS APIs. Get those three right and most of the rest is detail.

Next: sketch your own VPC with one public and two private subnets in every AZ, a NAT gateway per AZ, and an S3 gateway endpoint. Then go hunting for any IGW route still hanging off the data tier and delete it.

Quick check
01You spread private subnets across three AZs (a, b and c). To hold costs down, you send all of their outbound traffic (0.0.0.0/0) through one standard NAT gateway sitting in AZ-a's public subnet. What is the main risk you have taken on?
Incorrect — A standard NAT gateway is scoped to a single AZ, not to the Region. Only the newer --availability-mode regional variant spreads across zones on its own.
Incorrect — NAT works outbound only. It tracks the connections your instances open and refuses anything dialed in from outside, so it never makes a private instance reachable.
Correct — A standard NAT gateway lives in one AZ, so funneling every zone through it turns that zone into a single point of failure. The production pattern, and the exam answer, is one NAT gateway per AZ with each private route table pointing at its own.
Incorrect — A NAT gateway belongs in a public subnet, because it needs an internet gateway route for its Elastic IP. That placement is a requirement, not the flaw.
02Which statement correctly separates a security group from a network ACL (NACL) inside a VPC?
Incorrect — That has the two swapped. Security groups are the stateful layer; NACLs are the stateless one read in rule-number order.
Incorrect — A NACL cannot reference a security group at all. Only a security group can reference another security group, and NACLs are stateless.
Incorrect — Their scopes are the other way round: security groups sit on instances, NACLs sit on subnets.
Correct — Stateful, allow-only security groups on the instance; stateless allow-and-deny NACLs at the subnet edge, exactly as the lesson describes.
03Instances in a private subnet run a nightly job that copies several terabytes into an Amazon S3 bucket in the same Region. That traffic currently leaves through a NAT gateway (Network Address Translation), and the per-GB processing charge now dominates the monthly bill. What is the MOST cost-effective way to keep the copy running while cutting that charge?
Correct — A gateway endpoint for S3 costs nothing, drops into the route table as a single entry, and keeps the traffic on the AWS backbone instead of the metered NAT gateway.
Incorrect — Interface endpoints bill per hour and per GB, so they cost more than the free gateway endpoint that S3 already supports.
Incorrect — That hands the instances to internet scanners and still runs up data-transfer charges. It is a security regression, not a fix.
Incorrect — Two NAT gateways double the hourly charge and each one still bills per GB processed, so the total does not fall.

Related