NAT Gateway and Elastic IP for Private Subnet Egress
Scenario
Your private subnet has no internet access. EC2 instances and ECS tasks in private subnets need to make outbound calls — to download package updates, call AWS APIs, or reach external services — but must never be reachable from the internet directly.
A NAT Gateway in the public subnet allows one-way outbound internet access for private resources without exposing them to inbound connections.
- Allocate an aws_eip (Elastic IP) for the NAT Gateway.
- Create an aws_nat_gateway in the public subnet, using the EIP.
- Create a private route table with a default route (0.0.0.0/0) sending traffic to the NAT Gateway.
- Associate the private route table with the private subnet.
- Output the NAT Gateway ID and the EIP public IP.
Additional Context
NAT Gateways are placed in a public subnet (one with an IGW route), not in the private subnet. Traffic from private instances flows: Private Subnet → NAT Gateway (public subnet) → Internet Gateway → Internet. The NAT Gateway performs source address translation, replacing the private IP with the EIP.
NAT Gateways cost approximately $0.045/hour plus $0.045 per GB processed. For cost-sensitive scenarios, consider a NAT Instance (EC2 with IP forwarding) or VPC Endpoints (preferred for AWS service traffic, as they bypass the NAT entirely).
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "vpc_id" { type = string; description = "ID of the VPC containing the subnets." }
variable "public_subnet_id" { type = string; description = "Public subnet to place the NAT Gateway in." }
variable "private_subnet_id"{ type = string; description = "Private subnet that needs outbound internet." }
variable "environment" { type = string; default = "dev" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
# Elastic IP — a static public IP address for the NAT Gateway.
# The 'vpc' domain is required for VPC-associated EIPs.
resource "aws_eip" "nat" {
domain = "vpc"
tags = { Name = "nat-eip-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
}
# NAT Gateway — allows private resources to initiate outbound connections.
# Must be placed in a PUBLIC subnet (one with an IGW route).
# It uses the EIP as its external address.
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = var.public_subnet_id # public subnet — NOT the private subnet
tags = { Name = "main-nat-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
# NAT Gateway must be created after the IGW to ensure the subnet's internet route exists.
# In practice, Terraform usually handles this ordering, but depends_on is explicit.
}
# Private route table — routes all outbound traffic through the NAT Gateway.
resource "aws_route_table" "private" {
vpc_id = var.vpc_id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id # outbound to NAT, not IGW
}
tags = { Name = "private-rt-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
}
# Associate the private route table with the private subnet.
resource "aws_route_table_association" "private" {
subnet_id = var.private_subnet_id
route_table_id = aws_route_table.private.id
}outputs.tf
output "nat_gateway_id" { value = aws_nat_gateway.main.id }
output "nat_eip" { value = aws_eip.nat.public_ip }
output "private_route_table_id" { value = aws_route_table.private.id }Workflow Commands
terraform init
terraform apply \
-var="vpc_id=vpc-xxxx" \
-var="public_subnet_id=subnet-public" \
-var="private_subnet_id=subnet-private"
# Verify: launch an EC2 in the private subnet and test outbound:
# (from EC2 in private subnet)
curl -s https://ifconfig.me # should return the NAT EIP address
terraform destroy -var="vpc_id=..." -var="public_subnet_id=..." -var="private_subnet_id=..."Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: nat_eip = "54.201.123.45" nat_gateway_id = "nat-0abcdef1234567890" private_route_table_id = "rtb-0123456789abcdef0"

