DEV Community

Cover image for Your NAT Gateway is Bleeding Money: Save 80% with VPC Endpoints (Terraform Guide) πŸ’°
Suhas Mallesh
Suhas Mallesh

Posted on

Your NAT Gateway is Bleeding Money: Save 80% with VPC Endpoints (Terraform Guide) πŸ’°

Stop paying $0.09/GB for S3 and DynamoDB traffic through NAT Gateway. VPC Endpoints are FREE and cut data transfer costs by 80%. Here's the Terraform setup.

Here's a question that might surprise you: How much are you paying to access S3 from your private subnets?

If you're routing through a NAT Gateway (most teams are), you're paying:

  • $0.045/GB for NAT Gateway data processing
  • $0.09/GB for data transfer out to internet
  • Total: $0.135/GB 😱

For 1TB of S3 traffic per month, that's $135 just to access your own data.

VPC Endpoints are FREE and eliminate these costs entirely. Let me show you how.

πŸ’Έ The Hidden Cost of NAT Gateway Data Transfer

Most developers don't realize their architecture looks like this:

EC2 (private subnet) 
  β†’ NAT Gateway ($0.045/GB)
  β†’ Internet Gateway ($0.09/GB) 
  β†’ S3 (public endpoint)

Cost for 1TB: $135/month
Enter fullscreen mode Exit fullscreen mode

Even though S3 is in the same AWS region! You're paying internet rates to access your own storage.

🎯 The Solution: VPC Endpoints

VPC Endpoints create a direct private connection from your VPC to AWS services:

EC2 (private subnet) 
  β†’ VPC Endpoint (FREE!)
  β†’ S3

Cost for 1TB: $0/month πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

Savings: 100% of data transfer costs

πŸ“Š Cost Comparison

Without VPC Endpoints (Through NAT Gateway)

Monthly S3 traffic: 1TB

NAT Gateway processing: 1,000 GB Γ— $0.045 = $45
Data transfer out:      1,000 GB Γ— $0.09  = $90
Total monthly cost:                         $135

Annual cost: $1,620
Enter fullscreen mode Exit fullscreen mode

With VPC Endpoints

Monthly S3 traffic: 1TB

VPC Endpoint cost:                           $0
Data transfer (in-region):                   $0
Total monthly cost:                          $0

Annual savings: $1,620 πŸ’°
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ Terraform Implementation

Gateway Endpoints (FREE - S3 & DynamoDB)

S3 and DynamoDB endpoints are Gateway Endpoints - completely free, no hourly charges.

# vpc-endpoints.tf

# S3 Gateway Endpoint (FREE!)
resource "aws_vpc_endpoint" "s3" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${var.region}.s3"

  vpc_endpoint_type = "Gateway"

  route_table_ids = [
    aws_route_table.private_a.id,
    aws_route_table.private_b.id,
    aws_route_table.private_c.id,
  ]

  tags = {
    Name = "s3-vpc-endpoint"
  }
}

# DynamoDB Gateway Endpoint (FREE!)
resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${var.region}.dynamodb"

  vpc_endpoint_type = "Gateway"

  route_table_ids = [
    aws_route_table.private_a.id,
    aws_route_table.private_b.id,
    aws_route_table.private_c.id,
  ]

  tags = {
    Name = "dynamodb-vpc-endpoint"
  }
}

# Optional: Endpoint policy to restrict access
resource "aws_vpc_endpoint_policy" "s3" {
  vpc_endpoint_id = aws_vpc_endpoint.s3.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = "*"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::my-app-bucket/*",
          "arn:aws:s3:::my-app-bucket"
        ]
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

Deploy it:

terraform apply
# Cost: $0/month
# Instant savings on all S3 & DynamoDB traffic! πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

Interface Endpoints (Paid - Other AWS Services)

For other services like ECR, Secrets Manager, SSM, etc., use Interface Endpoints:

# interface-endpoints.tf

# Security group for interface endpoints
resource "aws_security_group" "vpc_endpoints" {
  name_prefix = "vpc-endpoints-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "vpc-endpoints-sg"
  }
}

# ECR API Endpoint (for Docker pulls)
resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true

  tags = {
    Name = "ecr-api-endpoint"
  }
}

# ECR DKR Endpoint (for Docker image layers)
resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true

  tags = {
    Name = "ecr-dkr-endpoint"
  }
}

# Secrets Manager Endpoint
resource "aws_vpc_endpoint" "secrets_manager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true

  tags = {
    Name = "secretsmanager-endpoint"
  }
}

# SSM Endpoint (for Session Manager, Parameter Store)
resource "aws_vpc_endpoint" "ssm" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.ssm"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true

  tags = {
    Name = "ssm-endpoint"
  }
}

# CloudWatch Logs Endpoint
resource "aws_vpc_endpoint" "logs" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.logs"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true

  tags = {
    Name = "logs-endpoint"
  }
}
Enter fullscreen mode Exit fullscreen mode

Interface endpoint costs:

  • $0.01/hour per endpoint = $7.30/month each
  • $0.01/GB data processed (vs $0.045 through NAT Gateway)

Still 78% cheaper than NAT Gateway for data transfer!

πŸ’° Complete Cost Analysis

Scenario: Microservices app with heavy AWS service usage

Monthly traffic:

  • S3: 500 GB
  • DynamoDB: 200 GB
  • ECR (Docker pulls): 300 GB
  • Secrets Manager: 50 GB
  • CloudWatch Logs: 100 GB

Total: 1,150 GB/month

Without VPC Endpoints (NAT Gateway routing)

NAT Gateway processing: 1,150 GB Γ— $0.045 = $51.75
Data transfer out:      1,150 GB Γ— $0.09  = $103.50
Total:                                      $155.25/month

Annual cost: $1,863
Enter fullscreen mode Exit fullscreen mode

With VPC Endpoints

S3 Gateway Endpoint:              $0 (500 GB free)
DynamoDB Gateway Endpoint:        $0 (200 GB free)

ECR Interface Endpoint:           $7.30/month + (300 GB Γ— $0.01) = $10.30
Secrets Manager Endpoint:         $7.30/month + (50 GB Γ— $0.01)  = $7.80
CloudWatch Logs Endpoint:         $7.30/month + (100 GB Γ— $0.01) = $8.30

Total:                            $33.70/month

Annual cost: $404.40
Savings: $1,458.60/year (78% reduction!) πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

πŸŽ“ Which Endpoints Should You Create?

Always Create (FREE):

βœ… S3 - If you use S3 at all (everyone does)

βœ… DynamoDB - If you use DynamoDB

Create if You Use Them (Paid but worthwhile):

βœ… ECR - If pulling Docker images from private subnets

βœ… Secrets Manager - If reading secrets from apps

βœ… SSM - If using Parameter Store or Session Manager

βœ… CloudWatch Logs - If sending logs from private instances

βœ… SQS - If processing queues

βœ… SNS - If publishing to topics

Complete Module for Common Endpoints

# modules/vpc-endpoints/main.tf

variable "vpc_id" {
  description = "VPC ID"
  type        = string
}

variable "region" {
  description = "AWS region"
  type        = string
}

variable "private_route_table_ids" {
  description = "List of private route table IDs"
  type        = list(string)
}

variable "private_subnet_ids" {
  description = "List of private subnet IDs for interface endpoints"
  type        = list(string)
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
}

# Gateway Endpoints (FREE)
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = var.private_route_table_ids

  tags = { Name = "s3-gateway-endpoint" }
}

resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${var.region}.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = var.private_route_table_ids

  tags = { Name = "dynamodb-gateway-endpoint" }
}

# Security group for interface endpoints
resource "aws_security_group" "endpoints" {
  name_prefix = "vpc-endpoints-"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  tags = { Name = "vpc-endpoints-sg" }
}

# Interface Endpoints (Paid)
locals {
  interface_endpoints = {
    ecr_api = "ecr.api"
    ecr_dkr = "ecr.dkr"
    logs    = "logs"
    ssm     = "ssm"
    secretsmanager = "secretsmanager"
  }
}

resource "aws_vpc_endpoint" "interface" {
  for_each = local.interface_endpoints

  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${var.region}.${each.value}"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true

  tags = { Name = "${each.key}-endpoint" }
}

output "endpoint_ids" {
  value = merge(
    { s3 = aws_vpc_endpoint.s3.id },
    { dynamodb = aws_vpc_endpoint.dynamodb.id },
    { for k, v in aws_vpc_endpoint.interface : k => v.id }
  )
}
Enter fullscreen mode Exit fullscreen mode

Usage

module "vpc_endpoints" {
  source = "./modules/vpc-endpoints"

  vpc_id     = aws_vpc.main.id
  region     = "us-east-1"
  vpc_cidr   = "10.0.0.0/16"

  private_route_table_ids = [
    aws_route_table.private_a.id,
    aws_route_table.private_b.id,
    aws_route_table.private_c.id,
  ]

  private_subnet_ids = [
    aws_subnet.private_a.id,
    aws_subnet.private_b.id,
    aws_subnet.private_c.id,
  ]
}
Enter fullscreen mode Exit fullscreen mode

πŸ” How to Verify It's Working

After deploying, test that traffic routes through endpoints:

# From an EC2 instance in private subnet

# Test S3 endpoint
aws s3 ls --region us-east-1 --debug 2>&1 | grep -i endpoint
# Should show: vpce-xxx.s3.us-east-1.vpce.amazonaws.com

# Test DynamoDB endpoint
aws dynamodb list-tables --region us-east-1 --debug 2>&1 | grep -i endpoint
# Should show: vpce-xxx.dynamodb.us-east-1.vpce.amazonaws.com

# Verify no NAT Gateway traffic
# Check NAT Gateway CloudWatch metrics - BytesOutToDestination should drop
Enter fullscreen mode Exit fullscreen mode

⚑ Quick Wins Checklist

5-Minute Setup (FREE endpoints):

  • βœ… Add S3 gateway endpoint
  • βœ… Add DynamoDB gateway endpoint (if used)
  • βœ… Deploy with terraform apply
  • βœ… Watch NAT Gateway traffic drop immediately

15-Minute Setup (Interface endpoints):

  • βœ… Identify which AWS services your apps use
  • βœ… Create security group for endpoints
  • βœ… Add relevant interface endpoints
  • βœ… Enable private DNS
  • βœ… Test connectivity

πŸ’‘ Pro Tips

1. Start with Gateway Endpoints (S3 & DynamoDB)

They're free and take 2 minutes to set up. Instant ROI.

2. Monitor NAT Gateway metrics

Before/after comparison proves the savings:

resource "aws_cloudwatch_dashboard" "nat_savings" {
  dashboard_name = "nat-gateway-savings"

  dashboard_body = jsonencode({
    widgets = [{
      type = "metric"
      properties = {
        metrics = [
          ["AWS/NATGateway", "BytesOutToDestination", { stat = "Sum" }]
        ]
        period = 86400
        region = var.region
        title  = "NAT Gateway Traffic (Should Decrease After VPC Endpoints)"
      }
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

3. Use endpoint policies for security

Lock down which resources can be accessed:

policy = jsonencode({
  Statement = [{
    Effect = "Allow"
    Principal = "*"
    Action = "s3:*"
    Resource = "arn:aws:s3:::my-allowed-bucket/*"
  }]
})
Enter fullscreen mode Exit fullscreen mode

4. Don't create endpoints you don't need

Each interface endpoint costs $7.30/month. Only create them for services you actively use.

⚠️ Common Gotchas

1. Route table association

Gateway endpoints MUST be associated with route tables. Don't forget:

route_table_ids = [all_your_private_route_tables]
Enter fullscreen mode Exit fullscreen mode

2. Security group for interface endpoints

Must allow inbound 443 from your VPC CIDR.

3. Private DNS

Set private_dns_enabled = true or your apps won't automatically use the endpoint.

4. Cross-region doesn't work

VPC Endpoints only work for same-region traffic. Cross-region still goes through internet.

πŸ“ˆ Real-World Example

Before VPC Endpoints:

  • NAT Gateway processing 2TB/month AWS service traffic
  • Cost: $90 (processing) + $180 (data transfer) = $270/month

After VPC Endpoints:

  • S3 (1TB): FREE via gateway endpoint
  • DynamoDB (500GB): FREE via gateway endpoint
  • ECR (300GB): $7.30 + $3 = $10.30
  • Other services (200GB): 3 endpoints Γ— $7.30 + $2 = $23.90
  • Total: $34.20/month

Annual savings: $2,829 πŸ’°

🎯 Summary

Service Without Endpoint With Endpoint Savings
S3 (1TB) $135/month $0 100%
DynamoDB (500GB) $67.50/month $0 100%
ECR (300GB) $40.50/month $10.30/month 75%
Other (per service) Varies ~$7-10/month 70-80%

Key takeaway: VPC Endpoints are the easiest AWS cost optimization you'll ever do. Gateway endpoints (S3, DynamoDB) are literally free money.

Stop routing your internal AWS traffic through NAT Gateway. Your wallet will thank you. πŸš€


Implemented VPC Endpoints? How much are you saving? Share in the comments! πŸ’¬

Follow for more AWS cost optimization with Terraform! ⚑

Top comments (0)