Save NAT gateway costs by using an EC2 - Terraform code included
If you have an AWS environment, chances are you need internet access for your private resources. While assigning Elastic IPs to each resource is an option, it can quickly become costly. AWS charges $3.60 per month for each public IP, and these costs can add up significantly if you have multiple resources.
A common approach is to use an AWS NAT Gateway, which is easy to set up and allows all private resources in your VPC to access the internet via a single instance. However, a NAT Gateway has a fixed cost of $33 per month, plus additional data transfer fees. If your traffic reaches 1 TB per month, your total NAT Gateway cost could rise to $79.
Another important factor is that a single NAT Gateway can become a Single Point of Failure (SPOF). Since NAT Gateways are deployed in a specific subnet, if your VPC spans multiple Availability Zones (AZs), routing all traffic through a single NAT Gateway introduces a potential failure risk. The solution is to deploy multiple NAT Gateways, but this significantly increases costs.
Using an EC2 Instance as a Cost-Effective NAT Solution
Instead of using an AWS NAT Gateway, you can configure an EC2 instance as a NAT device. In this setup, the EC2 instance forwards internet-bound traffic from your private resources using its own public IP address.
A small EC2 instance (e.g., t2.micro) is more than capable of handling this task and costs as little as $3.50 per month. While outbound traffic costs still apply, this approach allows you to significantly reduce AWS service fees.
However, keep in mind that:
- A single EC2 instance in one AZ can still be a single point of failure.
- Deploying one small EC2 instance per AZ is still more cost-effective than using multiple NAT Gateways.
This method is ideal for small companies, test environments, or individuals looking to reduce AWS costs while maintaining secure internet access for private resources.
Step-by-Step Guide: Configuring an EC2 Instance as a NAT Gateway
For this task, I recommend using a Linux operating system. Whether you choose Amazon Linux or Ubuntu, the process is similar. Personally, I prefer Ubuntu, but the steps are applicable to other Linux distributions as well.
Step-by-Step Configuration:
- Deploy a Linux Server in a Public Subnet:
Launch a small EC2 instance (e.g.,t2.micro) in a public subnet within your VPC. - Create a New Route Table for Private Subnets:
Create a route table for your private subnets and associate it with them. - Add a Route for
0.0.0.0/0:
Add a route in the private route table that points0.0.0.0/0(all internet traffic) to the network interface of the Linux server. - Disable Source/Destination Check:
Disable the source/destination check on the Linux server to allow it to forward traffic. - Configure the Linux Server:
Run the following commands on the Linux server to enable IP forwarding and configure iptables for NAT:
# Enable IP forwarding
echo 1 | tee /proc/sys/net/ipv4/ip_forward
sed -i '/net.ipv4.ip_forward=1/s/^#//g' /etc/sysctl.conf
sed -i '/net.ipv4.conf.all.accept_redirects=0/s/^#//g' /etc/sysctl.conf
sed -i '/net.ipv4.conf.all.send_redirects=0/s/^#//g' /etc/sysctl.conf
sysctl -p
# Configure iptables for NAT
mkdir -p /etc/iptables
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables-save > /etc/iptables/rules.v4These commands enable IP forwarding, configure the Linux server to act as a NAT device, and save the iptables rules to persist across reboots.
Conclusion
You successfully configured an EC2 instance as a NAT gateway! 🚀
Using an EC2 instance instead of an AWS NAT Gateway can significantly reduce your AWS costs, especially in multi-AZ environments. While an EC2 NAT instance introduces some management overhead, it is a great cost-saving alternative for:
- Startups & Small Businesses
- Test Environments
- Cost-Conscious AWS Users
If you want high availability, consider deploying multiple EC2 NAT instances—one per Availability Zone—to eliminate single points of failure while still keeping costs low.
One more thing... Automate Your NAT Instance Setup with Terraform
If you want to quickly deploy and test this NAT setup in AWS, you can use Terraform to create a new environment. This Terraform script will:
- Provision a VPC with 4 subnets (2 public, 2 private)
- Deploy a NAT EC2 instance in the public subnet
- Configure route tables to forward traffic from private subnets through the EC2 NAT instance
- Use a cloud-init script to automatically configure NAT on the instance
Step 1: Create a cloud-init.yml file
The cloud-init.yml file contains all the necessary commands to configure your NAT instance automatically.
#cloud-config
runcmd:
- echo 1 | tee /proc/sys/net/ipv4/ip_forward
- sed -i '/net.ipv4.ip_forward=1/s/^#//g' /etc/sysctl.conf
- sed -i '/net.ipv4.conf.all.accept_redirects=0/s/^#//g' /etc/sysctl.conf
- sed -i '/net.ipv4.conf.all.send_redirects=0/s/^#//g' /etc/sysctl.conf
- sysctl -p
- mkdir /etc/iptables
- iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
- iptables-save > /etc/iptables/rules.v4
Step 2: Create a main.tf file
The main.tf file defines your AWS infrastructure, including:
✅ VPC
✅ Public & Private Subnets
✅ EC2 NAT Instance
✅ Route Tables & Associations
provider "aws" {
region = "us-east-1" # Change to your preferred region
}
# Create VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "main-vpc"
}
}
# Create Internet Gateway
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
# Create Public Subnets
resource "aws_subnet" "public_subnet_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a" # Change to your preferred AZ
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-1"
}
}
resource "aws_subnet" "public_subnet_2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b" # Change to your preferred AZ
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-2"
}
}
# Create Private Subnets
resource "aws_subnet" "private_subnet_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
availability_zone = "us-east-1a" # Change to your preferred AZ
tags = {
Name = "private-subnet-1"
}
}
resource "aws_subnet" "private_subnet_2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.4.0/24"
availability_zone = "us-east-1b" # Change to your preferred AZ
tags = {
Name = "private-subnet-2"
}
}
# Create Route Table for Public Subnets
resource "aws_route_table" "public_route_table" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "public-route-table"
}
}
# Associate Public Subnets with Public Route Table
resource "aws_route_table_association" "public_subnet_1_association" {
subnet_id = aws_subnet.public_subnet_1.id
route_table_id = aws_route_table.public_route_table.id
}
resource "aws_route_table_association" "public_subnet_2_association" {
subnet_id = aws_subnet.public_subnet_2.id
route_table_id = aws_route_table.public_route_table.id
}
# Create EC2 Instance in Public Subnet
resource "aws_instance" "nat_instance" {
ami = "ami-0e1bed4f06a3b463d" # Ubuntu Server 22.04 LTS
instance_type = "t2.micro"
subnet_id = aws_subnet.public_subnet_1.id
associate_public_ip_address = true
source_dest_check = false # Disable source/destination check
key_name = "test" # Make sure you use an existing key pair
user_data = file("cloud-init.yml")
tags = {
Name = "nat-instance"
}
}
# Create Route Table for Private Subnets
resource "aws_route_table" "private_route_table" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
network_interface_id = aws_instance.nat_instance.primary_network_interface_id
}
tags = {
Name = "private-route-table"
}
}
# Associate Private Subnets with Private Route Table
resource "aws_route_table_association" "private_subnet_1_association" {
subnet_id = aws_subnet.private_subnet_1.id
route_table_id = aws_route_table.private_route_table.id
}
resource "aws_route_table_association" "private_subnet_2_association" {
subnet_id = aws_subnet.private_subnet_2.id
route_table_id = aws_route_table.private_route_table.id
}
# Security Group for NAT Instance
resource "aws_security_group" "nat_sg" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["10.0.0.0/16"] # Allow all traffic from VPC
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # Allow all outbound traffic
}
tags = {
Name = "nat-sg"
}
}
# Attach Security Group to NAT Instance
resource "aws_network_interface_sg_attachment" "nat_sg_attachment" {
security_group_id = aws_security_group.nat_sg.id
network_interface_id = aws_instance.nat_instance.primary_network_interface_id
}Step 3: Deploy with Terraform
Once you've created both files, run the following Terraform commands:
terraform init
terraform apply