VPC Gateways: Internet Gateway, NAT Gateway, and Route Table Mechanics
An Internet Gateway enables bidirectional internet access for public subnets. A NAT Gateway gives private subnet resources outbound internet access without exposing them to inbound connections. Both require route table entries to function. Understanding the routing rules prevents common 'no internet' debugging sessions.
Internet Gateway vs NAT Gateway
| | Internet Gateway (IGW) | NAT Gateway | |---|---|---| | Direction | Bidirectional (inbound + outbound) | Outbound only | | Attached to | VPC (one per VPC) | Subnet (deployed in public subnet) | | Requires public IP on instance | Yes (Elastic IP or auto-assigned) | No — NAT handles translation | | Cost | Free | $0.045/hour + $0.045/GB processed | | Use case | Public subnets, internet-facing resources | Private subnets needing outbound internet |
A route table entry, not the gateway itself, determines whether traffic reaches the internet
GotchaAWS VPCAttaching an IGW to a VPC does nothing by itself. A subnet becomes public only when its route table has a default route (0.0.0.0/0) pointing to the IGW. Similarly, a private subnet uses a NAT Gateway only when its route table points 0.0.0.0/0 to the NAT Gateway's ENI. Missing route table entries are the most common cause of 'my instance has no internet' problems.
Prerequisites
- VPC subnets
- Route tables
- CIDR notation
Key Points
- Internet Gateway: attached to VPC, but each subnet needs a route table entry 0.0.0.0/0 → igw-id.
- Public subnet = subnet with a route table that routes 0.0.0.0/0 to an IGW.
- Private subnet = subnet whose route table does NOT route to an IGW directly.
- NAT Gateway must be deployed in a public subnet — it needs internet access to route traffic on behalf of private instances.
Internet Gateway setup
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true # instances get auto-assigned public IPs
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id # this makes it a public subnet
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Without map_public_ip_on_launch = true, instances in this subnet still need an Elastic IP to be reachable from the internet. Auto-assigned public IPs change on instance stop/start — use Elastic IPs for stable public addresses.
NAT Gateway: outbound internet for private subnets
# Elastic IP for the NAT Gateway
resource "aws_eip" "nat" {
domain = "vpc"
}
# NAT Gateway deployed in the PUBLIC subnet
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id # must be in public subnet
depends_on = [aws_internet_gateway.main] # IGW must exist first
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
# no map_public_ip_on_launch — instances have no public IP
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id # outbound via NAT
}
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
Private instances make outbound connections (package downloads, API calls, ECR pulls). NAT Gateway replaces the source IP with its Elastic IP and tracks connection state to route responses back. Inbound connections initiated from the internet can't reach private instances — there's no way to address them directly.
Multi-AZ NAT Gateway pattern
A single NAT Gateway is a single point of failure and its cost is per-AZ data transfer. Production architectures deploy one NAT Gateway per AZ:
variable "azs" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_eip" "nat" {
count = length(var.azs)
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
count = length(var.azs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
resource "aws_route_table" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
Each private subnet's route table points to the NAT Gateway in the same AZ. If a NAT Gateway fails (rare), only the private subnets in that AZ lose internet access — other AZs are unaffected.
📝Egress-only Internet Gateway: IPv6 outbound without inbound
IPv6 addresses are globally routable — there's no concept of "private" IPv6 like RFC 1918 provides for IPv4. An Egress-Only Internet Gateway (EIGW) provides the IPv6 equivalent of a NAT Gateway: outbound IPv6 traffic is allowed, but the internet cannot initiate connections to IPv6 instances.
resource "aws_egress_only_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
# Add to private subnet route table for IPv6 outbound
resource "aws_route" "ipv6_egress" {
route_table_id = aws_route_table.private.id
destination_ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.main.id
}
Unlike NAT Gateway, EIGW is free. For dual-stack (IPv4 + IPv6) private subnets: NAT Gateway for IPv4 outbound, EIGW for IPv6 outbound.
An EC2 instance in a private subnet can't reach the internet. The VPC has an Internet Gateway attached and a NAT Gateway deployed in a public subnet with an Elastic IP. The instance's security group allows all outbound traffic. What is the most likely configuration error?
easyThe VPC, IGW, and NAT Gateway are all created correctly. The security group permits outbound traffic. The instance was recently launched in the private subnet.
AThe Internet Gateway needs to be associated with the private subnet
Incorrect.Internet Gateways are attached to the VPC, not to individual subnets. Associating an IGW with a private subnet would make it public — that's the wrong fix.BThe private subnet's route table does not have a route for 0.0.0.0/0 pointing to the NAT Gateway
Correct!Route tables control where traffic goes. Without a default route (0.0.0.0/0 → nat-gateway-id) in the private subnet's route table, outbound traffic has nowhere to go and is dropped. The NAT Gateway existing in the VPC does nothing unless the private subnet's route table explicitly points to it. Check the route table associated with the private subnet and add the missing route.CThe NAT Gateway's security group doesn't allow outbound traffic
Incorrect.NAT Gateways don't have security groups — they are managed AWS resources. Security groups apply to EC2 instances and ENIs, not to NAT Gateways.DThe instance needs a public IP address to use the NAT Gateway
Incorrect.NAT Gateway exists specifically to provide internet access to instances without public IPs. The instance should not have a public IP — that's the point of NAT.
Hint:The gateway exists. What tells the instance's traffic to use it?