AWS VPC Endpoints: Cost Comparison of Gateway, Interface, NAT, and Transit Gateway
There are four ways to make AWS services accessible from private subnets, and the costs vary dramatically. Gateway Endpoints are free but limited to S3 and DynamoDB. Interface Endpoints cost around $8.76/month/AZ. NAT Gateways run around $37.96/month/AZ. Transit Gateway attachments run around $43.80/month/AZ plus endpoint costs. I had to dig to find all the costs in one place, so here they are with the Terraform configuration for the VPC endpoints I use in my EKS clusters. Prices depend on the region.
The following VPC endpoints are the minimum required for managed nodes to join an EKS cluster when running in private subnets: ec2, ecr.dkr, ecr.api, sts, s3.
Cost comparison
https://pcg.io/insights/vpc-endpoints-explanation-and-cost-comparison/
There are four options to make AWS services accessible from private subnets:
| Option | Cost | Notes |
|---|---|---|
| Gateway Endpoint | Free | S3 and DynamoDB only. Needs an internet gateway, route table configuration, and security group rules/NACLs to allow traffic to the prefix list |
| Interface Endpoint | ~$8.76/month/AZ + ~$0.01/GB | Most AWS services. Provides a private IP in your subnet. Needs a security group rule allowing traffic to it |
| NAT Gateway | ~$37.96/month/AZ + ~$0.052/GB | Any service with a public API. For less critical workloads, a NAT instance via fck-nat.dev avoids NAT data processing fees. You only pay EC2 instance hours and EBS storage |
| Transit Gateway | ~$43.80/month/AZ + ~$0.02/GB | Interface Endpoint fees still apply. Likely only cost-effective across multiple connected VPCs |
Terraform
S3 and DynamoDB use Gateway Endpoints (free, no security group required, traffic routes via route tables). All other services use Interface Endpoints.
module "endpoints" {
source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
version = "6.6.1"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
security_group_ids = [
aws_security_group.vpc_endpoints_default.id,
]
endpoints = {
"sts" = {
service = "sts"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-sts-vpc-endpoint" }
},
dynamodb = {
service = "dynamodb"
service_type = "Gateway"
route_table_ids = flatten([module.vpc.private_route_table_ids])
tags = { Name = "${module.vpc.name}-dynamodb-vpc-endpoint" }
},
s3 = {
service = "s3"
service_type = "Gateway"
route_table_ids = flatten([module.vpc.private_route_table_ids])
tags = { Name = "${module.vpc.name}-s3-vpc-endpoint" }
},
"kinesis-streams" = {
service = "kinesis-streams"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-kinesis-streams-vpc-endpoint" }
},
"kinesisanalytics" = {
service = "kinesisanalytics"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-kinesisanalytics-vpc-endpoint" }
},
"logs" = {
service = "logs"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-logs-vpc-endpoint" }
},
"monitoring" = {
service = "monitoring"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-monitoring-vpc-endpoint" }
},
"sns" = {
service = "sns"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-sns-vpc-endpoint" }
},
"sqs" = {
service = "sqs"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-sqs-vpc-endpoint" }
},
"secretsmanager" = {
service = "secretsmanager"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-secretsmanager-vpc-endpoint" }
},
"execute-api" = {
service = "execute-api"
private_dns_enabled = true
tags = { Name = "${module.vpc.name}-execute-api-vpc-endpoint" }
},
}
tags = local.tags
}
# Security group for interface endpoints
# Gateway endpoints (S3, DynamoDB) do not use security groups
resource "aws_security_group" "vpc_endpoints_default" {
name = "${module.vpc.name}-vpc-endpoints-default"
description = "Default Security group for VPC endpoints"
vpc_id = module.vpc.vpc_id
tags = { Name = "${module.vpc.name}-vpc-endpoints-default" }
}
# Allow HTTPS from within the security group (i.e. resources that share this SG)
resource "aws_security_group_rule" "vpc_endpoints_default_self_ingress" {
description = "Allow all traffic from self VPC endpoints group"
security_group_id = aws_security_group.vpc_endpoints_default.id
type = "ingress"
protocol = "tcp"
from_port = 443
to_port = 443
self = true
}
resource "aws_security_group_rule" "vpc_endpoints_default_self_egress" {
description = "Allow secure traffic to self VPC endpoints group"
security_group_id = aws_security_group.vpc_endpoints_default.id
type = "egress"
protocol = "tcp"
from_port = 443
to_port = 443
self = true
}
# Gateway endpoints are reached via prefix lists, not security groups
resource "aws_security_group_rule" "vpc_endpoints_default_dynamodb_egress" {
description = "Allow secure traffic to DynamoDB via prefix list"
security_group_id = aws_security_group.vpc_endpoints_default.id
type = "egress"
protocol = "tcp"
from_port = 443
to_port = 443
prefix_list_ids = [module.endpoints.endpoints.dynamodb.prefix_list_id]
}
resource "aws_security_group_rule" "vpc_endpoints_default_s3_egress" {
description = "Allow secure traffic to S3 via prefix list"
security_group_id = aws_security_group.vpc_endpoints_default.id
type = "egress"
protocol = "tcp"
from_port = 443
to_port = 443
prefix_list_ids = [module.endpoints.endpoints.s3.prefix_list_id]
}