Save NAT gateway costs by using an EC2 - Terraform code included

Save NAT gateway costs by using an EC2 - Terraform code included
Photo by Arnold Francisca / Unsplash

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:

  1. Deploy a Linux Server in a Public Subnet:
    Launch a small EC2 instance (e.g., t2.micro) in a public subnet within your VPC.
  2. Create a New Route Table for Private Subnets:
    Create a route table for your private subnets and associate it with them.
  3. Add a Route for 0.0.0.0/0:
    Add a route in the private route table that points 0.0.0.0/0 (all internet traffic) to the network interface of the Linux server.
  4. Disable Source/Destination Check:
    Disable the source/destination check on the Linux server to allow it to forward traffic.
  5. 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.v4

These 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

Update cookies preferences