Why VPC Design is the Foundation That Makes or Breaks Everything
Every AWS resource lives inside a VPC. Get the design wrong and you face:
- IP exhaustion during auto-scaling — pods cannot schedule, instances cannot launch
- Costly re-architecture — you cannot resize a subnet; you must recreate everything
- Security gaps — flat networks where a compromised workload reaches every database
- Compliance failures — auditors asking why production and dev share network space
This guide gives you the knowledge to design VPCs that last years without re-architecture.
---
The 5 Critical Decisions in VPC Design
Decision 1: VPC CIDR Block Size
Your VPC CIDR defines the total IP pool. Once set, the primary CIDR cannot be changed.
Common CIDR blocks and their capacity:
| CIDR | Total IPs | Usable IPs | Best For |
|---|---|---|---|
| /16 | 65,536 | 65,531 | Production VPCs, EKS clusters, large workloads |
| /20 | 4,096 | 4,091 | Small/medium workloads, dev environments |
| /24 | 256 | 251 | Tiny VPCs, single-purpose networks |
Rule of thumb: Always use /16 for production VPCs. IP addresses are free — running out of them is expensive. A /16 gives you 65,000+ addresses, enough for years of growth.
AWS reserves 5 IPs per subnet: first 4 and last 1 (network address, VPC router, DNS, reserved, broadcast). A /24 subnet gives 251 usable IPs, not 256.
Private IP ranges (RFC 1918):
- 10.0.0.0/8 — 16.7M addresses (recommended for enterprise)
- 172.16.0.0/12 — 1M addresses
- 192.168.0.0/16 — 65K addresses (avoid — conflicts with home networks)
Critical rule: If you plan VPC peering or Transit Gateway, CIDRs cannot overlap. Plan non-overlapping addressing across all VPCs from day one.
Example enterprise scheme:
Production: 10.0.0.0/16
Staging: 10.1.0.0/16
Development: 10.2.0.0/16
Shared: 10.3.0.0/16
DR: 10.4.0.0/16
Decision 2: Subnet Tiers (Public, Private, Isolated)
A production VPC needs three tiers:
Public Subnets — Direct internet access:
- Application Load Balancers (ALBs)
- NAT Gateways
- Bastion hosts (if used)
- NOT application servers or databases
Private Subnets — Outbound internet via NAT, no inbound:
- Application servers (EC2, ECS tasks, EKS pods)
- Workers and background processors
- Internal microservices
Isolated/Data Subnets — No internet access at all:
- RDS databases
- ElastiCache clusters
- Internal-only services
This three-tier model ensures defense-in-depth.
Decision 3: Availability Zones
Always deploy across 3 AZs for production. With 3 tiers x 3 AZs = 9 subnets minimum.
Decision 4: Subnet Sizing (Where Most Teams Fail)
Subnet size determines how many resources can exist in that subnet. If EKS pods auto-scale and the subnet runs out of IPs, scaling fails silently — no new pods, no error in the application, just degraded performance until someone notices.
The EKS IP consumption problem:
Each EKS pod gets its own IP address (AWS VPC CNI). A single EC2 node running 30 pods consumes 30+ IPs from the subnet. A cluster with 10 nodes and 30 pods each = 300+ IPs just for pods. During a traffic spike that triggers HPA scaling to 100 pods per node across 20 nodes = 2,000 IPs consumed instantly.
If your private subnet is a /24 (251 usable IPs), you run out with just 8 nodes.
Recommended subnet sizes:
| Subnet Type | Recommended Size | Usable IPs | Reasoning |
|---|---|---|---|
| Public | /24 | 251 | Only ALBs and NAT GWs — few IPs needed |
| Private (EKS/ECS) | /19 | 8,187 | Pods consume IPs aggressively during scaling |
| Private (general compute) | /20 | 4,091 | EC2 instances, moderate growth |
| Isolated (databases) | /24 | 251 | Few RDS instances, rarely scales |
The /19 for EKS is critical. With 8,187 IPs per subnet across 3 AZs, you have 24,000+ IPs for pods — enough for hundreds of nodes and thousands of pods.
IP consumption formula for EKS:
IPs needed = (max_nodes × max_pods_per_node) + (max_nodes × ENI_overhead)
Example: 50 nodes × 50 pods + 50 × 3 ENIs = 2,650 IPs needed
With /19 (8,187 per AZ): plenty of headroom
With /24 (251 per AZ): FAILURE at 8 nodes
Decision 5: NAT Gateway Strategy
NAT Gateways allow private subnet resources to reach the internet (pull Docker images, call external APIs, download packages) without being directly accessible from the internet.
Options:
| Strategy | Cost | Availability | Best For |
|---|---|---|---|
| 1 NAT GW in 1 AZ | ~$32/month + data | No AZ redundancy | Dev/staging |
| 1 NAT GW per AZ (3 total) | ~$96/month + data | Full AZ redundancy | Production |
| NAT Instance (t3.micro) | ~$8/month | Low throughput | Cost-sensitive dev |
Production rule: One NAT Gateway per AZ. If a single-AZ NAT GW fails, all private subnet resources in other AZs lose internet access. At $32/month per NAT GW, the $96/month total is cheap insurance.
Data processing charges: NAT Gateway charges $0.045/GB processed. At 1TB/month egress through NAT, that is $45 in processing fees alone. For high-bandwidth workloads (pulling large Docker images, data transfers), consider VPC endpoints to bypass NAT for AWS service traffic.
---
VPC Flow Logs — Enhanced Logging for Security and Troubleshooting
VPC Flow Logs capture information about IP traffic going to and from network interfaces in your VPC. They are essential for security auditing, troubleshooting connectivity issues, and compliance.
What Flow Logs Capture
Each flow log record contains:
- Source and destination IP addresses
- Source and destination ports
- Protocol (TCP/UDP/ICMP)
- Number of packets and bytes
- Start and end time
- Action (ACCEPT or REJECT)
- Log status
Enabling Flow Logs
# Create flow log for entire VPC (recommended)
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def456 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/flow-logs/production \
--deliver-logs-permission-arn arn:aws:iam::123456789:role/vpc-flow-logs-role
# Or send to S3 for cheaper long-term storage
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def456 \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::my-flow-logs-bucket/production/
Enhanced Flow Logs (v5 format)
Enhanced flow logs add fields that standard logs miss:
| Field | Purpose |
|---|---|
| vpc-id | Which VPC the traffic belongs to |
| subnet-id | Which subnet — critical for debugging routing |
| instance-id | Which EC2 instance |
| tcp-flags | SYN, ACK, FIN, RST — diagnose connection issues |
| pkt-src-addr | Original source before NAT translation |
| flow-direction | Ingress or egress — understand traffic patterns |
| traffic-path | Shows if traffic went through NAT GW, TGW, peering |
Enable enhanced format:
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def456 \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::flow-logs-bucket/ \
--log-format '${version} ${vpc-id} ${subnet-id} ${instance-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${flow-direction} ${traffic-path} ${tcp-flags}'
Flow Log Use Cases
Security auditing: Detect unexpected outbound connections (data exfiltration), port scanning attempts, rejected traffic patterns that indicate misconfigured security groups.
Troubleshooting: "Why can't service A talk to service B?" Flow logs show if traffic is being REJECTED and at which network interface — pinpoints security group or NACL issues.
Compliance: PCI-DSS and SOC2 require network traffic logging. Flow Logs satisfy this requirement.
Cost: CloudWatch Logs ingestion at $0.50/GB. High-traffic VPCs can generate 10-50 GB/day of flow logs. Use S3 destination ($0.023/GB storage) with Athena for querying to reduce costs.
---
Case Study 1: E-Commerce Platform (50 Microservices on EKS)
Business Context
- E-commerce company processing 200K orders/day
- 50 microservices running on EKS
- Traffic spikes 10x during flash sales (Diwali, Black Friday)
- Must auto-scale from 100 pods to 2,000 pods within 5 minutes
- RDS PostgreSQL + ElastiCache Redis + OpenSearch
The Problem They Faced
Original VPC used /24 subnets for everything. During their first major sale:
- Pods scaled from 100 to 400 → worked fine
- Scaled from 400 to 800 → subnet ran out of IPs in AZ-a
- New pods stuck in Pending state: "failed to allocate IP address"
- 30% of traffic dropped for 45 minutes until manual intervention
The Correct Design
VPC CIDR: 10.0.0.0/16 (65,536 IPs)
Subnet Layout:
| Subnet | CIDR | AZ | Usable IPs | Purpose |
|---|---|---|---|---|
| public-a | 10.0.0.0/24 | us-east-1a | 251 | ALB, NAT GW |
| public-b | 10.0.1.0/24 | us-east-1b | 251 | ALB, NAT GW |
| public-c | 10.0.2.0/24 | us-east-1c | 251 | ALB, NAT GW |
| private-eks-a | 10.0.32.0/19 | us-east-1a | 8,187 | EKS pods |
| private-eks-b | 10.0.64.0/19 | us-east-1b | 8,187 | EKS pods |
| private-eks-c | 10.0.96.0/19 | us-east-1c | 8,187 | EKS pods |
| data-a | 10.0.200.0/24 | us-east-1a | 251 | RDS, ElastiCache |
| data-b | 10.0.201.0/24 | us-east-1b | 251 | RDS, ElastiCache |
| data-c | 10.0.202.0/24 | us-east-1c | 251 | RDS, ElastiCache |
Total private subnet capacity for EKS: 24,561 IPs (3 x 8,187)
Why /19 for EKS subnets:
- Peak scaling: 100 nodes x 50 pods = 5,000 IPs
- With /19 per AZ: 8,187 IPs available per AZ
- Utilization at peak: 5,000 / 24,561 = 20% (plenty of headroom)
- Can grow 5x before any concern
Key design decisions:
- EKS pods use AWS VPC CNI (each pod gets a VPC IP) — requires large subnets
- NAT Gateway per AZ — ensures scaling in any AZ is not dependent on cross-AZ NAT
- Data subnets have no route to internet — RDS cannot be reached from outside
- VPC Flow Logs to S3 with 90-day retention for security auditing
Terraform Snippet
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "ecommerce-production"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.32.0/19", "10.0.64.0/19", "10.0.96.0/19"]
database_subnets = ["10.0.200.0/24", "10.0.201.0/24", "10.0.202.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # One per AZ for HA
one_nat_gateway_per_az = true
enable_flow_log = true
create_flow_log_cloudwatch_iam_role = true
create_flow_log_cloudwatch_log_group = true
flow_log_traffic_type = "ALL"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
---
Case Study 2: SaaS Platform on ECS Fargate (Multi-Tenant)
Business Context
- B2B SaaS platform with 500 enterprise customers
- 15 microservices running on ECS Fargate
- Each customer tenant has isolated resources
- Moderate traffic — 10K requests/minute steady, 30K during business hours
- Must comply with SOC2 (network logging required)
Design Considerations
- Fargate tasks consume 1 IP each (simpler than EKS — no pod networking multiplication)
- Peak: 15 services x 10 tasks each x 3 AZs = 450 IPs maximum
- Growth forecast: 3x in 2 years = 1,350 IPs
- /22 subnets (1,019 IPs each) are sufficient — no need for /19
VPC Design
VPC CIDR: 10.1.0.0/16
| Subnet | CIDR | AZ | Usable IPs | Purpose |
|---|---|---|---|---|
| public-a | 10.1.0.0/24 | ap-south-1a | 251 | ALB, NAT GW |
| public-b | 10.1.1.0/24 | ap-south-1b | 251 | ALB, NAT GW |
| public-c | 10.1.2.0/24 | ap-south-1c | 251 | ALB, NAT GW |
| private-app-a | 10.1.16.0/22 | ap-south-1a | 1,019 | ECS Fargate tasks |
| private-app-b | 10.1.20.0/22 | ap-south-1b | 1,019 | ECS Fargate tasks |
| private-app-c | 10.1.24.0/22 | ap-south-1c | 1,019 | ECS Fargate tasks |
| data-a | 10.1.200.0/24 | ap-south-1a | 251 | RDS Aurora, Redis |
| data-b | 10.1.201.0/24 | ap-south-1b | 251 | RDS Aurora, Redis |
| data-c | 10.1.202.0/24 | ap-south-1c | 251 | RDS Aurora, Redis |
Total private capacity: 3,057 IPs (3 x 1,019) — handles 3x growth comfortably.
Why /22 is Right Here (Not /19)
ECS Fargate = 1 IP per task. With 15 services averaging 10 tasks = 150 IPs steady state. Even at 3x peak = 450 IPs. A /22 (1,019 IPs per AZ) gives 6x headroom beyond peak. No need to waste /19 address space.
Security Architecture
Internet → ALB (public subnet, Security Group: allow 443)
↓
ECS Tasks (private subnet, SG: allow from ALB SG only)
↓
RDS Aurora (data subnet, SG: allow from ECS SG only, port 5432)
Network ACLs (stateless, subnet-level):
- Public subnets: Allow inbound 443 (HTTPS), allow outbound all
- Private subnets: Allow inbound from public subnet CIDR, allow outbound all
- Data subnets: Allow inbound from private subnet CIDR on DB ports ONLY, deny all other inbound
VPC Endpoints (bypass NAT, save cost, improve security):
- S3 Gateway Endpoint (free) — Fargate pulls images from ECR via S3
- ECR API + ECR DKR Interface Endpoints — container image pulls stay within VPC
- CloudWatch Logs Endpoint — log shipping without NAT
- Secrets Manager Endpoint — secret retrieval without internet
These endpoints eliminate NAT Gateway data processing charges for AWS service traffic (saving hundreds/month at scale) and improve security by keeping traffic within the AWS network.
SOC2 Compliance: Flow Logs Configuration
resource "aws_flow_log" "vpc_flow_log" {
vpc_id = module.vpc.vpc_id
traffic_type = "ALL"
log_destination_type = "s3"
log_destination = aws_s3_bucket.flow_logs.arn
max_aggregation_interval = 60 # 1-minute granularity
destination_options {
file_format = "parquet" # 70% smaller than text, faster Athena queries
per_hour_partition = true # Partition by hour for efficient querying
}
tags = { Name = "saas-production-flow-logs" }
}
# Retention policy: 1 year for SOC2 compliance
resource "aws_s3_bucket_lifecycle_configuration" "flow_logs_lifecycle" {
bucket = aws_s3_bucket.flow_logs.id
rule {
id = "retain-1-year"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
---
Case Study 3: Startup MVP Growing to Scale (Plan for the Future)
Business Context
- Early-stage startup with 3 engineers
- Currently 3 services on EC2 (MVP phase)
- Planning migration to EKS within 12 months
- Serves Indian market (ap-south-1)
- Budget-conscious but does not want to re-architect later
The Challenge
Design a VPC today that works for EC2 now and EKS later without re-building.
The Design: Future-Proof from Day One
VPC CIDR: 10.2.0.0/16 (full /16 even though current needs are tiny)
| Subnet | CIDR | AZ | Usable IPs | Current Use | Future Use |
|---|---|---|---|---|---|
| public-a | 10.2.0.0/24 | ap-south-1a | 251 | ALB | ALB |
| public-b | 10.2.1.0/24 | ap-south-1b | 251 | (unused) | ALB |
| public-c | 10.2.2.0/24 | ap-south-1c | 251 | (unused) | ALB |
| private-a | 10.2.32.0/19 | ap-south-1a | 8,187 | 3 EC2 instances | EKS pods |
| private-b | 10.2.64.0/19 | ap-south-1b | 8,187 | (unused) | EKS pods |
| private-c | 10.2.96.0/19 | ap-south-1c | 8,187 | (unused) | EKS pods |
| data-a | 10.2.200.0/24 | ap-south-1a | 251 | RDS (single-AZ) | RDS Multi-AZ |
| data-b | 10.2.201.0/24 | ap-south-1b | 251 | (unused) | RDS replica |
| data-c | 10.2.202.0/24 | ap-south-1c | 251 | (unused) | ElastiCache |
Why /19 subnets even though only 3 EC2 instances today:
- /19 costs nothing extra — subnet size does not affect billing
- When they migrate to EKS in 12 months, the subnets are ready
- No re-architecture, no new subnets, no application downtime for network changes
- The 3 EC2 instances use 3 IPs out of 8,187 — that is fine
Cost-Optimized NAT Strategy for Startups
Phase 1 (MVP — saving money):
- Single NAT Gateway in AZ-a only ($32/month)
- All private subnets route through the single NAT GW
- Risk: If AZ-a fails, private subnets in AZ-b/c lose internet
- Acceptable for a startup with low traffic and non-critical workloads
Phase 2 (Growing — when revenue justifies it):
- Add NAT Gateways in AZ-b and AZ-c ($96/month total)
- Update route tables so each AZ routes to its own NAT
- Zero downtime change — just add routes
Phase 3 (Production — EKS migration):
- Add VPC endpoints for ECR, S3, CloudWatch (reduces NAT data charges)
- Enable VPC Flow Logs (compliance requirement for enterprise customers)
- Implement Transit Gateway for multi-VPC connectivity (shared services VPC)
The Key Insight
Designing for /19 private subnets costs you nothing today but saves you from a painful re-architecture later. The only cost difference between a /24 and a /19 subnet is zero — you are just allocating address space from your /16 VPC. The mistake teams make is being conservative with subnet sizing because they think small subnets are "simpler." They are not — they are a ticking time bomb.
---
Common Mistakes (What Not To Do)
Mistake 1: Using /24 for EKS private subnets
Impact: Auto-scaling fails at ~8 nodes (50 pods each = 400 IPs > 251 available)
Fix: Use /19 minimum for any subnet running EKS pods
Mistake 2: Single NAT Gateway for production
Impact: AZ failure takes down all private subnet internet access
Fix: One NAT GW per AZ for production workloads
Mistake 3: No VPC Flow Logs
Impact: Cannot debug connectivity issues, fails security audits
Fix: Enable flow logs to S3 on day one (cost: $0.023/GB stored)
Mistake 4: Overlapping CIDRs across environments
Impact: Cannot peer VPCs, cannot use Transit Gateway, cannot connect to on-premises
Fix: Plan non-overlapping /16 blocks per environment from day one
Mistake 5: Putting databases in private subnets (not isolated)
Impact: Compromised application server can route to internet and exfiltrate data
Fix: Use isolated subnets with no internet route for databases
Mistake 6: Not planning for VPC endpoints
Impact: All AWS service traffic routes through NAT GW, costing $0.045/GB
Fix: Add S3, ECR, CloudWatch, Secrets Manager endpoints to avoid NAT for AWS traffic
---
Quick Reference: Subnet Sizing Decision Matrix
| Workload Type | Minimum Subnet Size | Recommended | Why |
|---|---|---|---|
| ALB only | /28 (11 IPs) | /24 | Room for future ALBs |
| EC2 instances (< 20) | /24 (251 IPs) | /24 | Adequate for small fleets |
| ECS Fargate (< 100 tasks) | /22 (1,019 IPs) | /22 | 1 IP per task, moderate scaling |
| ECS Fargate (100-500 tasks) | /20 (4,091 IPs) | /20 | High-density task scheduling |
| EKS pods (any scale) | /19 (8,187 IPs) | /19 | Pods consume IPs aggressively |
| RDS / ElastiCache | /24 (251 IPs) | /24 | Few instances, rarely scales |
| Lambda (VPC-attached) | /19 (8,187 IPs) | /19 | Each concurrent execution uses an ENI IP |
Important: VPC-attached Lambda functions consume IPs from your subnet for each concurrent execution. At 1,000 concurrent Lambda invocations = 1,000 IPs needed. If your Lambda is in a /24 subnet, it fails at 251 concurrent executions.
---
Summary
---
Frequently Asked Questions
How do I choose the right VPC CIDR block?
Use a /16 CIDR (65,536 IPs) for production VPCs to allow growth, ensuring it doesn't overlap with other VPCs, on-premises networks, or partner networks you might peer with. Common choices are 10.0.0.0/16 for production, 10.1.0.0/16 for staging. Plan your IP space across all environments upfront — CIDR conflicts prevent VPC peering and transit gateway attachments later.
What is the difference between public and private subnets?
Public subnets have a route to an Internet Gateway, giving resources public IP addresses and direct internet access. Private subnets route internet traffic through a NAT Gateway — resources have only private IPs and can reach the internet but aren't directly reachable from it. Place databases, application servers, and internal services in private subnets; only load balancers and bastion hosts need public subnets.
How many subnets do I need per VPC?
At minimum, create one public and one private subnet per availability zone — typically 6 subnets for a 3-AZ deployment. For better isolation, add a separate data subnet tier for databases. This gives you 9 subnets (3 public + 3 private/app + 3 private/data). Each subnet should be at least /24 (254 IPs) to accommodate scaling.
How do I calculate subnet sizes using CIDR notation?
A /24 gives 256 IPs (251 usable after AWS reserves 5), /25 gives 128, /26 gives 64, /27 gives 32. Divide your VPC CIDR evenly: a /16 VPC can hold 256 /24 subnets. For quick mental math, each bit in the subnet mask doubles/halves the range. Use /24 for most application subnets and /28 (16 IPs) for small subnets like NAT Gateway placements.
What is a NAT Gateway and when do I need one?
A NAT Gateway allows resources in private subnets to initiate outbound internet connections (for updates, API calls) while remaining unreachable from the internet. You need one in each AZ where private subnets have resources needing internet access. NAT Gateways cost $0.045/hour plus $0.045/GB processed — for cost savings in development, use a single NAT Gateway or NAT instance instead.
---
Related Resources
- Cloud Region Selection Guide — Choose the right region before designing your VPC
- AWS vs Azure vs GCP Compute Services — Match compute services to your VPC design
- Kubernetes Resource Limits — Manage pod resources within your subnet capacity
- Architecture Center — Production reference architectures using these VPC patterns
- Subnet Calculator Kit — Calculate CIDR ranges interactively