AWS VPC
How an AWS VPC is put together. CIDR and subnet sizing, what makes a subnet public, NAT gateway costs, VPC endpoints, and security groups versus NACLs.
Amazon VPC
A VPC is your own isolated network inside AWS. Almost everything else runs inside one, which means VPC mistakes surface later as connectivity problems that look like application bugs, and as a NAT gateway line on the bill that nobody can explain.
VPC Network Architecture
Private subnets use a NAT Gateway (in the public subnet) for outbound internet access
CIDR and subnet sizing
A VPC gets a private CIDR block, conventionally something like 10.0.0.0/16: 65,536 addresses.
Size generously. The primary CIDR cannot be changed after creation. You can attach secondary CIDR blocks later, but that is a workaround rather than a fix, and overlapping ranges make future VPC peering or a Transit Gateway connection impossible. A /16 costs nothing extra.
Then split into subnets, each bound to one Availability Zone:
10.0.0.0/16 VPC
├── 10.0.0.0/20 public-a (eu-west-1a)
├── 10.0.16.0/20 public-b (eu-west-1b)
├── 10.0.32.0/20 private-a (eu-west-1a)
├── 10.0.48.0/20 private-b (eu-west-1b)
└── 10.0.64.0/20 database-a (eu-west-1a)
Two sizing details that bite:
AWS reserves five addresses in every subnet: network address, VPC router, DNS, a reserved address, and broadcast. A /28 gives you 11 usable addresses, not 16.
On EKS, every pod consumes a VPC IP. A /24 private subnet sounds ample until 200 pods exhaust it and new pods fail to start. Use /20 or larger for anything running Kubernetes.
What actually makes a subnet public
Nothing about the subnet itself. A subnet is public if its route table sends 0.0.0.0/0 to an Internet Gateway. That is the whole definition.
| Route table | Makes the subnet |
|---|---|
0.0.0.0/0 → Internet Gateway | Public |
0.0.0.0/0 → NAT Gateway | Private with outbound internet |
No 0.0.0.0/0 route | Fully isolated |
A resource in a public subnet also needs a public IP to be reachable. The route makes the subnet public, the IP makes the instance reachable.
aws ec2 describe-route-tables \
--filters Name=vpc-id,Values=vpc-0abc123 \
--query 'RouteTables[].{ID:RouteTableId,Routes:Routes[].{Dest:DestinationCidrBlock,Target:GatewayId}}'
A subnet with no explicit route table association uses the VPC's main route table, which is a frequent cause of a "private" subnet that unexpectedly has internet access, or a public one that does not.
NAT gateways are where the money goes
A NAT gateway lets private subnets reach the internet outbound while remaining unreachable inbound. It is also one of the most expensive small components in AWS: an hourly charge per gateway plus a per-GB data processing charge, on top of normal data transfer.
Run one per Availability Zone for high availability and you are paying three hourly charges before a single byte moves. Teams routinely find NAT is their largest networking cost.
Three ways to reduce it, in order of payoff:
1. VPC endpoints for AWS services. Traffic to S3 and DynamoDB through a Gateway endpoint is free and bypasses NAT entirely. If your workload pulls container images from ECR, writes logs to CloudWatch, or reads from S3, that traffic is otherwise flowing through NAT and being charged per gigabyte.
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123 \
--service-name com.amazonaws.eu-west-1.s3 \
--route-table-ids rtb-0abc123
Interface endpoints (PrivateLink) cover most other AWS services. They carry their own hourly and per-GB charge, so they pay off when they displace meaningful NAT volume. ECR and CloudWatch Logs usually qualify.
2. One NAT gateway instead of three in non-production. You lose AZ redundancy, which is an acceptable trade for a dev environment and not for production.
3. Check what is actually using it with flow logs before assuming.
Security groups and NACLs
Two independent layers, and the differences matter.
| Security group | Network ACL | |
|---|---|---|
| Attaches to | Instance / ENI | Subnet |
| State | Stateful (return traffic auto-allowed) | Stateless (return traffic needs its own rule) |
| Rules | Allow only | Allow and deny |
| Evaluation | All rules together | In numbered order, first match wins |
| Can reference | Other security groups | CIDR only |
Use security groups as your primary control. They are stateful, which removes a whole class of error, and they can reference each other, so "the database accepts traffic from the application tier" is expressed directly rather than by hardcoding IP ranges:
aws ec2 authorize-security-group-ingress \
--group-id sg-database \
--protocol tcp --port 5432 \
--source-group sg-application
That rule keeps working as instances come and go, which a CIDR-based rule does not.
NACLs are stateless, and that is the classic trap: allowing inbound 443 without also allowing outbound on the ephemeral port range (1024 to 65535) means requests arrive and responses cannot leave. Symptoms look like a hung connection rather than a rejection. Use NACLs only for coarse subnet-level denies, blocking a known-bad CIDR, and leave the default allow-all otherwise.
Connecting VPCs
| Option | For | Note |
|---|---|---|
| VPC Peering | Two VPCs | Not transitive; every pair needs its own connection |
| Transit Gateway | Many VPCs | A hub; scales properly, has its own hourly and per-GB cost |
| PrivateLink | Exposing one service privately | Endpoint-based, no CIDR overlap concerns |
| Site-to-Site VPN / Direct Connect | On-premises | VPN over internet, Direct Connect dedicated |
Peering is not transitive, if A peers with B and B with C, A cannot reach C. Beyond three or four VPCs, a Transit Gateway is the right answer.
All of these require non-overlapping CIDR blocks, which is why the sizing decision at the very beginning matters so much.
Flow logs
Not enabled by default, and the only way to answer "what is actually talking to what":
aws ec2 create-flow-logs \
--resource-type VPC --resource-ids vpc-0abc123 \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::acme-flow-logs
Send them to S3 and query with Athena rather than paying CloudWatch Logs ingestion rates for high-volume data. They are how you find which workload is driving NAT charges, and they record ACCEPT and REJECT, so a REJECT entry tells you a security group or NACL blocked something, which beats guessing.
Debugging connectivity, in order
When something cannot reach something else, check in this sequence:
- Security group on the destination. Does it allow the source on that port?
- Security group on the source. Outbound allows it? (Default is allow-all, but people restrict it.)
- Route table. Is there a route to the destination at all?
- NACL. Both directions, remembering statelessness.
- DNS,
enableDnsSupportandenableDnsHostnameson the VPC.
VPC Reachability Analyzer does this automatically and names the blocking component:
aws ec2 create-network-insights-path \
--source i-0source --destination i-0dest \
--protocol tcp --destination-port 5432
It is much faster than manual inspection and worth reaching for first.
Go deeper in this section
| Sub-topic | What you will learn |
|---|---|
| VPC Subnets | Public and private subnet design across AZs |
| Route Tables | Routing, associations and the main table |
| NAT Gateway | Outbound internet for private subnets, and its cost |
Quick reference
| Task | Command |
|---|---|
| List VPCs | aws ec2 describe-vpcs |
| Subnets and free IPs | aws ec2 describe-subnets --query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,Free:AvailableIpAddressCount}' |
| Route tables | aws ec2 describe-route-tables --filters Name=vpc-id,Values=<vpc> |
| Security group rules | aws ec2 describe-security-groups --group-ids <sg> |
| Allow from another SG | aws ec2 authorize-security-group-ingress --group-id <sg> --protocol tcp --port <p> --source-group <sg> |
| Create a gateway endpoint | aws ec2 create-vpc-endpoint --vpc-id <vpc> --service-name com.amazonaws.<region>.s3 |
| Enable flow logs | aws ec2 create-flow-logs --resource-type VPC --resource-ids <vpc> --traffic-type ALL |
| Diagnose a path | aws ec2 create-network-insights-path --source <a> --destination <b> --protocol tcp |
Key takeaways
- The VPC CIDR cannot be changed. Size generously and avoid ranges that would overlap future peers.
- AWS reserves 5 IPs per subnet, and on EKS every pod consumes one. Use
/20or larger. - A subnet is public only because its route table points
0.0.0.0/0at an Internet Gateway. - NAT gateways are often the largest networking cost. Gateway endpoints for S3 and DynamoDB are free and remove that traffic.
- Security groups are stateful, NACLs are not. A NACL allowing inbound without outbound ephemeral ports silently breaks responses.
- Reference security groups by ID rather than CIDR so rules survive instance churn.
- Peering is not transitive. Past a few VPCs, use a Transit Gateway.
- Reachability Analyzer names the blocking component faster than manual checking.
Check your understanding
Requests arrive, responses never leave
A NACL allows inbound TCP 443 to a subnet. Clients connect but every request hangs and eventually times out. The security group allows all outbound. What is wrong?
An unexplained NAT bill
A team sees large NAT gateway data processing charges. Their workload runs in private subnets and pulls container images and writes logs to AWS services. What is the most effective fix?
Frequently Asked Questions
What makes a subnet public or private in AWS?
Only its route table. A subnet is public when its route table sends 0.0.0.0/0 to an Internet Gateway, and private when it does not, typically routing that traffic to a NAT gateway for outbound access instead. Nothing about the subnet object itself marks it either way. A resource in a public subnet also needs a public IP to be reachable from outside, so "public subnet" and "publicly reachable instance" are two separate conditions.
How do I reduce NAT gateway costs?
Add VPC endpoints for the AWS services your workload uses. Gateway endpoints for S3 and DynamoDB are free and remove that traffic from NAT entirely, which often covers a surprising share of the volume since ECR image layers live in S3. Interface endpoints for services such as ECR APIs and CloudWatch Logs carry their own hourly and per-GB charges but typically cost less than the NAT processing they replace. In non-production, running a single NAT gateway rather than one per Availability Zone cuts the hourly charge at the cost of redundancy. Enable flow logs first to see what is actually flowing.
What is the difference between a security group and a network ACL?
A security group attaches to an instance or network interface, is stateful so return traffic is automatically permitted, contains only allow rules, and can reference other security groups as a source. A network ACL attaches to a subnet, is stateless so each direction needs its own rule, supports both allow and deny, and is evaluated in numbered order. Use security groups as the primary control and NACLs only for coarse subnet-level blocking. The stateless behaviour of NACLs is the most common source of confusing half-working connectivity.
What CIDR block should I choose for my VPC?
Something large and unlikely to overlap anything you may later connect to, 10.0.0.0/16 is the common default, giving 65,536 addresses at no extra cost. The primary CIDR cannot be changed after creation, and overlapping ranges make VPC peering, Transit Gateway attachments and VPN connections to on-premises networks impossible. If you run multiple environments or accounts, allocate distinct ranges to each from the outset. Remember AWS reserves five addresses in every subnet, and that EKS consumes one VPC IP per pod.
Why can't my EC2 instance reach the internet?
Work through it in order. Is there a 0.0.0.0/0 route in the subnet's route table, pointing at an Internet Gateway for a public subnet or a NAT gateway for a private one? Is the subnet associated with the route table you think it is, rather than falling back to the VPC's main table? Does the security group allow outbound traffic? For a public subnet, does the instance actually have a public IP? And if a NACL has been customised, does it allow outbound on the port and inbound on the ephemeral range? VPC Reachability Analyzer checks all of this and names the blocking component.
When should I use VPC peering versus a Transit Gateway?
Peering is simple and cost-effective for a small number of VPCs, but it is not transitive. Each pair needs its own connection, so the number of connections grows quadratically and becomes unmanageable past three or four VPCs. A Transit Gateway acts as a hub, so each VPC attaches once and routing is managed centrally. It has its own hourly and per-GB charges, so peering is cheaper at small scale. Both require non-overlapping CIDR blocks.