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 ~$8.76/month/AZ. NAT Gateways run ~$37.96/month/AZ. And Transit Gateway attachments are ~$43.80/month/AZ plus endpoint costs. I had to do some digging to find all the costs in one place, so here they are with the Terraform configuration for the VPC endpoints setup I use in my EKS clusters. The prices do 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:
- A Gateway Endpoint is free of charge but is only available for S3 and DynamoDB. Requires an internet gateway, route table configuration, and security group rules/NACLs to allow traffic to the prefix list.
- An Interface Endpoint costs approximately $8.76 per month per AZ plus about $0.01 per GB and is available for most AWS services. This provides a private IP in your subnet and requires security group rules to allow traffic to it.
- A NAT Gateway can be used to access AWS services or any other services with a public API. The cost is $37.96 per month per AZ plus $0.052 per GB. However, for less critical workloads, you can use a NAT instances using fck-nat.dev which is more cost-effective as with EC2 you don’t have to pay for NAT data processing, only the EC2 instance hours and EBS storage.
- A Transit Gateway attachment costs approximately $43.80 per month per AZ plus about $0.02 per GB. Additionally, if you use an Interface Endpoint, the cost is approximately $8.76 per month per AZ plus about $0.01 per GB. So likely needs to calculated around multiple connected VPCs to be cost-effective.
Terraform
S3 and DynamoDB use Gateway Endpoints (free, no security group required; traffic is routed 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]
}