GitHub repo: ShobanChiddarth/nat-instance-aws-terraform
Introduction
AWS NAT Gateway is a solution for letting EC2 instances in private subnets access the internet. Here is how it usually works. Private subnets would have a route table associated that has a default route 0.0.0.0/0 pointed to a NAT gateway that is present in a public subnet (one that has a default route 0.0.0.0/0 pointed to the internet gateway of the VPC).
This way private instances can download and install software updates, pull public Docker images and so on without needing a public IP address. And also the internet would not have a way to access the private EC2 instances. This is similar to setting up Port Address Translation (NAT, masquerading) in on premises networks so that PCs can use the internet without needing a public IP address. This setup is extremely common in home networks.
In this project I am basically replacing the NAT Gateway with another EC2 instance to save costs.
Architecture
For this demo project we have
- 1 VPC
- 2 subnets, one public and one private
- 1 bastion instance in the public subnet
- 1 NAT instance in the public subnet
- 2 Private EC2 instances in the private subnet
- Route tables, security groups, IGW and all other miscellaneous
The intended traffic flow is, the private EC2 instances will use the NAT instance as their router and the private subnet will have a default route pointing to the NAT instance's ENI. And the NAT instance will do masquerading and communicate with the internet.
The bastion will be used to SSH into the private EC2 instances in order to check for internet connectivity.
NAT Instance config
You can create a regular EC2 instance but make sure to have these configured if you want it to be a NAT instance.
1. source_dest_check = false
AWS, by default, blocks traffic that enters or leaves an EC2 instance if the traffic that enters or leaves the instance does not originate from it or is not destined for it. This to prevent EC2 instances from acting like routers. So you must disable this option on the NAT instance.
2. Security Group config
The NAT instance security group must allow the traffic that will be forwarded from private subnets. For simplicity in this demo, all traffic from the VPC CIDR is allowed. And allow all egress traffic (default), and optional bastion config for SSH access. Here is how my NAT instance's SSH group works.
resource "aws_security_group" "nat_instance_sg" {
name = "nat_instance_sg"
vpc_id = aws_vpc.NatInstanceDemoVPC.id
description = "allow everything"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [ aws_security_group.bastion_sg.id ]
}
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [ aws_vpc.NatInstanceDemoVPC.cidr_block ]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [ "0.0.0.0/0" ]
}
}
3. Route table (of private subnets)
The route table must have a default route that directs all queries to the NAT instance's primary ENI.
resource "aws_route_table" "to_nat_instance" {
vpc_id = aws_vpc.NatInstanceDemoVPC.id
route {
cidr_block = "0.0.0.0/0"
network_interface_id = aws_instance.nat_instance.primary_network_interface_id
}
depends_on = [ aws_eip.nat_instance_eip ]
}
4. user_data
This is the most important part of that NAT instance config. The following is the script intended to be put in user_data of the NAT instance.
#!/bin/bash
apt-get update
apt-get upgrade -y
set -eux
export DEBIAN_FRONTEND=noninteractive
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | debconf-set-selections
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | debconf-set-selections
apt-get install -y iptables-persistent
sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-nat.conf
PRIMARY_IFACE=$(ip route | awk '/default/ {print $5; exit}')
iptables -t nat -A POSTROUTING -o "$PRIMARY_IFACE" -j MASQUERADE
iptables -A FORWARD -i "$PRIMARY_IFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -o "$PRIMARY_IFACE" -j ACCEPT
netfilter-persistent save
I got the script from the internet and modified it a little. The script enables IP forwarding and configures iptables masquerading. I would say understanding each command is recommended before using it in production, but for a lab environment the script can be used as-is. However, on modern Linux distributions, nftables may be preferred over legacy iptables configurations.
5. Make sure the NAT instance has an Elastic IP
Assigning an Elastic IP ensures the NAT instance keeps the same public-facing address across stop/start cycles. The NAT functionality itself does not depend on a fixed public IP, but using an EIP avoids unexpected outbound IP changes and makes firewall allowlists, logging, and operational management easier.
Demo
This video is a live demo of test infrastructure created for NAT instance test working and the private EC2 instances are able to access the internet through the public IP of the NAT instance.
Instructions to run it are in the README of the repo linked above.
Cost Calculation
I used calculator.aws for these numbers.
NAT Gateway
- $0.045/hour for just existing
- $0.045/GB for data processing
So in a month, it would be 0.045*24*30 = $32/month + 0.045 for every GB of data transfer. For illustration assume 300GB data transfer per month. Which means `300*0.045 = $13.5/month.
So in total it would be way more than 32+13.5= $45.5 so definitely more than $50 per month.
NAT Instance
- Shared on-demand Linux
t3.microinap-south-1=> $8.18/Month - 8GB gp3 with minimum values for input output (as it won't be modified that much) => $0.73/month => round it up to $1/month as updates and patching will take some IO
- EIP for a month = $3.65 USD/month
- 300 GB of network egress per month would be covered in total network egress bill anyway, there is no per EC2 billing. When using NAT Gateway the data transfer will be billed twice, once when the NAT gateway processes it and once when it leaves AWS.
So in total it will be 8.18+1+3.65 = $12.83/month which means around $15 per month.
Comparison
| NAT Gateway | NAT Instance |
|---|---|
| ~$50/month | ~$15/month |
| ~$600/year | ~$180/month |
You are saving $420/year ($35/month), not counting the enormous data processing of high stakes environments and counting it as t3.micro which is what is available for free tier in ap-south-1 so in non-free tier accounts you could use even cheaper EC2 instance types (and different pricing options) given that you manage the NAT instances, patch them regularly, provision according to load and etc.
When NAT Gateway is better
NAT Gateway is a managed service that provides built-in availability within an Availability Zone, automatic scaling, and no patching requirements. A NAT instance is cheaper but introduces operational responsibility and can become a single point of failure if not designed for redundancy.
Conclusion
I have now learnt a cost efficient way to allow EC2 instances in private subnets to access the internet and I will be using this in the upcoming projects (the one I am currently building is telegram proxy server infrastructure for an entire country).
Earlier I ran into the cost problem of NAT Gateways for small college projects when I wanted to let private EC2 instances temporarily access the internet so what I did earlier was I deployed the infra on terraform and immediately deleted the NAT gateway to avoid costs.
Now I won't have to worry about that and I can just deploy another EC2 instance for cheaper rates that acts as a NAT gateway.

Top comments (0)