AWS PrivateLink Deep Dive: Private Connectivity Patterns
PrivateLink enables private connectivity to AWS services and your own services without traversing the public internet. Traffic stays on the AWS backbone.
This guide covers VPC Endpoints, Endpoint Services, cross-account patterns, and Terraform automation.
TL;DR
- Interface Endpoints = ENIs for AWS services (S3, EC2, etc.)
- Gateway Endpoints = route table entries (S3, DynamoDB only)
- Endpoint Services = expose your services via PrivateLink
- Cross-account sharing for multi-account architectures
- Full Terraform examples included
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Consumer VPC │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ ││
│ │ ┌──────────┐ ┌──────────────────┐ ││
│ │ │ App │───────▶│ VPC Endpoint │ ││
│ │ │ │ │ (Interface) │ ││
│ │ └──────────┘ └────────┬─────────┘ ││
│ │ │ ││
│ └────────────────────────────────┼─────────────────────────────┘│
└───────────────────────────────────┼─────────────────────────────┘
│ Private (AWS backbone)
┌───────────────────────────────────┼─────────────────────────────┐
│ Provider VPC │
│ ┌────────────────────────────────┼─────────────────────────────┐│
│ │ ▼ ││
│ │ ┌──────────────────┐ ││
│ │ │ Endpoint Service │ ││
│ │ │ (NLB) │ ││
│ │ └────────┬─────────┘ ││
│ │ │ ││
│ │ ┌────────▼─────────┐ ││
│ │ │ Your Service │ ││
│ │ └──────────────────┘ ││
│ └──────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
VPC Endpoints for AWS Services
Interface Endpoints
# Interface endpoint for ECR
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 = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true # Use AWS service DNS names
tags = {
Name = "ecr-api-endpoint"
}
}
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 = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "ecr-dkr-endpoint"
}
}
# Security group for 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 = [aws_vpc.main.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Gateway Endpoints (S3 and DynamoDB)
# Gateway endpoint for S3
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[*].id
tags = {
Name = "s3-endpoint"
}
}
# Gateway endpoint for DynamoDB
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[*].id
tags = {
Name = "dynamodb-endpoint"
}
}
Common Endpoints Module
# modules/vpc-endpoints/main.tf
variable "vpc_id" {}
variable "subnet_ids" {}
variable "security_group_id" {}
variable "route_table_ids" {}
variable "region" {}
locals {
interface_endpoints = [
"ecr.api",
"ecr.dkr",
"logs",
"monitoring",
"ssm",
"ssmmessages",
"ec2messages",
"secretsmanager",
"kms",
"sts",
"elasticloadbalancing",
"autoscaling",
"eks",
]
gateway_endpoints = [
"s3",
"dynamodb",
]
}
resource "aws_vpc_endpoint" "interface" {
for_each = toset(local.interface_endpoints)
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.${each.value}"
vpc_endpoint_type = "Interface"
subnet_ids = var.subnet_ids
security_group_ids = [var.security_group_id]
private_dns_enabled = true
tags = {
Name = "${replace(each.value, ".", "-")}-endpoint"
}
}
resource "aws_vpc_endpoint" "gateway" {
for_each = toset(local.gateway_endpoints)
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.${each.value}"
vpc_endpoint_type = "Gateway"
route_table_ids = var.route_table_ids
tags = {
Name = "${each.value}-endpoint"
}
}
Create Your Own Endpoint Service
Expose your service via PrivateLink:
# Network Load Balancer (required for endpoint service)
resource "aws_lb" "api" {
name = "api-nlb"
internal = true
load_balancer_type = "network"
subnets = aws_subnet.private[*].id
enable_cross_zone_load_balancing = true
}
resource "aws_lb_target_group" "api" {
name = "api-tg"
port = 8080
protocol = "TCP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
interval = 30
port = "traffic-port"
protocol = "TCP"
healthy_threshold = 3
unhealthy_threshold = 3
}
}
resource "aws_lb_listener" "api" {
load_balancer_arn = aws_lb.api.arn
port = 443
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
# VPC Endpoint Service
resource "aws_vpc_endpoint_service" "api" {
acceptance_required = true # Manual approval for consumers
network_load_balancer_arns = [aws_lb.api.arn]
allowed_principals = [
"arn:aws:iam::111111111111:root", # Account 1
"arn:aws:iam::222222222222:root", # Account 2
]
tags = {
Name = "api-endpoint-service"
}
}
output "endpoint_service_name" {
value = aws_vpc_endpoint_service.api.service_name
}
Consumer Side
# In consumer account
resource "aws_vpc_endpoint" "provider_api" {
vpc_id = aws_vpc.consumer.id
service_name = "com.amazonaws.vpce.eu-west-2.vpce-svc-xxxxxxxxx"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.endpoint.id]
private_dns_enabled = false # Can't use with cross-account
tags = {
Name = "provider-api-endpoint"
}
}
# Create Route53 private hosted zone for nice DNS
resource "aws_route53_zone" "provider_api" {
name = "api.provider.internal"
vpc {
vpc_id = aws_vpc.consumer.id
}
}
resource "aws_route53_record" "provider_api" {
zone_id = aws_route53_zone.provider_api.zone_id
name = "api.provider.internal"
type = "A"
alias {
name = aws_vpc_endpoint.provider_api.dns_entry[0].dns_name
zone_id = aws_vpc_endpoint.provider_api.dns_entry[0].hosted_zone_id
evaluate_target_health = true
}
}
Cross-Account Patterns
Hub-and-Spoke with PrivateLink
┌─────────────────────┐
│ Shared Services │
│ Account │
│ ┌───────────────┐ │
│ │ Endpoint Svc │ │
│ │ (API, Auth) │ │
│ └───────────────┘ │
└─────────┬───────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Workload A │ │ Workload B │ │ Workload C │
│ Account │ │ Account │ │ Account │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ VPC Endpoint│ │ │ │ VPC Endpoint│ │ │ │ VPC Endpoint│ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Terraform for Multi-Account
# Provider account - endpoint service
resource "aws_vpc_endpoint_service" "shared" {
acceptance_required = false # Auto-accept from allowed accounts
network_load_balancer_arns = [aws_lb.shared.arn]
allowed_principals = var.consumer_account_arns
}
# Store service name in SSM for consumers
resource "aws_ssm_parameter" "endpoint_service_name" {
name = "/shared/endpoint-service-name"
type = "String"
value = aws_vpc_endpoint_service.shared.service_name
}
# Consumer account - using data source
data "aws_ssm_parameter" "endpoint_service_name" {
provider = aws.shared
name = "/shared/endpoint-service-name"
}
resource "aws_vpc_endpoint" "shared_service" {
vpc_id = aws_vpc.main.id
service_name = data.aws_ssm_parameter.endpoint_service_name.value
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.endpoint.id]
}
SaaS Integration Patterns
Connect to third-party SaaS via PrivateLink:
# Example: Datadog PrivateLink
resource "aws_vpc_endpoint" "datadog" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.datadog.id]
private_dns_enabled = false
}
# Private hosted zone for Datadog
resource "aws_route53_zone" "datadog" {
name = "datadoghq.com"
vpc {
vpc_id = aws_vpc.main.id
}
}
resource "aws_route53_record" "datadog" {
for_each = {
"agent-http-intake.logs" = aws_vpc_endpoint.datadog.dns_entry[0]
"api" = aws_vpc_endpoint.datadog.dns_entry[0]
}
zone_id = aws_route53_zone.datadog.zone_id
name = "${each.key}.datadoghq.com"
type = "A"
alias {
name = each.value.dns_name
zone_id = each.value.hosted_zone_id
evaluate_target_health = true
}
}
Endpoint Policies
Restrict what can be done through an endpoint:
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[*].id
# Restrict to specific buckets
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = "*"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = [
"arn:aws:s3:::company-data-bucket",
"arn:aws:s3:::company-data-bucket/*",
"arn:aws:s3:::company-logs-bucket",
"arn:aws:s3:::company-logs-bucket/*",
]
},
{
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = "*"
Condition = {
StringNotEquals = {
"aws:PrincipalAccount" = var.account_id
}
}
}
]
})
}
Cost Optimization
ENDPOINT TYPE COST
============= ====
Gateway (S3/DDB) Free
Interface $0.01/hr per AZ + $0.01/GB processed
Tips:
- Use Gateway endpoints for S3/DynamoDB (free)
- Consolidate Interface endpoints across fewer AZs for dev
- Use endpoint policies to prevent data exfiltration
Troubleshooting
Endpoint not resolving:
# Check private DNS is enabled
aws ec2 describe-vpc-endpoints --vpc-endpoint-ids vpce-xxx
# Check DNS resolution
dig +short ec2.eu-west-2.amazonaws.com
# Should return private IP if working
Connection timeout:
# Check security group allows inbound 443
aws ec2 describe-security-groups --group-ids sg-xxx
# Check route tables include endpoint
aws ec2 describe-route-tables --route-table-ids rtb-xxx
References
- PrivateLink Docs: https://docs.aws.amazon.com/vpc/latest/privatelink/
- Endpoint Services: https://docs.aws.amazon.com/vpc/latest/privatelink/endpoint-service.html
- Pricing: https://aws.amazon.com/privatelink/pricing/