VPC networking
Subnets, routing, NAT, security groups.
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 (us-east-1) and tag itaws 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-1aaws 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.
aws ec2 create-internet-gateway \--query InternetGateway.InternetGatewayId --output textigw-0a72c1b9e8f3d4a56aws ec2 attach-internet-gateway --internet-gateway-id igw-0a72c1b9e8f3d4a56 \--vpc-id vpc-0f3a9c2d41b7e8a15aws ec2 create-route-table --vpc-id vpc-0f3a9c2d41b7e8a15 \--query RouteTable.RouteTableId --output textrtb-0e4f5a6b7c8d9e0f1aws 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 publicaws 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:
# Elastic IP for the NAT gatewayaws ec2 allocate-address --query AllocationId --output texteipalloc-07c3d2e1f0a9b8c7d# The NAT gateway goes in the PUBLIC subnetaws 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 allaws ec2 create-route-table --vpc-id vpc-0f3a9c2d41b7e8a15 \--query RouteTable.RouteTableId --output textrtb-03b6d9e8f7a6c5d40aws ec2 create-route --route-table-id rtb-03b6d9e8f7a6c5d40 \--destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-054e8c1b2a3f4d5e6aws ec2 associate-route-table --route-table-id rtb-03b6d9e8f7a6c5d40 \--subnet-id subnet-0c9a8b7f6e5d4c3b2
--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.
# One SG per tier; the db SG admits Postgres only from the app SGaws ec2 create-security-group --group-name app-sg --description "App tier" \--vpc-id vpc-0f3a9c2d41b7e8a15 --query GroupId --output textsg-0d1c2b3a4f5e6d7c8aws ec2 create-security-group --group-name db-sg --description "Data tier" \--vpc-id vpc-0f3a9c2d41b7e8a15 --query GroupId --output textsg-0aa11b22c33d44e55aws 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.
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.
aws ec2 describe-vpcs --filters Name=tag:Name,Values=lab-vpc \--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock}' --output tableaws ec2 describe-route-tables --filters Name=vpc-id,Values=vpc-0f3a9c2d41b7e8a15 \--query 'RouteTables[].{Rtb:RouteTableId,Routes:Routes[].{D:DestinationCidrBlock,G:GatewayId,N:NatGatewayId}}' --output jsonaws ec2 describe-security-groups --filters Name=vpc-id,Values=vpc-0f3a9c2d41b7e8a15 \--query 'SecurityGroups[].{Name:GroupName,Id:GroupId}' --output table
-----------------------------| 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 pathapp-sg | sg-0d1c2b3a4f5e6d7c8db-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.