A
Arun's Blog
← Back to all posts

Centralized VPC Endpoints with AWS Transit Gateway: A Cost Analysis

AWSNetworking
TL;DR

Centralizing VPC endpoints in a hub VPC with Transit Gateway sounds appealing, but it's only cost-effective at scale. For 2-3 VPCs with fewer than 6 endpoint types, deploying endpoints in each VPC is ~3x cheaper. Centralization makes sense when you have 10+ VPCs or 10+ endpoint types. Don't forget: you must manually create Route 53 Private Hosted Zones for cross-VPC DNS resolution.

Introduction

When building multi-VPC architectures in AWS, you'll eventually face a decision: should you deploy VPC endpoints in every VPC, or centralize them in a shared services VPC? This post explores the architecture, implementation, cost trade-offs, and critical DNS considerations.

The Architecture

The centralized VPC endpoint pattern uses AWS Transit Gateway to share VPC endpoints across multiple VPCs:

Centralized VPC Endpoints Architecture showing spoke VPCs connected through Transit Gateway to a hub VPC containing shared endpoints
Centralized VPC endpoint architecture with Transit Gateway

Components

  1. Spoke VPCs (A & B): Workload VPCs where your EC2 instances run
  2. Hub VPC: Centralized VPC hosting the shared VPC endpoints
  3. Transit Gateway: Connects all VPCs and routes traffic between them
  4. VPC Endpoints: Interface endpoints for AWS services (SSM, SSM Messages, EC2 Messages)
  5. Route 53 Private Hosted Zones: Enable DNS resolution across all VPCs

The Terraform Implementation

VPCs and Subnets

Each VPC requires DNS support enabled:

resource "aws_vpc" "centralized_vpc_endpoint" {
  cidr_block           = "10.2.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "transit-gw-centralized-vpc-endpoint"
  }
}

Transit Gateway and Attachments

The Transit Gateway connects all VPCs:

resource "aws_ec2_transit_gateway" "vpc_endpoint" {
  description = "Transit Gateway for VPC A and VPC B to access VPC endpoints"
  tags = {
    Name = "transit-gw-vpc-endpoint"
  }
}

resource "aws_ec2_transit_gateway_vpc_attachment" "vpc_a_attachment" {
  transit_gateway_id = aws_ec2_transit_gateway.vpc_endpoint.id
  vpc_id             = aws_vpc.vpc_a.id
  subnet_ids         = [aws_subnet.subnet_a.id]
}

Routing

Spoke VPCs route traffic destined for the hub VPC through the Transit Gateway:

resource "aws_route_table" "rt_a" {
  vpc_id = aws_vpc.vpc_a.id

  route {
    cidr_block         = "10.2.0.0/16"  # Hub VPC CIDR
    transit_gateway_id = aws_ec2_transit_gateway.vpc_endpoint.id
  }
}

The hub VPC routes return traffic back to spoke VPCs:

resource "aws_route_table" "rt_centralized" {
  vpc_id = aws_vpc.centralized_vpc_endpoint.id

  route {
    cidr_block         = "10.0.0.0/16"  # VPC A
    transit_gateway_id = aws_ec2_transit_gateway.vpc_endpoint.id
  }

  route {
    cidr_block         = "10.1.0.0/16"  # VPC B
    transit_gateway_id = aws_ec2_transit_gateway.vpc_endpoint.id
  }
}

VPC Endpoints

Interface endpoints are created in the hub VPC:

resource "aws_vpc_endpoint" "ssm" {
  vpc_id              = aws_vpc.centralized_vpc_endpoint.id
  service_name        = "com.amazonaws.${var.region}.ssm"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.subnet_centralized.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = false  # Important: set to false when using manual PHZs

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

Security Groups

The endpoint security group must allow HTTPS (port 443) from spoke VPCs:

resource "aws_security_group" "vpc_endpoint_sg" {
  name        = "vpc-endpoint-sg"
  description = "Allow HTTPS from VPC A and B"
  vpc_id      = aws_vpc.centralized_vpc_endpoint.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16", "10.1.0.0/16"]
  }

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

The DNS Problem (Critical!)

Here's where many implementations fail. When you create a VPC endpoint with private_dns_enabled = true, AWS automatically:

  1. Creates a Private Hosted Zone for the service domain
  2. Associates it only with the VPC where the endpoint lives
  3. Creates DNS records pointing to the endpoint's private IPs
The Problem

Spoke VPCs don't have access to the auto-created PHZ. When an EC2 instance in VPC A tries to reach ssm.us-east-1.amazonaws.com, it resolves to the public AWS IP, not your private endpoint.

The Solution: Manual Private Hosted Zones

You must create Private Hosted Zones manually and associate them with all VPCs:

resource "aws_route53_zone" "ssm" {
  name = "ssm.${var.region}.amazonaws.com"

  vpc {
    vpc_id = aws_vpc.centralized_vpc_endpoint.id
  }
  vpc {
    vpc_id = aws_vpc.vpc_a.id
  }
  vpc {
    vpc_id = aws_vpc.vpc_b.id
  }
}

resource "aws_route53_record" "ssm" {
  zone_id = aws_route53_zone.ssm.zone_id
  name    = "ssm.${var.region}.amazonaws.com"
  type    = "A"

  alias {
    name                   = aws_vpc_endpoint.ssm.dns_entry[0].dns_name
    zone_id                = aws_vpc_endpoint.ssm.dns_entry[0].hosted_zone_id
    evaluate_target_health = false
  }
}
Important

Set private_dns_enabled = false on your endpoints when using manual PHZs to avoid conflicts.

Setting PHZ Created By Associated VPCs Visible in Console
private_dns_enabled = true AWS (automatic) Only endpoint's VPC No
Manual PHZ You (Terraform) Any VPCs you specify Yes

Cost Analysis

Let's break down the real costs of this architecture.

AWS Pricing (us-east-1)

Component Per Hour Per Month (730 hrs)
Interface VPC Endpoint $0.01/endpoint/AZ $7.30
Transit Gateway Attachment $0.05/attachment $36.50
TGW Data Processing $0.02/GB variable
Endpoint Data Processing $0.01/GB variable
Route 53 Private Hosted Zone - $0.50/zone
Route 53 DNS Queries (private) - FREE
Note on Route 53

When using private_dns_enabled = true, AWS manages the PHZ automatically at no extra cost. When using manual PHZs (required for centralized architecture), you pay $0.50/month per zone.

Scenario: 2 Spoke VPCs, 3 Endpoints (SSM)

Centralized Approach

Item Calculation Monthly Cost
3 VPC Endpoints (hub only) 3 x $7.30 $21.90
3 TGW Attachments (A, B, hub) 3 x $36.50 $109.50
3 Route 53 PHZs (manual) 3 x $0.50 $1.50
Fixed Total $132.90
+ TGW data processing $0.02/GB variable

Distributed Approach (endpoints in each spoke VPC)

Item Calculation Monthly Cost
6 VPC Endpoints (3 per VPC x 2) 6 x $7.30 $43.80
No Transit Gateway $0 $0
Route 53 PHZs (auto-managed) included $0
Fixed Total $43.80

The Verdict

For 2 VPCs with 3 endpoints, the distributed approach is ~3x cheaper!

The centralized pattern saves money only when you have many VPCs or many endpoint types.

Break-Even Analysis

The break-even formula depends on both the number of VPCs (N) and endpoint types (E):

Break-even VPCs = (7.30E + 36.50) / (7.30E - 36.50)

This only works when E > 5 (otherwise centralized is never cheaper on cost alone).

Break-Even Table

Endpoint Types VPCs Needed for Centralized to Win
3 Never (distributed always cheaper)
5 Never (break-even at infinity)
6 11 VPCs
8 5 VPCs
10 3 VPCs
15 2 VPCs
20+ 2 VPCs
Break-even analysis chart showing when centralized VPC endpoints become cost-effective based on number of VPCs and endpoint types
Break-even analysis: Centralized vs Distributed VPC endpoints

Data Processing Costs: A Real-World Example

Let's calculate data processing costs for a realistic scenario: SSM access to EC2 instances in each VPC, 5 times per day.

Estimating SSM Session Data

Session Activity Data
Connection handshake ~50KB
Commands typed ~1-2KB each
Output returned ~1-5MB (varies)
Typical session total ~5MB

Monthly Data Cost Comparison

Approach Rate Monthly Data Data Cost
Centralized $0.05/GB 1.5GB $0.075
Distributed $0.01/GB 1.5GB $0.015

Complete Monthly Cost (Fixed + Data)

Component Centralized Distributed
VPC Endpoints $21.90 $43.80
TGW Attachments $109.50 $0
Route 53 PHZs $1.50 $0
Data Processing $0.08 $0.02
Total $132.98 $43.82
Key Takeaway

For typical SSM usage (5 sessions/day per VPC), data costs are negligible (~$0.08 vs $0.02 per month). Fixed costs dominate the bill entirely. You'd need ~18,000 GB/month of data transfer before data costs become significant.

Common VPC Endpoints

If you're considering centralization, here are endpoints you might add:

Gateway Endpoints (FREE)

  • S3: com.amazonaws.{region}.s3
  • DynamoDB: com.amazonaws.{region}.dynamodb

Interface Endpoints ($0.01/hr/AZ)

Compute & Containers: EC2, ECS, ECS Agent, ECS Telemetry, ECR API, ECR Docker, EKS, Lambda

Security & Identity: Secrets Manager, KMS, STS, IAM

Monitoring & Logging: CloudWatch Logs, CloudWatch Monitoring, X-Ray

Messaging: SNS, SQS, EventBridge, Step Functions

Common Combinations

Use Case Endpoints Needed
SSM Access ssm, ssmmessages, ec2messages
ECS/Fargate ecs, ecs-agent, ecs-telemetry, ecr.api, ecr.dkr, s3, logs
Lambda in VPC lambda, sts, logs
Secrets + Encryption secretsmanager, kms, sts

When to Centralize

Centralize When:

  • You have 10+ VPCs
  • You need 10+ different endpoint types
  • You want centralized security controls and logging
  • You have a hub-and-spoke architecture already using Transit Gateway
  • Operational simplicity outweighs cost

Distribute When:

  • You have few VPCs (< 5)
  • You need few endpoint types (< 6)
  • Cost is the primary concern
  • You want simpler DNS (auto private DNS works per-VPC)
  • You want to avoid Transit Gateway complexity

Conclusion

The centralized VPC endpoint pattern is a powerful architecture for large-scale AWS deployments, but it's not always the most cost-effective choice. Before implementing:

  1. Count your VPCs and endpoints - Use the break-even formula
  2. Don't forget DNS - Manual Private Hosted Zones are required
  3. Consider operational benefits - Centralized management may justify higher costs
  4. Start distributed, centralize later - It's easier to consolidate than to distribute

The math is clear: for small deployments (2-3 VPCs, < 6 endpoint types), putting endpoints in each VPC is significantly cheaper. Centralization makes financial sense only at scale.