月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を使うと、こんなことが起きます:

  1. オリジン(S3)から全世界への転送料金が不要になる。CloudFront自身が世界中の数百のエッジロケーションを持ってるので、多くのユーザーはエッジから配信されるんです。

  2. S3への直アクセスを最小化できる。キャッシュヒット率が高いほど、オリジン転送が減るという仕組み。

  3. リージョン間の転送も削減できる。複数リージョンを使ってる場合、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ヶ月でわかったデータ転送コスト削減の本質は、複数の施策を組み合わせることなんだと思います。

  1. VPCエンドポイント(ゲートウェイタイプ)から始めるべき。S3・DynamoDBで月の転送量の大半を占めることが多いんですよね。無料だし、実装も簡単ですから。

  2. CloudFrontは単なるキャッシュじゃなく、オリジン転送削減のためのツール。キャッシュヒット率を30%上げるだけで、月20万円のコスト削減。地味に便利です。

  3. インターフェースエンドポイント(SNS/SQS)は「時給課金だからムダ」は誤解。転送料金削減でペイして余るんですよね。セキュアな通信パスも得られる一石二鳥。

  4. ポリシー設計がキモ。セキュアだけど硬すぎると運用が地獄になります。リソースパターンの正規表現で柔軟性と安全性のバランス取る工夫が必要。

  5. NAT削除は焦らない。外部API通信はまだNATが必要なんですよね。エンドポイント導入後も、外部通信用ルートテーブル経由のNATは残しておくのが無難です。

次のアクションはこんな感じです。

  • VPCフローログを有効化して、現在のNAT通信量を可視化する
  • S3・DynamoDB用のゲートウェイエンドポイント導入(1日で実装可能)
  • CloudFront導入予定がなければ、先に検討してみる
  • インターフェースエンドポイント(SNS/SQS/Secrets Manager)は、ゲートウェイ型のコスト改善を見てから段階的に進める

月100万円のAWS請求で月20万円削減できた経験から言えるのは、これらのツールは「理論」じゃなく、実装すれば確実に効くということです。うちのチームでも、今ではデフォルトでVPCエンドポイント前提の設計に変わりました。やってみる価値は十分あります。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事