Cheap AWS NAT gateway (replacement)
AWS NAT Gateways are expensive for what they do. As of 2/3/2024, monthly price for a NAT gateway is about $30/month in us-east-1. Per best practice, a VPC needs to have a private subnet and use NAT Gateway for outbound (ie Internet) connection. As soon as you allow your dev teams to stand up their VPCs on demand, your AWS bill will balloon up from those costs.
Outbound connection isn't needed all the time (for example, you need it to maintain SSM connection, pull updates, upload to S3, etc).
One cheaper option is to replace NAT gateways with NAT instances. These are regular EC2 instances configured to route traffic with few simple Linux commands. It's a trivial effort to setup an EC2 instance for NAT and point your 0.0.0.0/0 route to it.
The idea is to accomplish following:
- Use one of the smallest (cheapest) Graviton instances, such as t4g.nano. Even though you will be limited by its network bandwidth, it should work fine for non-production workloads
- Create an AMI from it and deploy it to all accounts/VPCs in your AWS organization (again, for non-prod workloads; there's a chance production workload doesn't even need an outbound connection).
Terraform example to create a NAT instance
Imagine there is a VPC with NAT gateway and you need to replace it with NAT instance. The Terraform code below will create an EC2 instance from Amazon Linux and set it up for NAT'ing.
data "aws_ami" "amazon_linux_arm64" {
owners = ["amazon"]
filter {
name = "architecture"
values = ["arm64"]
}
filter {
name = "name"
values = ["al2023-ami-2023*"]
}
most_recent = true
}
resource "aws_instance" "nat" {
ami = data.aws_ami.amazon_linux_arm64.image_id
instance_type = "t4g.nano"
source_dest_check = false
iam_instance_profile = aws_iam_instance_profile.test_profile.name
subnet_id = module.vpc.public_subnets[0]
vpc_security_group_ids = [aws_security_group.nat.id, aws_security_group.base.id]
tags = {
Name = "${module.vpc.name}-nat-instance"
}
user_data = <<EOL
#!/bin/bash
# Turning on IP Forwarding
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sysctl -p
# iptables
yum install -y iptables iptables-services
systemctl enable iptables
systemctl start iptables
# FW changes.
iptables -I INPUT -j ACCEPT
## Making a catchall rule for routing and masking the private IP
iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
iptables -F FORWARD
service iptables save
systemctl restart iptables
## Ref:
# https://docs.aws.amazon.com/vpc/latest/userguide/VPC_NAT_Instance.html#nat-routing-table
EOL
}
resource "aws_route" "nat" {
route_table_id = module.vpc.private_route_table_ids[0]
destination_cidr_block = "0.0.0.0/0"
network_interface_id = aws_instance.nat.primary_network_interface_id
}
This code assuming that you are setting NAT instance from get-go and intend to use it. Practically, it would make sense to create a separate EC2, create AMI from it and then launch NAT instaces from it without needing to configure it each time.
Other options
fck-nat
Checkout https://fck-nat.dev/v1.3.0/ if you need another way to deploy NAT instances. I liked the naming of it, thus sharing it here :).