EC2-Otherが月80万円…データ転送コストから逃げ続けた結果とVPCエンドポイント導入の記録
「EC2-Otherって何?」で済ませてきたツケが月80万円でした。VPCエンドポイントとCloudFrontで向き合った3ヶ月の実装記録、チューニング中の部分も含めて公開します。
先日、請求ダッシュボードを眺めていたら「EC2-Other」の項目が月80万円を突破していることに気づいて、胃が痛くなった。EC2本体じゃなくて「Other」だぞ?調べたら大半がデータ転送コストで、正直「何年もこれ垂れ流してたのか」と頭を抱えた。
うちのチームでは以前から月額500万円の請求書を見て動いた話で全社的なコスト削減の取り組みをしていたんだけど、データ転送コストは「なんか複雑そう」という空気感で後回しにされてきた経緯があった。今回はもう逃げずに全部向き合って、VPCエンドポイントとCloudFrontを組み合わせた結果、3ヶ月で月80万円のコスト削減に成功した話を書く。
完璧に解決したとは言えないし、正直まだチューニング中の部分もある。でも「やってみてわかったこと」を包み隠さず残しておきたい。
まず現状把握:どこでデータ転送コストが発生していたのか
AWSのデータ転送コストは本当にわかりにくい。公式ドキュメントを読むたびに「なんでこんなに複雑にするんだ」と思うんだけど、まずは現状把握から始めた。
Cost Explorerでサービス別・使用タイプ別にフィルタリングして、「DataTransfer」を含む項目を全部洗い出すのが最初の一手だった。
# AWS CLIでデータ転送コスト上位を抽出
aws ce get-cost-and-usage \
--time-period Start=2026-03-01,End=2026-04-01 \
--granularity MONTHLY \
--filter '{
"Dimensions": {
"Key": "USAGE_TYPE_GROUP",
"Values": ["EC2: Data Transfer - Internet", "EC2: Data Transfer - Region to Region", "EC2: Data Transfer - CloudFront"]
}
}' \
--group-by '[{"Type": "DIMENSION", "Key": "USAGE_TYPE"}]' \
--metrics BlendedCost
結果を整理すると、大きく3つの問題が見えてきた。
| コスト発生源 | 月額(概算) | 原因 |
|---|---|---|
| S3へのNATゲートウェイ経由通信 | ¥320,000 | VPCエンドポイント未設定 |
| API Gateway → EC2のクロスAZ通信 | ¥180,000 | マルチAZ設計の副作用 |
| 静的アセットのオリジン直接配信 | ¥300,000 | CloudFront未活用 |
| 合計 | ¥800,000 | - |
S3へのNATゲートウェイ経由が一番デカかった。プライベートサブネットのLambdaやECSタスクが、S3にアクセスするたびにNATゲートウェイを通っていた。NATゲートウェイは処理したデータ量に応じて課金されるから、大量のS3 I/Oがそのまま転送コストになっていた構図だ。
xychart-beta
title "月別データ転送コスト推移(改善前後)"
x-axis ["2026-01", "2026-02", "2026-03", "2026-04(対策開始)", "2026-05", "2026-06"]
y-axis "コスト(万円)" 0 --> 100
bar [72, 78, 80, 68, 32, 22]
line [72, 78, 80, 68, 32, 22]
VPCエンドポイントの実装:ゲートウェイ型とインターフェース型の使い分け
VPCエンドポイントには「ゲートウェイ型」と「インターフェース型(PrivateLink)」の2種類ある。最初は「両方入れときゃいいでしょ」って思ってたんだけど、コスト構造が全然違うので要注意だ。
| 種別 | 対象サービス | コスト | 仕組み |
|---|---|---|---|
| ゲートウェイ型 | S3・DynamoDB | 無料 | ルートテーブルにエントリ追加 |
| インターフェース型(PrivateLink) | Secrets Manager など多数 | 時間課金(約$0.01/時間/AZ)+データ処理量課金(約$0.01/GB) | ENIを作成 |
ゲートウェイ型はエンドポイント自体が無料で、データ転送コストも発生しない。これはマジで入れない理由がない。一方でインターフェース型は使い方によっては逆にコストが上がることもあるから、事前の試算が必須だ。
うちのチームで最初にやったのはゲートウェイ型のS3エンドポイントの設定だった。CDKで書くとこんな感じ:
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cdk from 'aws-cdk-lib';
export class VpcEndpointStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: cdk.StackProps) {
super(scope, id, props);
const vpc = ec2.Vpc.fromLookup(this, 'ExistingVpc', {
vpcId: 'vpc-xxxxxxxxxx',
});
// ゲートウェイ型:S3エンドポイント(無料)
const s3Endpoint = vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
// プライベートサブネットのルートテーブルに自動追加
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
});
// ゲートウェイ型:DynamoDB(こちらも無料)
const dynamoEndpoint = vpc.addGatewayEndpoint('DynamoDBEndpoint', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
});
// インターフェース型:Secrets Manager(時間+データ課金あり)
// NATゲートウェイ通過コストと比較して採算が取れる場合のみ
const secretsEndpoint = vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
privateDnsEnabled: true,
// マルチAZにするとENI数が増えてコストが上がるので注意
subnets: { availabilityZones: ['ap-northeast-1a'] }, // 本番はマルチAZ推奨だが要コスト試算
});
// S3バケットポリシーでVPCエンドポイント経由のみ許可(セキュリティ強化)
// 必要に応じてバケットポリシーを追加する
new cdk.CfnOutput(this, 'S3EndpointId', {
value: s3Endpoint.vpcEndpointId,
});
}
}
インターフェース型のエンドポイントは、NATゲートウェイのコストと比較して本当に安くなるか計算してから入れないと痛い目を見る。VPCフローログ1年半運用で月15万円の請求に気づいた話でも触れられているけど、フローログを使ってトラフィック量を把握してから判断するのが正解だ。
以下の計算式で試算することをおすすめする:
# VPCエンドポイント vs NAT Gateway コスト試算
def calc_endpoint_vs_nat(monthly_gb: float, num_az: int = 2):
"""
monthly_gb: 月間データ転送量(GB)
num_az: マルチAZ数
"""
# NAT Gatewayコスト
nat_data_cost = monthly_gb * 0.045 # $0.045/GB
nat_hour_cost = 730 * 0.045 * num_az # 時間課金
nat_total = nat_data_cost + nat_hour_cost
# インターフェース型エンドポイントコスト
endpoint_data_cost = monthly_gb * 0.01 # $0.01/GB
endpoint_hour_cost = 730 * 0.01 * num_az # 時間課金
endpoint_total = endpoint_data_cost + endpoint_hour_cost
savings = nat_total - endpoint_total
print(f"月間転送量: {monthly_gb} GB")
print(f"NAT Gateway総コスト: ${nat_total:.2f}")
print(f"VPCエンドポイント総コスト: ${endpoint_total:.2f}")
print(f"月間削減額: ${savings:.2f}")
print(f"削減率: {savings/nat_total*100:.1f}%")
return savings > 0
# 実際に試算した例(S3への月間1TB転送)
calc_endpoint_vs_nat(monthly_gb=1024, num_az=2)
# 月間転送量: 1024 GB
# NAT Gateway総コスト: $112.82
# VPCエンドポイント総コスト: $24.86
# 月間削減額: $87.96
# 削減率: 77.9%
S3・DynamoDB向けのゲートウェイ型を入れるだけで、月の転送コストが約35万円削減できた。これは文字通り「設定変えただけ」でお金が浮いたので、チーム内でちょっとした英雄扱いになった(笑)。
AWS構成図:VPCエンドポイント最適化後のアーキテクチャ
graph TB
subgraph Internet["インターネット"]
Users["👥 ユーザー"]
CFront["CloudFront"]
end
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph PublicSubnet["パブリックサブネット"]
ALB["Application\nLoad Balancer"]
NATGW["NAT Gateway\n⚠️ 対象トラフィック削減"]
end
subgraph PrivateSubnet_1A["プライベートサブネット (AZ-a)"]
ECS_A["ECS Task"]
Lambda_A["Lambda"]
end
subgraph PrivateSubnet_1C["プライベートサブネット (AZ-c)"]
ECS_C["ECS Task"]
Lambda_C["Lambda"]
end
subgraph Endpoints["VPCエンドポイント"]
S3EP["S3 Gateway Endpoint\n✅ 無料"]
DDB_EP["DynamoDB Gateway Endpoint\n✅ 無料"]
SM_EP["Secrets Manager\nInterface Endpoint\n💰 有料だがNATより安"]
end
end
subgraph AWSServices["AWSマネージドサービス"]
S3["Amazon S3"]
DDB["DynamoDB"]
SM["Secrets Manager"]
APIGW["API Gateway"]
end
Users -->|"HTTPS"| CFront
CFront -->|"静的アセット\nキャッシュ"| S3
CFront -->|"動的コンテンツ"| ALB
ALB --> ECS_A
ALB --> ECS_C
ECS_A -->|"プライベート通信"| S3EP
ECS_C -->|"プライベート通信"| S3EP
Lambda_A -->|"プライベート通信"| S3EP
Lambda_A -->|"プライベート通信"| DDB_EP
Lambda_A -->|"プライベート通信"| SM_EP
S3EP --> S3
DDB_EP --> DDB
SM_EP --> SM
ECS_A -->|"インターネット向け\nのみNAT経由"| NATGW
NATGW --> APIGW
style S3EP fill:#2ecc71,color:#fff
style DDB_EP fill:#2ecc71,color:#fff
style SM_EP fill:#f39c12,color:#fff
style NATGW fill:#e74c3c,color:#fff
style CFront fill:#3498db,color:#fff
この構成のポイントは「NATゲートウェイを完全になくさない」こと。サードパーティAPIへのアウトバウンド通信など、インターネットが必要なものはNAT経由のままでいい。AWSのマネージドサービスへの通信だけをエンドポイント経由に切り替えるのが現実的だ。
CloudFrontでの静的アセットキャッシュ最適化
VPCエンドポイントで35万円削減した後、次に手を付けたのがCloudFrontの設定見直しだった。正直最初は「CloudFrontは入ってるからもう大丈夫でしょ」と思ってたんだけど、甘かった。
うちのシステムはS3からの静的アセット配信にCloudFrontを使っていたんだけど、キャッシュヒット率が23% しかなかった。つまり7割以上のリクエストがS3オリジンまで到達していた状態だ。「CloudFront入れてある」と「CloudFrontが機能している」は全然別の話だった。
# CloudFrontのキャッシュヒット率を確認
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=XXXXXXXX Name=Region,Value=Global \
--start-time 2026-05-01T00:00:00Z \
--end-time 2026-06-01T00:00:00Z \
--period 86400 \
--statistics Average
原因を調べたら、クエリストリングやCookieをキャッシュキーに含めすぎていたのが問題だった。細かいクエリパラメータの差異でキャッシュが別々に作られてしまっていて、実質的にパススルーと大差ない状態になっていた。
CloudFrontのキャッシュポリシーを見直した設定がこれ:
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
// カスタムキャッシュポリシー(静的アセット用)
const staticAssetsCachePolicy = new cloudfront.CachePolicy(this, 'StaticAssetsCachePolicy', {
cachePolicyName: 'StaticAssets-Optimized-2026',
defaultTtl: cdk.Duration.days(30),
maxTtl: cdk.Duration.days(365),
minTtl: cdk.Duration.seconds(0),
// クエリストリングをキャッシュキーから除外(重要!)
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
// Cookieも除外
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
// バージョン管理はファイルパスで行う(content-hashをパスに含める)
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
});
// 動的コンテンツ用のオリジンリクエストポリシー
const dynamicOriginRequestPolicy = new cloudfront.OriginRequestPolicy(this, 'DynamicOriginPolicy', {
originRequestPolicyName: 'Dynamic-API-Policy',
queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(),
cookieBehavior: cloudfront.OriginRequestCookieBehavior.all(),
headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList(
'Authorization',
'Accept',
'Accept-Language',
),
});
// ディストリビューション設定
const distribution = new cloudfront.Distribution(this, 'MainDistribution', {
defaultBehavior: {
origin: new origins.LoadBalancerV2Origin(alb, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, // 動的コンテンツはキャッシュしない
originRequestPolicy: dynamicOriginRequestPolicy,
},
additionalBehaviors: {
// 静的アセットパスはS3から長期キャッシュ
'/static/*': {
origin: new origins.S3Origin(staticBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachePolicy: staticAssetsCachePolicy,
compress: true,
},
'/_next/static/*': {
origin: new origins.S3Origin(staticBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachePolicy: staticAssetsCachePolicy,
compress: true,
},
'/images/*': {
origin: new origins.S3Origin(staticBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachePolicy: staticAssetsCachePolicy,
compress: true,
// 画像最適化:将来的にCloudFront Image Transformationも検討
},
},
// WAFは別途関連付け
priceClass: cloudfront.PriceClass.PRICE_CLASS_200, // アジア・北米・欧州カバー
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, // HTTP/3対応(2026年はこれが標準)
});
この設定変更でキャッシュヒット率が23%から78% まで改善した。体感でもオリジンへのリクエスト数がガクッと減ったのがわかる。
もう一つ地味に効いたのが、S3オリジンへのCloudFrontアクセスを「オリジンアクセスコントロール(OAC)」経由に統一したこと。2026年時点ではOAIは非推奨になっているので、まだOAIを使っている人は移行した方がいい。セキュリティ観点の対応ではあるんだけど、バケットポリシーを整理する過程でパブリックアクセスが残っていた不要なオブジェクトを発見してついでに整理できた。棚ぼた的な副産物だった。
CloudFrontとLambda@Edgeの使い分けについてはLambda@EdgeとCloudFront Functions、本番で両方使い倒して見えた本当の使い分け2026が参考になると思う。あちらでも言及されているけど、キャッシュカスタマイズ程度ならFunctionsで十分で、重い処理だけEdgeに委ねるのが2026年の定石になってきた。
3ヶ月の実装結果と、まだ残っている課題
3ヶ月かけた結果をまとめるとこうなった:
pie title コスト削減の内訳(月額)
"S3 Gatewayエンドポイント" : 32
"DynamoDB Gatewayエンドポイント" : 3
"Secrets Manager Interfaceエンドポイント" : 5
"CloudFrontキャッシュ最適化" : 40
トータルで月80万円の削減。年間にすると約960万円。うちのサービス規模でこれだけ浮くのは結構デカかった。しかも大部分が「設定を正しく直す」だけで達成できたのが、正直悔しくもあり救いでもある。
ただ、正直まだ完全に解決できていない課題もある。
クロスAZ通信のコストは、まだ18万円/月残っている。ALBとECSタスクが異なるAZにまたがるケースでのデータ転送コストだ。これはAZアフィニティの設定(ECS Service ConnectのDNS-based routing)で改善できそうと踏んでいるが、可用性への影響を慎重に評価中だ。ECS Service Connectに移行して6ヶ月、App Meshの複雑さから解放された話で触れられているService Connectの設定と組み合わせれば行けそうなんだけど、ここは可用性との兼ね合いが難しい。
# クロスAZ通信を可視化するVPCフローログクエリ(Athena)
SELECT
srcaddr,
dstaddr,
sum(bytes) as total_bytes,
sum(bytes) / 1073741824.0 as total_gb
FROM vpc_flow_logs
WHERE
action = 'ACCEPT'
AND srcaddr LIKE '10.0.1.%' -- AZ-aのサブネット
AND dstaddr LIKE '10.0.2.%' -- AZ-cのサブネット
AND year = '2026'
AND month = '05'
GROUP BY srcaddr, dstaddr
ORDER BY total_gb DESC
LIMIT 20;
あと、インターフェース型エンドポイントをマルチAZで運用するとENIのコストが倍になる点も要注意だ。単一AZにするとエンドポイント障害時のリスクがあるので、ここはサービスのSLAと相談しながら判断してほしい。うちは今Secrets Managerエンドポイントを1AZで運用していて、ちょっとヒヤヒヤしている。
Verify & Trustの原則で言えば、VPCフローログ、有効化して放置してませんか?Athena×Grafana構成を1年半運用してわかったことのような可視化基盤があると、転送コストの変化を即座に察知できて良い。今回もフローログの分析がなければここまで深掘りできなかったと思う。
皆さんのチームではデータ転送コストって把握できてます?「EC2-Other」が怪しく膨らんでたら、ぜひCost Explorerで使用タイプ別に分解してみてほしい。思わぬところから掘り出し物のコスト削減ポイントが見つかるはずだ。
まとめ
3ヶ月の取り組みで見えてきたポイントを整理する。
1. S3・Dynamoのゲートウェイエンドポイントはまず入れる 無料で効果が大きい。NATゲートウェイ経由でS3にアクセスしているワークロードがあれば、即座に設定すべき。CDKで10分もあれば入れられる。
2. インターフェース型エンドポイントは試算してから 時間課金とデータ課金がある。NATゲートウェイとのコスト比較を事前に計算して、黒字になる場合のみ導入する。月間データ転送量が数百GB以上なら採算が取れることが多い。
3. CloudFrontのキャッシュヒット率を定期確認する 「入れてあれば安心」は危険。クエリストリングやCookieの設定ミスで23%まで落ちることがある。本番後も定期的にCacheHitRateメトリクスを確認する習慣をつける。
4. VPCフローログはAthena連携で分析する 「どのホスト間でどれだけ通信しているか」が見えないと、コスト削減の優先順位がつけられない。フローログはS3に吐いてAthenaで分析する構成が2026年の標準だ。
5. クロスAZ通信は可用性と天秤で判断 完全に消すことはできないが、Service ConnectやAZアフィニティ設定で軽減できる。SLAを確認しながら進める。
次のアクション:まずaws ce get-cost-and-usageでDataTransferを含む使用タイプを洗い出して、S3・DynamoDBへのNAT経由アクセスがないか確認してほしい。ゲートウェイエンドポイントを入れるだけで効果が出るケースはかなり多いはずだ。