What you will learn
- What a VPC is and why everything in AWS lives inside one
- How CIDR blocks and subnets carve up your network — and the 5 IPs AWS takes from every subnet
- What makes a subnet public vs private — the exact three things required
- How Bastion Hosts let you SSH into private servers
- How NAT Gateway gives private servers internet access without exposing them
- Security Groups vs NACLs — stateful vs stateless in plain language
- VPC Endpoints — how to reach S3 without going through the internet
- VPC Flow Logs for debugging network problems
Why this matters
Every resource you create in AWS — EC2, RDS, Lambda, ECS — lives inside a VPC. Get VPC wrong and you either expose your production database to the internet (a security disaster) or lock things down so tightly your application cannot reach the services it needs (an operational disaster).
At Swiggy, a misconfigured NACL during a network change silently dropped 30% of API traffic for 12 minutes. VPC Flow Logs identified the culprit in minutes. At Razorpay, routing S3 traffic through NAT Gateway instead of a VPC Endpoint was adding significant cost every month for traffic that never needed to touch the internet.
VPC is not a one-time setup. It is the foundation every other service sits on.
What is a VPC
A VPC (Virtual Private Cloud) is your own isolated private network inside AWS — completely separate from every other AWS customer. When you launch an EC2 instance or an RDS database, it lives inside your VPC and gets a private IP address that only resources inside the same VPC can reach by default.
AWS Account└── VPC (your private network) ├── Subnet A (Availability Zone 1a) │ └── EC2 instances, RDS, Lambda... └── Subnet B (Availability Zone 1b) └── EC2 instances, RDS, Lambda...When you open a new AWS account, AWS creates a default VPC in every region with everything pre-configured. It is fine for experimenting. In production, you create your own custom VPC and control everything yourself.
CIDR Blocks and Subnets
A VPC has a CIDR block — the pool of IP addresses your entire network can use. You pick this when creating the VPC.
AWS only allows three private IP ranges inside a VPC:
10.0.0.0/8 → 10.0.0.0 to 10.255.255.255172.16.0.0/12 → 172.16.0.0 to 172.31.255.255192.168.0.0/16 → 192.168.0.0 to 192.168.255.255A common production choice is 10.0.0.0/16 which gives you 65,536 IP addresses to distribute across subnets.
Subnets divide your VPC into smaller pieces
You cannot launch EC2 instances directly into a VPC. You launch them into subnets. A subnet is a slice of the VPC CIDR block, locked to one Availability Zone.
VPC: 10.0.0.0/16 (65,536 addresses)├── Subnet A (10.0.1.0/24) in ap-south-1a → 256 addresses├── Subnet B (10.0.2.0/24) in ap-south-1b → 256 addresses├── Subnet C (10.0.10.0/24) in ap-south-1a → 256 addresses└── Subnet D (10.0.11.0/24) in ap-south-1b → 256 addressesThe 5 reserved IPs — AWS takes these in every subnet
Using 10.0.1.0/24 as an example:
| Address | Reserved for |
|---|---|
| 10.0.1.0 | Network address — identifies the subnet itself |
| 10.0.1.1 | VPC Router — handles routing inside the VPC |
| 10.0.1.2 | AWS DNS server |
| 10.0.1.3 | Reserved by AWS for future use |
| 10.0.1.255 | Broadcast address |
A /24 subnet has 256 total addresses. Subtract 5 reserved = 251 usable.
RememberAlways size subnets bigger than you think you need. Once created, a subnet cannot be resized. If you need 29 EC2 instances and pick a /27 (32 - 5 = 27 usable), you are already stuck. Pick a /26 (64 - 5 = 59 usable) and have breathing room.
What Makes a Subnet Public or Private
This confuses a lot of engineers. A subnet does not magically become public just by existing. Three specific things must all be true for a subnet to be reachable from the internet.
A subnet is PUBLIC only if ALL THREE are true: 1. The VPC has an Internet Gateway attached 2. The subnet's Route Table has a route: 0.0.0.0/0 → Internet Gateway 3. The EC2 instance has a public IP or Elastic IP A subnet is PRIVATE if any one of those is missing Public Route Table: Destination Target 10.0.0.0/16 local ← stays inside VPC 0.0.0.0/0 igw-abc123 ← everything else goes to internet Private Route Table: Destination Target 10.0.0.0/16 local ← stays inside VPC 0.0.0.0/0 nat-abc123 ← everything else goes to NAT GatewayProduction setup always follows this pattern:
Public subnets → Load Balancers, NAT Gateways, Bastion HostsPrivate subnets → EC2 app servers, RDS databases, ElastiCacheYour EC2 instances and databases should never be directly reachable from the internet. Only the Load Balancer sits in the public subnet.
Common MistakeCreating an Internet Gateway and attaching it to the VPC but forgetting to add the 0.0.0.0/0 → IGW route to the Route Table. The gateway exists but traffic has no route to use it. Public instances cannot reach the internet and you spend 20 minutes wondering why. Always check Route Tables first when debugging connectivity.
Bastion Host — SSH into Private Instances
Your EC2 instances are in private subnets. They have no public IP. You cannot SSH into them directly from the internet. A Bastion Host solves this.
A Bastion Host is a small EC2 instance in the public subnet that acts as a jump server — the only entry point for SSH access.
Your laptop ↓ SSH on port 22Bastion Host (public subnet, has public IP) ↓ SSH on port 22Private EC2 (private subnet, no public IP)Security Group rules:
Bastion Security Group: Inbound: Allow port 22 from YOUR specific IP address only Outbound: Allow port 22 to Private EC2 Security Group Private EC2 Security Group: Inbound: Allow port 22 from Bastion Security Group ID (reference the SG itself, not an IP address)SecurityNever open SSH (port 22) from 0.0.0.0/0 on any Security Group — not even the Bastion. Automated scanners try port 22 on every public IP in the world within minutes of an instance launching. Restrict SSH to your specific IP only.
NAT Gateway — Private Servers Reaching the Internet
Private EC2 instances often need to reach the internet — to pull OS updates, download packages, call external payment APIs. But they cannot have a public IP or anyone could reach them.
NAT Gateway solves this. It sits in the public subnet and acts as a middleman.
Private EC2 (10.0.10.5) wants to reach api.razorpay.com ↓Request goes to NAT Gateway in public subnet ↓NAT Gateway replaces source IP with its own Elastic IP (e.g. 13.235.45.67) ↓Request reaches the internet ↓Response comes back to NAT Gateway's Elastic IP ↓NAT Gateway forwards it back to Private EC2 (10.0.10.5)The internet sees the NAT Gateway's IP, never the private EC2 IP. The private instance stays invisible.
Key facts about NAT Gateway:
| Property | Value |
|---|---|
| Where it lives | Public subnet (always) |
| Needs | An Elastic IP |
| Managed by | AWS — no patching needed |
| Scales automatically | Up to 100 Gbps |
| Cost | Per hour + per GB processed |
| High Availability | Create one per AZ |
One NAT Gateway per AZ — not just one total
Wrong — one NAT Gateway for everything: AZ-1a private instances → NAT GW (in AZ-1a) → internet ✓ AZ-1b private instances → NAT GW (in AZ-1a) → internet ✗ cross-AZ traffic costs money If AZ-1a goes down → AZ-1b private instances lose internet entirely Right — one NAT Gateway per AZ: AZ-1a private instances → NAT GW A (in AZ-1a) → internet AZ-1b private instances → NAT GW B (in AZ-1b) → internet If AZ-1a goes down → AZ-1b continues working normallySecurity Groups vs NACLs
Both control traffic but at different levels with different behaviours.
Security Groups — attached to instances, stateful
A Security Group is a firewall attached directly to EC2 instances, RDS databases, Lambda functions, and other resources. Allow rules only — there is no explicit deny.
Stateful means return traffic is automatically allowed:
User sends request to your server on port 443 ↓Security Group checks inbound rule → port 443 allowed → request passes ↓Server sends response back ↓Response automatically allowed outbound — no separate rule neededSecurity Group chaining:
Internet ↓ allow 80, 443 from 0.0.0.0/0ALB Security Group ↓ allow 8080 from ALB Security Group IDApp EC2 Security Group ↓ allow 5432 from App EC2 Security Group IDRDS Security GroupThe database only accepts connections from the app servers, not from the internet. The app servers only accept connections from the load balancer. The source is a Security Group ID — not an IP address. IPs change when instances restart. Security Group IDs never change.
NACLs — attached to subnets, stateless
Network ACLs apply to all traffic entering or leaving a subnet. They have both allow and deny rules. Rules are evaluated by number — lowest wins.
Stateless means return traffic is checked separately:
Request arrives on port 443 → inbound rule checked → ALLOWEDResponse leaves on ephemeral port 54321 → outbound rule checked separatelyIf no outbound rule allows port 54321 → response BLOCKEDThis is the ephemeral port trap. When a client connects to your server, the response goes back to a random high port (1024-65535) that the client chose. NACLs must explicitly allow outbound traffic on the full ephemeral port range.
NACL outbound rules for a web server: Allow TCP port 443 outbound Allow TCP ports 1024-65535 outbound (ephemeral ports)Quick comparison:
| Security Group | NACL | |
|---|---|---|
| Applied to | Instance | Subnet |
| Rules | Allow only | Allow and Deny |
| Stateful | Yes | No |
| Return traffic | Auto-allowed | Must explicitly allow |
| Block a specific IP | Cannot | Yes — add a Deny rule |
| Rule evaluation | All rules together | Lowest number first |
RememberUse NACLs when you need to block a specific IP address — Security Groups cannot do explicit deny. For everything else, Security Groups are simpler and correct. NACLs are a second layer of defence, not a replacement.
VPC Endpoints — Private Path to AWS Services
Without a VPC Endpoint, your private EC2 instances accessing S3 travel this path:
Private EC2 → NAT Gateway → internet → S3You pay NAT Gateway charges for traffic that never needed to touch the internet. S3 is an AWS service. AWS has a direct private path to it.
With a VPC Endpoint:
Private EC2 → VPC Endpoint → S3 (entirely inside AWS network)Two types of VPC Endpoints:
| Gateway Endpoint | Interface Endpoint | |
|---|---|---|
| Services | S3 and DynamoDB only | Most other AWS services |
| Cost | Free | Per hour + per GB |
| How it works | Entry added to your Route Table | Private IP created in your subnet |
TipCreate Gateway Endpoints for S3 and DynamoDB in every VPC from day one. They are completely free, more secure (traffic never leaves AWS), and eliminate NAT Gateway charges for S3 and DynamoDB traffic. For a data-heavy workload this can save significant money every month.
VPC Flow Logs — See All Network Traffic
VPC Flow Logs record every accepted and rejected connection in your VPC. They do not affect traffic — they only observe it.
Three things every Flow Log record tells you:
srcaddr → where the traffic came fromdstaddr → where it was goingaction → ACCEPT or REJECTThis is how you debug network problems:
Something is blocked but you cannot figure out why ↓Check Flow Logs and filter for REJECT ↓REJECT on inbound → Security Group or NACL blocking incoming trafficREJECT on outbound → Security Group or NACL blocking the responseACCEPT inbound but REJECT outbound → stateless NACL blocking response → add ephemeral port outbound ruleFlow Logs can go to CloudWatch Logs for real-time alerting or S3 for archiving and Athena analysis.
NAT Instance — The Old Way (and Why Source/Destination Check Matters)
Before NAT Gateway existed, teams used NAT Instances — regular EC2 instances configured to forward traffic on behalf of private subnet instances. Today NAT Gateway is always the better choice. But one concept from NAT Instance is tested everywhere: Source/Destination Check.
By default, every EC2 instance checks that it is the actual source or destination of any packet it receives. If the packet is not addressed to it, the instance drops it.
A NAT Instance needs to forward packets that are addressed to other destinations — that is its entire job. So Source/Destination Check must be disabled on a NAT Instance, otherwise it silently drops all the traffic it is supposed to forward.
NAT Instance with Source/Destination Check ON (wrong): Private EC2 sends packet to 8.8.8.8 via NAT Instance NAT Instance receives packet → checks if it is addressed to itself → no → drops it Private EC2 cannot reach the internet NAT Instance with Source/Destination Check OFF (correct): Private EC2 sends packet to 8.8.8.8 via NAT Instance NAT Instance receives packet → does not check → forwards it Private EC2 reaches the internetNAT Gateway handles this automatically. You never think about it. NAT Instance requires you to disable it manually.
| NAT Gateway | NAT Instance | |
|---|---|---|
| Managed by | AWS — fully managed | You — patch it, size it, monitor it |
| Availability | Highly available in its AZ | Manual setup with ASG needed |
| Bandwidth | Up to 100 Gbps, auto-scales | Limited by instance type |
| Source/Destination Check | Not applicable | Must be disabled manually |
| Use as Bastion | No | Yes |
| Cost | Per hour + per GB | EC2 instance cost |
Use NAT Gateway for everything new. NAT Instance is legacy.
VPC Peering — Connect Two VPCs Directly
VPC Peering creates a direct private connection between two VPCs. Traffic flows over AWS's private network — not the internet.
VPC A (10.0.0.0/16) ←——peering connection——→ VPC B (172.16.0.0/16)EC2 in VPC A can reach EC2 in VPC B using private IPsNo internet involvedTwo rules that catch engineers:
No transitive peering:
VPC A peered with VPC BVPC B peered with VPC C Can VPC A reach VPC C through VPC B?Answer: No. Peering is not transitive.To connect A and C — create a direct peering between A and C.No overlapping CIDRs:
VPC A: 10.0.0.0/16VPC B: 10.0.0.0/16 ← same rangeCannot peer — AWS cannot route between identical address spaces Fix: Plan CIDRs before creating VPCs.If you need to peer later, non-overlapping ranges are a hard requirement.For connecting many VPCs at scale, Transit Gateway is cleaner than maintaining many peering connections between every pair.
Networking Cost — What Is Free and What Is Not
AWS charges for data transfer in ways that surprise teams. Understanding the rules saves real money.
Free: Traffic within the same AZ using private IP addresses → always free Traffic from internet into AWS (ingress) → free Traffic between services using private IPs in the same AZ → free Charged: Traffic between AZs → $0.01 per GB each way Traffic going out to the internet → $0.09 per GB (first 10 TB/month) NAT Gateway processing → $0.045 per GB Example — EC2 to RDS in different AZs: EC2 in ap-south-1a → RDS in ap-south-1b Every GB transferred costs $0.02 (both directions count) Fix: run EC2 and RDS in the same AZ for read-heavy workloads Example — EC2 to S3 through NAT Gateway: Private EC2 → NAT Gateway → S3 NAT Gateway charges $0.045 per GB processed Fix: S3 Gateway Endpoint is free, eliminates NAT charges for S3 entirelyTipThe biggest unexpected AWS networking bill is usually NAT Gateway processing fees for S3 and DynamoDB traffic. Gateway Endpoints for both services are free to create and eliminate that cost entirely. Create them on day one in every VPC.
Hands-on Lab — Build a VPC from Scratch in the Console
This lab creates a production-style VPC with public and private subnets, NAT Gateway, and an S3 VPC Endpoint — entirely through the console.
Step 1 — Create the VPC
VPC → Your VPCs → Create VPCName: devops-prod-vpcIPv4 CIDR: 10.0.0.0/16Click: Create VPC After creation:VPC → Your VPCs → devops-prod-vpc → Actions → Edit VPC SettingsEnable: DNS hostnames ✓Step 2 — Create 4 subnets
VPC → Subnets → Create Subnet → select devops-prod-vpc Create all 4:Name: public-1a CIDR: 10.0.1.0/24 AZ: ap-south-1aName: public-1b CIDR: 10.0.2.0/24 AZ: ap-south-1bName: private-1a CIDR: 10.0.10.0/24 AZ: ap-south-1aName: private-1b CIDR: 10.0.11.0/24 AZ: ap-south-1b For the two public subnets:Select subnet → Actions → Edit subnet settings → Enable auto-assign public IPv4Step 3 — Create and attach Internet Gateway
VPC → Internet Gateways → Create Internet GatewayName: devops-igw → Create Select devops-igw → Actions → Attach to VPC → select devops-prod-vpcStep 4 — Create public Route Table
VPC → Route Tables → Create Route TableName: public-rt VPC: devops-prod-vpc Select public-rt → Routes → Edit routes → Add route:Destination: 0.0.0.0/0 Target: devops-igw → Save Select public-rt → Subnet Associations → Edit → check public-1a and public-1bStep 5 — Create NAT Gateways (one per AZ)
VPC → NAT Gateways → Create NAT GatewayName: nat-gw-1a Subnet: public-1a Elastic IP: Allocate Elastic IP → Create Repeat for AZ-1b:Name: nat-gw-1b Subnet: public-1b Elastic IP: Allocate Elastic IP → Create Wait for both to show Status: Available (takes 1-2 minutes)Step 6 — Create private Route Tables
VPC → Route Tables → Create Route TableName: private-rt-1a VPC: devops-prod-vpc Select private-rt-1a → Routes → Add route:Destination: 0.0.0.0/0 Target: nat-gw-1a → Save Subnet Associations → Edit → check private-1a Repeat for AZ-1b:Name: private-rt-1b, 0.0.0.0/0 → nat-gw-1b, associate with private-1bStep 7 — Create free S3 VPC Endpoint
VPC → Endpoints → Create EndpointService category: AWS servicesSearch: s3 → select com.amazonaws.ap-south-1.s3 → Type: GatewayVPC: devops-prod-vpcRoute Tables: select private-rt-1a and private-rt-1b → Create AWS automatically adds a route for S3 in both private Route Tables.S3 traffic from private subnets now goes directly — no NAT Gateway needed.Step 8 — Enable VPC Flow Logs
VPC → Your VPCs → devops-prod-vpc → Flow Logs → Create Flow LogFilter: AllDestination: Send to CloudWatch LogsLog group: /vpc/devops-prod-vpc → CreateCleanup order (order matters)
Delete NAT Gateways → wait until deleted (2-3 min)Release Elastic IPsDelete VPC EndpointsDetach and delete Internet GatewayDelete subnets → delete Route Tables → delete VPCCommon Mistakes to Avoid
Common MistakePutting EC2 instances in a public subnet when they should be in private. A public subnet means your instance has a public IP and is directly reachable from the internet. Your app servers and databases should always be in private subnets behind a Load Balancer.
Common MistakeOne NAT Gateway for the whole VPC. If that NAT Gateway's AZ has a problem, all private instances in all other AZs lose internet access. One NAT Gateway per AZ is the correct production setup. Yes it costs more. An outage costs more.
Common MistakeUsing NAT Gateway to reach S3 from private EC2. NAT Gateway charges per GB processed. The S3 Gateway Endpoint is completely free. This is one of the most common unnecessary AWS costs and takes 2 minutes to fix.
TipThe quickest way to debug a network problem is to look at the error type. Connection timeout means something is blocking the packet — check Security Groups and NACLs. Connection refused means the packet reached the server but nothing is listening on that port — the Security Group is fine, the problem is the application. These two errors tell you exactly where to look.
TipThe AWS VPC wizard can create a standard public-private VPC setup for you automatically. For production, always build manually so you understand exactly what was created and why. Understanding the VPC is not optional — every networking issue you ever debug traces back to it.