Intermediate
Introduction: The Hidden Cost Sitting in Your AWS Bill
If you’re running workloads in private subnets — and you should be — there’s a good chance a significant chunk of your monthly AWS bill comes from NAT Gateway data processing charges. At $0.045 per GB processed (in us-east-1), plus the $0.045/hour hourly charge (~$32.40/month just to keep it running), NAT Gateways are one of the most overlooked cost drivers in AWS environments.
Here’s the thing: a large portion of traffic flowing through your NAT Gateway is likely destined for AWS services — S3, DynamoDB, CloudWatch, ECR, STS, Secrets Manager, and others. This traffic never needs to leave the AWS network. VPC Endpoints let you create private connections from your VPC directly to supported AWS services, bypassing the NAT Gateway entirely.
This article is for intermediate AWS practitioners — you understand VPCs, subnets, route tables, and security groups, and you want to practically reduce costs and tighten your network security posture. We’ll cover the two types of VPC Endpoints, when to use each, exact CLI commands to set them up, routing and policy configuration, common mistakes, and cost analysis.
Prerequisites
- An AWS account with a VPC containing private subnets and at least one NAT Gateway
- AWS CLI v2 installed and configured with appropriate IAM permissions
- Basic understanding of VPC route tables, security groups, and IAM policies
- IAM permissions including
ec2:CreateVpcEndpoint,ec2:ModifyVpcEndpoint,ec2:DescribeVpcEndpoints, andec2:DeleteVpcEndpoints
Gateway Endpoints vs Interface Endpoints: Understanding the Two Types
AWS offers two fundamentally different types of VPC Endpoints. Choosing the wrong one — or not understanding how each works — leads to misconfigurations, unexpected costs, or both.
Gateway Endpoints
Gateway Endpoints are available for only two services: Amazon S3 and Amazon DynamoDB. They work by adding a route to your VPC route table that directs traffic destined for the service to the endpoint. There is no hourly charge and no data processing charge — they are completely free.
Key characteristics:
- Free to use — no hourly or per-GB charges
- Work at the route table level — a prefix list route is added to specified route tables
- Cannot be accessed from outside the VPC (no VPN, Direct Connect, or VPC peering access)
- Supported only for S3 and DynamoDB
- Cannot be associated with a security group
- Support endpoint policies for access control
Interface Endpoints (AWS PrivateLink)
Interface Endpoints use AWS PrivateLink and create Elastic Network Interfaces (ENIs) with private IP addresses in your specified subnets. They support a much broader set of AWS services — over 100 services including CloudWatch, ECR, STS, Secrets Manager, KMS, SNS, SQS, and many more. They also support S3 (as an alternative to the Gateway Endpoint).
Key characteristics:
- Charged per hour (~$0.01/hour per AZ in us-east-1, ~$7.20/month per AZ) plus $0.01 per GB processed
- Create ENIs in your subnets — can be secured with security groups
- Accessible from on-premises via VPN or Direct Connect
- Support Private DNS to override the default public DNS for the service
- Support endpoint policies
Quick Comparison Table
| Feature | Gateway Endpoint | Interface Endpoint |
|---|---|---|
| Supported Services | S3, DynamoDB only | 100+ AWS services |
| Cost | Free | ~$0.01/hr per AZ + $0.01/GB |
| Mechanism | Route table prefix list entry | ENI with private IP in subnet |
| Security Group Support | No | Yes |
| On-Premises Access | No | Yes |
| Endpoint Policy | Yes | Yes |
| Private DNS | N/A | Yes (optional) |
Rule of thumb: For S3 and DynamoDB, always start with Gateway Endpoints (free). For everything else, use Interface Endpoints. Only use an Interface Endpoint for S3 if you need on-premises access to S3 via PrivateLink.
Step 1: Identify Your NAT Gateway Traffic Targets
Before configuring endpoints, figure out what traffic is actually flowing through your NAT Gateway. VPC Flow Logs are your best tool here.
If you don’t already have VPC Flow Logs enabled, enable them:
# Create a CloudWatch Log Group for flow logs
aws logs create-log-group --log-group-name /vpc/flow-logs
# Enable VPC Flow Logs (you need an IAM role that allows vpc-flow-logs to publish to CloudWatch)
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def456789a \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/flow-logs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/VPCFlowLogsRole
Once you have flow logs, query them with CloudWatch Logs Insights to identify the top destination IPs from your private subnets:
# In CloudWatch Logs Insights, run this query:
# fields @timestamp, srcAddr, dstAddr, bytes
# | filter srcAddr like "10.0."
# | stats sum(bytes) as totalBytes by dstAddr
# | sort totalBytes desc
# | limit 20
Cross-reference the top destination IPs with AWS IP ranges (published at https://ip-ranges.amazonaws.com/ip-ranges.json) to identify which AWS services your instances are communicating with:
import json
import urllib.request
def identify_aws_service(ip_address):
"""Look up which AWS service owns a given IP address."""
url = "https://ip-ranges.amazonaws.com/ip-ranges.json"
response = urllib.request.urlopen(url)
data = json.loads(response.read())
import ipaddress
target = ipaddress.ip_address(ip_address)
matches = []
for prefix in data["prefixes"]:
network = ipaddress.ip_network(prefix["ip_prefix"])
if target in network:
matches.append({
"service": prefix["service"],
"region": prefix["region"],
"prefix": prefix["ip_prefix"]
})
return matches
# Example usage
results = identify_aws_service("52.217.109.76")
for r in results:
print(f"Service: {r['service']}, Region: {r['region']}, Prefix: {r['prefix']}")
Common high-traffic AWS services you’ll find: S3 (often the biggest by far — think logs, backups, artifacts), DynamoDB, ECR (container image pulls), CloudWatch (logs and metrics), STS (every SDK call that assumes a role), and Secrets Manager or KMS.
Step 2: Configure Gateway Endpoints for S3 and DynamoDB
This is the single highest-impact, lowest-effort optimization you can make. Gateway Endpoints for S3 and DynamoDB are free and take minutes to set up.
Create an S3 Gateway Endpoint
First, identify your VPC ID and the route table IDs for your private subnets:
# List your VPCs
aws ec2 describe-vpcs --query "Vpcs[*].{ID:VpcId,CIDR:CidrBlock,Name:Tags[?Key=='Name']|[0].Value}" --output table
# List route tables for your VPC
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=vpc-0abc123def456789a" \
--query "RouteTables[*].{ID:RouteTableId,Name:Tags[?Key=='Name']|[0].Value,SubnetAssociations:Associations[*].SubnetId}" \
--output json
Create the S3 Gateway Endpoint and associate it with your private subnet route tables:
# Create S3 Gateway Endpoint
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123def456789a \
--service-name com.amazonaws.us-east-1.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-0111111111111111a rtb-0222222222222222b
# Verify the endpoint was created
aws ec2 describe-vpc-endpoints \
--filters "Name=vpc-id,Values=vpc-0abc123def456789a" \
--query "VpcEndpoints[*].{ID:VpcEndpointId,Service:ServiceName,Type:VpcEndpointType,State:State}" \
--output table
After creation, verify the route was added to your route tables:
# Check that the prefix list route was added
aws ec2 describe-route-tables \
--route-table-ids rtb-0111111111111111a \
--query "RouteTables[0].Routes[?GatewayId!=null && starts_with(GatewayId,'vpce-')]" \
--output json
You should see a route with a destination of the S3 prefix list (e.g., pl-63a5400a) pointing to your VPC endpoint ID.
Create a DynamoDB Gateway Endpoint
# Create DynamoDB Gateway Endpoint
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123def456789a \
--service-name com.amazonaws.us-east-1.dynamodb \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-0111111111111111a rtb-0222222222222222b
Add an Endpoint Policy (Don’t Skip This)
By default, the endpoint policy allows full access to the service. In a production environment, restrict it. For example, limit the S3 endpoint to specific buckets:
# Create a policy file: s3-endpoint-policy.json
cat > s3-endpoint-policy.json << 'EOF'
{
"Statement": [
{
"Sid": "AllowSpecificBuckets",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-data-bucket",
"arn:aws:s3:::my-app-data-bucket/*",
"arn:aws:s3:::my-app-logs-bucket",
"arn:aws:s3:::my-app-logs-bucket/*"
]
}
]
}
EOF
# Apply the policy to the endpoint
aws ec2 modify-vpc-endpoint \
--vpc-endpoint-id vpce-0abc123def456789a \
--policy-document file://s3-endpoint-policy.json
Important: If your instances need to access ECR (which stores container images in S3), or need to install packages from Amazon Linux repos hosted on S3, you must include those S3 buckets in your endpoint policy — or keep the policy permissive enough to allow it. This is one of the most common causes of "everything broke after I added a VPC endpoint."
Step 3: Configure Interface Endpoints for Other AWS Services
For services beyond S3 and DynamoDB, you'll need Interface Endpoints. Let's set up endpoints for the most commonly used services in a typical workload: ECR (for container image pulls), CloudWatch Logs, STS, and Secrets Manager.
Identify Available Endpoint Services
# List all available VPC endpoint services in your region
aws ec2 describe-vpc-endpoint-services \
--query "ServiceNames[?contains(@,'amazonaws')]" \
--output text | tr '\t' '\n' | sort
Create a Security Group for Interface Endpoints
Interface Endpoints create ENIs, which means you can (and should) attach security groups:
# Create a security group for VPC endpoints
aws ec2 create-security-group \
--group-name vpc-endpoints-sg \
--description "Security group for VPC Interface Endpoints" \
--vpc-id vpc-0abc123def456789a
# Allow HTTPS (443) inbound from your VPC CIDR
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc123def456789a \
--protocol tcp \
--port 443 \
--cidr 10.0.0.0/16
Why port 443? All AWS service API calls use HTTPS. Your instances in the private subnet make API calls over port 443 to the endpoint ENI.
Create Interface Endpoints
# ECR requires TWO endpoints: ecr.api and ecr.dkr
# Plus you need S3 (for image layer storage) — handled by the Gateway Endpoint above
# ECR API endpoint
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc