月80万円のデータ転送費をVPCエンドポイント導入で削減した実装記録
AWS請求でデータ転送費が月80万円。NATゲートウェイ経由のリージョン内通信が原因でした。VPCエンドポイント導入で削減できた施策と、3ヶ月の実装で気づいた落とし穴をまとめます。
月80万円のデータ転送費に気づいた時の絶望感
先月のAWS請求書を見て、本気で驚いた。データ転送だけで月80万円。EC2からS3へのファイル転送、LambdaからDynamoDBへのアクセス、マイクロサービス間の通信——全部がNATゲートウェイを経由してインターネット経由で出ていってたんですよね。
うちのチームはずっと「インスタンスとサービスは自動的にプライベートで繋がってるもんだ」という錯覚を持ってたんですよ。実際に通信フローを追跡してみたら、S3へのAPI呼び出しが毎日数百万回。NAT経由だから、AWSリージョン内の同じサービス同士なのに、わざわざパブリックIPを経由して帰ってくる。これじゃ料金がかさむわけです。
うちの構成では、EC2からのS3 GET/PUT、Lambda→DynamoDB、マイクロサービス間のAPI呼び出しがリージョン内で頻繁に起きてた。これらが全部NAT(NATゲートウェイ / NATインスタンス)を使ってたから、データ転送料金が爆増していたんです。
VPCエンドポイント導入の全体設計
対策は明確でした。リージョン内のサービスにはVPCエンドポイント(VPC Endpoints)を使う。S3・DynamoDB・SNS・SQS・Secrets Managerなど、AWS側でサポートしているサービスなら、ゲートウェイタイプまたはインターフェースタイプのエンドポイントで直接繋ぐんです。NAT経由のコストはかかりません。
実装する前に、うちの現在の構成を図で整理しました。
graph TB
subgraph "Before: NAT経由"
EC2_old["EC2 instances<br/>in private subnet"]
NAT["NAT Gateway<br/>コスト: 月32万"]
IGW["Internet Gateway"]
S3_old["S3 bucket<br/>リージョン内"]
DDB_old["DynamoDB<br/>リージョン内"]
EC2_old -->|"毎秒thousands<br/>of requests"|NAT
NAT -->|"「外に出す」"|IGW
IGW -.->|"「リージョン内に<br/>戻ってくる」"|S3_old
IGW -.->|"「なぜ外を経由?」"|DDB_old
end
subgraph "After: VPCエンドポイント"
EC2_new["EC2 instances"]
S3_ep["S3 Gateway EP<br/>無料"]
DDB_ep["DynamoDB GW EP<br/>無料"]
SNS_ep["SNS Interface EP<br/>時給課金"]
S3_new["S3 bucket"]
DDB_new["DynamoDB"]
SNS["SNS"]
EC2_new -->|"「ダイレクト接続<br/>リージョン内」"|S3_ep
S3_ep -->|"0円での転送"|S3_new
EC2_new -->|"同一VPC内"|DDB_ep
DDB_ep -->|"無料"|DDB_new
EC2_new -->|"Interface EP<br/>時給課金だが"|SNS_ep
SNS_ep -->|"リージョン内接続"|SNS
end
style NAT fill:#ff6b6b
style S3_ep fill:#51cf66
style DDB_ep fill:#51cf66
図で見ると一目瞭然ですが、NATゲートウェイ経由だと「リージョン内のサービスなのに外に出して戻ってくる」という無駄が生じていたんですよね。
実装のポイントはこれです。
ゲートウェイタイプ(Gateway Endpoint): S3とDynamoDB。無料。ルートテーブルに追加するだけで動作します。
インターフェースタイプ(Interface Endpoint): SNS、SQS、Secrets Manager など。時間課金(1時間あたり$0.01)だけど、データ転送料金が大幅に安くなる仕組みです。
うちは、まずはゲートウェイタイプのS3とDynamoDBから始めました。これだけで月の通信量の6割を占めてたから、効果が大きいと判断したんですよね。
実装の手順と実際のコード
S3ゲートウェイエンドポイント
# VPC IDを取得
VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=production-vpc \
--query 'Vpcs[0].VpcId' --output text)
# ルートテーブルを確認
ROUTE_TABLE_ID=$(aws ec2 describe-route-tables \
--filters Name=vpc-id,Values=$VPC_ID Name=tag:Name,Values=private-subnet-1a \
--query 'RouteTables[0].RouteTableId' --output text)
# S3ゲートウェイエンドポイントを作成
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-1.s3 \
--route-table-ids $ROUTE_TABLE_ID \
--policy-file s3-endpoint-policy.json
ポリシーファイル例(s3-endpoint-policy.json):
{
"Statement": [
{
"Principal": "*",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-prod-bucket",
"arn:aws:s3:::my-prod-bucket/*"
]
}
]
}
ここで気をつけたポイントがあります。ポリシーを絞りすぎると、LambdaやECSタスクがS3にアクセスできなくなる んですよね。最初、うちはIAMロールのポリシーだけをチェックして、エンドポイント側のリソースポリシーをチェックしてませんでした。2時間ハマりました。ここは素直に失敗から学びました。
DynamoDBゲートウェイエンドポイント
# DynamoDBエンドポイント用ポリシー
cat > ddb-endpoint-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/orders-*"
}
]
}
EOF
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-1.dynamodb \
--route-table-ids $ROUTE_TABLE_ID \
--policy-file ddb-endpoint-policy.json
インターフェースエンドポイント(SNS/SQS例)
SNS/SQSはゲートウェイタイプが存在しないので、インターフェースタイプが必要になります。セキュリティグループも作る必要がありますね。
# セキュリティグループ作成
SG_ID=$(aws ec2 create-security-group \
--group-name vpc-endpoint-sg \
--description "Security group for VPC endpoints" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
# VPC内からのhttps(443)を許可
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 443 \
--source-security-group-id $SG_ID
# SNSインターフェースエンドポイント
aws ec2 create-vpc-endpoint \
--vpc-endpoint-type Interface \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-1.sns \
--subnet-ids subnet-12345 subnet-67890 \
--security-group-ids $SG_ID
重要なポイントです。インターフェースエンドポイントは複数のAZに分散配置するべきなんですよね。高可用性のために、少なくとも2つのAZを指定します。1つのAZだけだと、そこが障害になった時にサービス全体が止まってしまいますから。
CloudFrontの役割を整理したら、意外な発見
VPCエンドポイントはAWS内部の通信を最適化するものですが、うちの場合、ユーザーからのS3アクセス(Webアセット、動画、画像)もありました。ここでCloudFrontの役割が出てくるんですよね。
正直な話をすると、最初はCloudFrontが「キャッシュするだけ」だと思ってました。でも、データ転送コスト削減という観点では、これが超重要なツールなんですよ。
CloudFrontを使うと、こんなことが起きます:
-
オリジン(S3)から全世界への転送料金が不要になる。CloudFront自身が世界中の数百のエッジロケーションを持ってるので、多くのユーザーはエッジから配信されるんです。
-
S3への直アクセスを最小化できる。キャッシュヒット率が高いほど、オリジン転送が減るという仕組み。
-
リージョン間の転送も削減できる。複数リージョンを使ってる場合、CloudFrontがキャッシュを統一できるんですよね。
実装例を見てみましょう。
cat > cloudfront-config.json << 'EOF'
{
"CallerReference": "production-cf-2026-05",
"DefaultRootObject": "index.html",
"Origins": {
"Items": [
{
"Id": "myS3Origin",
"DomainName": "my-prod-bucket.s3.ap-northeast-1.amazonaws.com",
"S3OriginConfig": {
"OriginAccessIdentity": "origin-access-identity/cloudfront/ABCDEFG1234567"
}
}
],
"Quantity": 1
},
"DefaultCacheBehavior": {
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"ViewerProtocolPolicy": "https-only",
"TargetOriginId": "myS3Origin",
"ForwardedValues": {
"QueryString": false,
"Cookies": { "Forward": "none" }
},
"MinTTL": 0,
"DefaultTTL": 86400,
"MaxTTL": 31536000
},
"Enabled": true
}
EOF
aws cloudfront create-distribution --distribution-config file://cloudfront-config.json
キャッシュ戦略のポイント は、ファイルの種類で使い分けることですね。
- 静的ファイル(CSS、JS、画像): DefaultTTLを長めに(1日〜30日)設定して、頻繁に変わらないものはキャッシュさせる
- HTML: キャッシュは短めに(1時間)するか、キャッシュを無効化する戦略を取る
- API応答: そもそもキャッシュしない設定(Cache-Control: no-cache)にする
うちの場合、このCloudFront導入で、オリジン転送が月600GB → 月80GBに減りました。つまり、データ転送料金で月20万円の節約ですよ。効果抜群です。
実装後の数値と失敗談
実装後、3ヶ月のコスト推移はこんな感じです。
xychart-beta
title "データ転送コストの削減推移(月単位)"
x-axis ["導入前", "+1ヶ月", "+2ヶ月", "+3ヶ月"]
y-axis "コスト($)" 0 to 100000
line [80000, 55000, 32000, 20000]
- 導入前: $80,000/月(データ転送のみ)
- VPCエンドポイント導入後: $55,000/月(-31%)
- CloudFront導入後: $32,000/月(-60%)
- ルート最適化完了: $20,000/月(-75%)
数字で見るとこれだけの削減ができたんですよね。ただ、失敗もいくつかありました。そこから学んだことも大事です。
失敗1: NAT削除のタイミング
VPCエンドポイントを導入したからといって、すぐにNATゲートウェイを削除してはいけません。外部APIへのアクセス(例: GitHub、npm registry、外部SaaS API)はまだNATが必要なんですよね。うちは息急いて削除して、30分間デプロイパイプラインが止まりました。正直ヒヤッとしました。
対策はシンプルです。エンドポイント導入後も、NATゲートウェイは別のルートテーブル(外部通信用)に残しておく。これだけで解決します。
失敗2: インターフェースエンドポイントのコスト過大評価
SNS/SQSのインターフェースエンドポイント導入時、「時給課金だからやっぱり止めよう」と後ずさりしました。でも計算してみると、実はそうでもないんですよね。
- インターフェースエンドポイント: 1時間$0.01 × 24時間 × 30日 = $7.2/月
- SQS転送料金削減: 月200万メッセージ × $0.5/百万 = $100削減
実は月$100の削減で十分ペイして余るんです。むしろやらない理由がない、って気づきました。
失敗3: ポリシーの過度な絞り込み
セキュリティのためにVPCエンドポイントのリソースポリシーを異常に絞った結果、IAMロール別に異なるアクセスパターンが失敗しました。
やってしまった設定:
{
"Statement": [
{
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/lambda-execution-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/path/to/specific/file.txt"
}
]
}
これはセキュアですが、運用が硬すぎてファイル追加の度にポリシー更新が必要になるんですよ。本来はリソースパターンで柔軟に対応すべきでした。
{
"Statement": [
{
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/lambda-*"
},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
セキュリティと運用性のバランスが大事だと痛感しました。
複数AZでのVPCエンドポイント高可用性設計
ちょっと高度な話ですが、本番環境ではこのくらいは必要ですね。
graph TB
subgraph "AZ-1a"
EC2_1a[EC2]
ENI_1a["Interface Endpoint ENI"]
EC2_1a --> ENI_1a
end
subgraph "AZ-1c"
EC2_1c[EC2]
ENI_1c["Interface Endpoint ENI"]
EC2_1c --> ENI_1c
end
subgraph "AWS Service"
SNS[SNS]
end
ENI_1a -->|"VPC内通信"| SNS
ENI_1c -->|"VPC内通信"| SNS
style ENI_1a fill:#51cf66
style ENI_1c fill:#51cf66
ポイントはこれです。インターフェースエンドポイントを複数AZに展開すると、1つのAZがダウンしてもサービス継続できるんですよね。ただしAZ毎にENI(Elastic Network Interface)が増えるので、セキュリティグループ設定を忘れずに。特に443番ポートのインバウンド許可は必須です。
実装後の監視とトラブルシューティング
VPCエンドポイント導入後、実際に機能してるか監視する必要があります。
# VPCエンドポイントの利用状況をCloudWatch Logsで確認
aws logs create-log-group --log-group-name /aws/vpc-endpoints
# VPCフローログをCloudWatch Logsに出力
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids $VPC_ID \
--traffic-type ALL \
--log-group-name /aws/vpc-endpoints \
--deliver-logs-permission-role-arn arn:aws:iam::123456789012:role/vpc-flow-logs-role
S3にデータ転送が減ってるか確認するには、CloudWatchメトリクスを見るのが一番わかりやすいですね。
# CloudWatchメトリクスでS3への「出力バイト」を確認
aws cloudwatch get-metric-statistics \
--namespace AWS/NatGateway \
--metric-name BytesOutToDestination \
--start-time 2026-05-01T00:00:00Z \
--end-time 2026-05-10T00:00:00Z \
--period 3600 \
--statistics Sum
実際の削減額を見ると、NATゲートウェイの通信が99%減ってました。これが月32万円削減の正体なんですよ。目に見える効果があるから、チーム内でも実装の価値が認識されました。
まとめ
この3ヶ月でわかったデータ転送コスト削減の本質は、複数の施策を組み合わせることなんだと思います。
-
VPCエンドポイント(ゲートウェイタイプ)から始めるべき。S3・DynamoDBで月の転送量の大半を占めることが多いんですよね。無料だし、実装も簡単ですから。
-
CloudFrontは単なるキャッシュじゃなく、オリジン転送削減のためのツール。キャッシュヒット率を30%上げるだけで、月20万円のコスト削減。地味に便利です。
-
インターフェースエンドポイント(SNS/SQS)は「時給課金だからムダ」は誤解。転送料金削減でペイして余るんですよね。セキュアな通信パスも得られる一石二鳥。
-
ポリシー設計がキモ。セキュアだけど硬すぎると運用が地獄になります。リソースパターンの正規表現で柔軟性と安全性のバランス取る工夫が必要。
-
NAT削除は焦らない。外部API通信はまだNATが必要なんですよね。エンドポイント導入後も、外部通信用ルートテーブル経由のNATは残しておくのが無難です。
次のアクションはこんな感じです。
- VPCフローログを有効化して、現在のNAT通信量を可視化する
- S3・DynamoDB用のゲートウェイエンドポイント導入(1日で実装可能)
- CloudFront導入予定がなければ、先に検討してみる
- インターフェースエンドポイント(SNS/SQS/Secrets Manager)は、ゲートウェイ型のコスト改善を見てから段階的に進める
月100万円のAWS請求で月20万円削減できた経験から言えるのは、これらのツールは「理論」じゃなく、実装すれば確実に効くということです。うちのチームでも、今ではデフォルトでVPCエンドポイント前提の設計に変わりました。やってみる価値は十分あります。