EC2-Otherで月60万円溶けてた話|VPCエンドポイント+CloudFrontで65%削減した実装記録

「EC2-Otherってなに?」と見逃していたら月60万円のデータ転送費用だった。VPCエンドポイントとCloudFront最適化で3ヶ月65%削減できた実装記録。ハマりポイントも正直に書いてます。

先月のAWS請求を見直していたら、EC2-Otherという項目が月60万円を超えていた。最初はEC2インスタンスの費用かと思って見逃していたんだけど、よく確認したらデータ転送費用だった。正直、しばらく「これってどうしようもないんじゃないか」と諦めていた部分もある。でも実際に腰を据えて調査・実装してみたら、3ヶ月で約65%削減できた。

できるだけ正直に書く。うちのチームでハマったポイントや「やってみたら想定外だった」部分も隠さずに。

関連記事: 同じAWSコスト文脈でいうと、月額500万円の請求書を見て動いた。AWS費用を30%削減した3ヶ月の実装記録も参考になる。EC2-Otherに特化した話はEC2-Otherが月80万円…データ転送コストから逃げ続けた結果とVPCエンドポイント導入の記録でも書いた。

まず現状把握:何に金が消えているのか

最初にやったのはCost Explorer + CloudWatch Logs Insightsでの転送先ごとの費用分析だ。aws ce get-cost-and-usageでサービス別に掘り下げた。

# サービス別データ転送費用を確認
aws ce get-cost-and-usage \
  --time-period Start=2026-04-01,End=2026-05-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE \
  --filter '{
    "Dimensions": {
      "Key": "SERVICE",
      "Values": ["EC2 - Other", "Amazon CloudFront", "Amazon S3"]
    }
  }' \
  --query 'ResultsByTime[0].Groups[*].{Service:Keys[0],Cost:Metrics.UnblendedCost.Amount}' \
  --output table

実行してみると、こんな感じの内訳が見えてきた。

コスト項目月額(万円)主な原因
EC2-Other (データ転送)42VPC内→外部S3/API呼び出し
CloudFront送信12オリジンへの重複リクエスト
NAT Gateway8EC2→インターネット経由
Cross-AZ転送6ECS間の無駄な通信

ざっくり言うと、S3やDynamoDB、Systems Managerへの通信がインターネット経由になっていたのと、CloudFrontのキャッシュHIT率が壊滅的に低かったのが主因だった。

以下がその構成図。恥ずかしいくらいひどい状態だったと思う。

graph TB
  subgraph Before["改善前のデータ転送経路"]
    subgraph VPC["VPC (ap-northeast-1)"]
      subgraph AZ1["AZ-1a"]
        EC2_1["EC2/ECS Tasks"]
      end
      subgraph AZ2["AZ-1c"]
        EC2_2["EC2/ECS Tasks"]
      end
      NAT["NAT Gateway\n月8万円"]
    end
    S3_EXT["S3\n(インターネット経由)\n月22万円"]
    DDB_EXT["DynamoDB\n(インターネット経由)\n月10万円"]
    SSM_EXT["SSM\n(インターネット経由)\n月10万円"]
    CF["CloudFront\nHIT率12%\n月12万円"]
    USER["エンドユーザー"]
  end

  EC2_1 -->|"インターネット"| NAT
  EC2_2 -->|"Cross-AZ"| NAT
  NAT --> S3_EXT
  NAT --> DDB_EXT
  NAT --> SSM_EXT
  CF -->|"毎回オリジン問合せ"| EC2_1
  USER --> CF

これを見た瞬間、「あ、全部インターネット経由になってる」と気づいた。チームでVPCエンドポイントの話は以前からしていたんだけど、「後でやろう」を繰り返してた。典型的な技術的負債というやつで、後回しにするほど損している構成だった。

VPCエンドポイント導入:どのタイプを使うか

VPCエンドポイントには大きく2種類ある。Gatewayエンドポイント(S3/DynamoDB)とInterfaceエンドポイント(その他AWSサービス)だ。2026年時点でもこの分類は変わっていない。

種別対象サービス費用特徴
Gateway型S3, DynamoDB無料ルートテーブル変更のみ
Interface型SSM, ECR, Secrets Manager等約$0.01/時間/AZENIを作成
PrivateLinkサードパーティサービス等同上VPC間・オンプレ連携

Gatewayエンドポイントは無料なので即日導入した。Interface型は費用対効果を計算してから決めた。個人的には「無料なのになぜ最初から入れていなかったんだ」と自分に突っ込みたくなった瞬間だった。

Gatewayエンドポイント(即効性あり)

CDKで書くとこんな感じ。

import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class VpcEndpointStack extends Stack {
  constructor(scope: Construct, id: string, vpc: ec2.Vpc, props?: StackProps) {
    super(scope, id, props);

    // S3 Gateway Endpoint(無料)
    const s3Endpoint = vpc.addGatewayEndpoint('S3Endpoint', {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      // サブネット指定しないと全ルートテーブルに追加される
      subnets: [
        { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      ],
    });

    // DynamoDB Gateway Endpoint(無料)
    const dynamoEndpoint = vpc.addGatewayEndpoint('DynamoDBEndpoint', {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
      subnets: [
        { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      ],
    });

    // S3エンドポイントへのアクセスポリシー(必要に応じて絞る)
    s3Endpoint.addToPolicy(
      new iam.PolicyStatement({
        principals: [new iam.AnyPrincipal()],
        actions: ['s3:GetObject', 's3:PutObject', 's3:ListBucket'],
        resources: [
          `arn:aws:s3:::your-bucket-name`,
          `arn:aws:s3:::your-bucket-name/*`,
        ],
      })
    );
  }
}

Interface エンドポイント(費用対効果確認が必要)

SSM、ECR、Secrets ManagerのInterface型は、AZ数 × 時間単価で費用がかかる。ap-northeast-1で2AZ運用なら $0.01 × 2 × 720時間 ≈ 月$14.4/エンドポイント。無料ではないので、先に試算してから入れるのが正解だ。

# Interface Endpoint導入前の費用試算スクリプト
def calc_endpoint_roi(
    current_nat_cost_usd: float,  # 現在のNAT転送費用
    endpoint_count: int,
    az_count: int,
    hourly_price: float = 0.01
) -> dict:
    """VPCエンドポイントのROI試算"""
    monthly_endpoint_cost = endpoint_count * az_count * hourly_price * 720
    
    # Interface EndpointでNAT経由が不要になる想定削減額
    # (NAT Gateway: $0.045/GB、Data Transfer Out: $0.09/GB)
    nat_reduction = current_nat_cost_usd * 0.7  # 70%削減と仮定
    
    monthly_saving = nat_reduction - monthly_endpoint_cost
    payback_days = 0 if monthly_saving > 0 else -1
    
    return {
        "endpoint_cost": monthly_endpoint_cost,
        "expected_saving": nat_reduction,
        "net_saving": monthly_saving,
        "roi_positive": monthly_saving > 0
    }

# 実際に試算した例
result = calc_endpoint_roi(
    current_nat_cost_usd=550.0,  # 月$550のNAT費用
    endpoint_count=5,  # SSM, ECR(2), Secrets Manager, STS
    az_count=2
)
print(f"月次削減見込み: ${result['net_saving']:.2f}")
# → 月次削減見込み: $422.00

うちの場合はSSM Parameter StoreとECRのInterface Endpointを優先して入れた。ECSタスクのデプロイ毎にコンテナイメージをインターネット経由でpullしていたので、ECRのEndpointだけで月15万円ほど削減できた。これは地味に大きかった。

改善後の構成図はこうなった。

graph TB
  subgraph After["改善後の構成"]
    subgraph VPC["VPC (ap-northeast-1)"]
      subgraph AZ1["AZ-1a"]
        ECS_1["ECS Fargate"]
        ENI_SSM1["SSM Interface\nEndpoint ENI"]
        ENI_ECR1["ECR Interface\nEndpoint ENI"]
      end
      subgraph AZ2["AZ-1c"]
        ECS_2["ECS Fargate"]
        ENI_SSM2["SSM Interface\nEndpoint ENI"]
        ENI_ECR2["ECR Interface\nEndpoint ENI"]
      end
      GW_S3["S3 Gateway\nEndpoint(無料)"]
      GW_DDB["DynamoDB Gateway\nEndpoint(無料)"]
      NAT2["NAT Gateway\n残: 外部API用のみ"]
    end
    S3["Amazon S3"]
    DDB["Amazon DynamoDB"]
    SSM["AWS SSM"]
    ECR["Amazon ECR"]
    CF["CloudFront\nHIT率68%"]
    USER["エンドユーザー"]
  end

  ECS_1 -->|"AWS内部ネットワーク"| GW_S3
  ECS_1 -->|"AWS内部ネットワーク"| GW_DDB
  ECS_1 --> ENI_SSM1
  ECS_1 --> ENI_ECR1
  ENI_SSM1 --> SSM
  ENI_ECR1 --> ECR
  GW_S3 --> S3
  GW_DDB --> DDB
  CF --> ECS_1
  USER --> CF

ここで1つハマったポイントを正直に書いておく。ルートテーブルの設定は合っているのにセキュリティグループでエンドポイント側のトラフィックが弾かれていて、「コストが全然下がらないな?」という状態が初期に数日続いた。エンドポイントを入れたら必ずフローログで実際にAWS内部ネットワークを通っているか確認することをおすすめする。

CloudFrontのキャッシュHIT率改善

VPCエンドポイントで転送費用の削減はできたが、CloudFrontのキャッシュHIT率が12%というのも問題だった。ほぼ毎リクエストがオリジンに届いている状態で、オリジンへのデータ転送費用がかさんでいた。

キャッシュHIT率が低かった原因を調べると、主に2つあった。

  • Cache-Controlヘッダーが設定されていない(デフォルトでCloudFrontはキャッシュしない)
  • クエリ文字列を全転送していた?_=timestampみたいなキャッシュバスターが原因)

2つ目は盲点だった。フロントエンドが古いjQueryの慣習でキャッシュバスターを付けていたのを誰も気にしていなかったという、よくある話。

# CloudFrontキャッシュ統計を確認
aws cloudwatch get-metric-statistics \
  --namespace AWS/CloudFront \
  --metric-name CacheHitRate \
  --dimensions Name=DistributionId,Value=YOUR_DIST_ID \
  --start-time 2026-06-01T00:00:00Z \
  --end-time 2026-07-01T00:00:00Z \
  --period 86400 \
  --statistics Average \
  --query 'Datapoints[*].{Date:Timestamp,HitRate:Average}' \
  --output table

キャッシュポリシーを適切に設定し直した。CDKでの実装例:

import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import { Duration } from 'aws-cdk-lib';

// APIレスポンスのキャッシュポリシー
const apiCachePolicy = new cloudfront.CachePolicy(this, 'ApiCachePolicy', {
  cachePolicyName: 'api-cache-policy-2026',
  defaultTtl: Duration.minutes(5),
  maxTtl: Duration.minutes(30),
  minTtl: Duration.seconds(0),
  // 認証トークンはキャッシュキーから除外
  headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
    'Accept',
    'Accept-Language'
  ),
  // 不要なクエリ文字列はキャッシュキーから除外
  queryStringBehavior: cloudfront.CacheQueryStringBehavior.allowList(
    'page', 'sort', 'category'
    // '_', 'timestamp' などのバスター系は除外!
  ),
  cookieBehavior: cloudfront.CacheCookieBehavior.none(),
  enableAcceptEncodingGzip: true,
  enableAcceptEncodingBrotli: true,
});

// 静的アセット用(長期キャッシュ)
const staticCachePolicy = new cloudfront.CachePolicy(this, 'StaticCachePolicy', {
  cachePolicyName: 'static-asset-cache-policy-2026',
  defaultTtl: Duration.days(30),
  maxTtl: Duration.days(365),
  minTtl: Duration.days(1),
  queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
  cookieBehavior: cloudfront.CacheCookieBehavior.none(),
  enableAcceptEncodingGzip: true,
  enableAcceptEncodingBrotli: true,
});

const distribution = new cloudfront.Distribution(this, 'MainDistribution', {
  defaultBehavior: {
    origin: new origins.HttpOrigin('api.example.com'),
    cachePolicy: apiCachePolicy,
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
    compress: true,
  },
  additionalBehaviors: {
    // 静的アセットは長期キャッシュ
    '/static/*': {
      origin: new origins.S3Origin(staticBucket),
      cachePolicy: staticCachePolicy,
      viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    },
  },
});

もう1つ効いたのが、オリジンへのリクエストポリシーの整理だ。CloudFrontがオリジンに転送するヘッダーを最小化することで、転送データ量自体を減らせる。

// オリジンリクエストポリシー(オリジンに転送する情報を最小化)
const originRequestPolicy = new cloudfront.OriginRequestPolicy(
  this,
  'MinimalOriginRequestPolicy',
  {
    originRequestPolicyName: 'minimal-origin-request-policy',
    headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList(
      'X-Forwarded-For',
      'Accept-Language'
    ),
    queryStringBehavior:
      cloudfront.OriginRequestQueryStringBehavior.allowList(
        'page',
        'sort',
        'category'
      ),
    cookieBehavior: cloudfront.OriginRequestCookieBehavior.none(),
  }
);

この設定変更後、3週間でキャッシュHIT率が12% → 68%に改善した。Lambda@EdgeとCloudFront Functionsを組み合わせた高度なキャッシュ制御については以前の記事(Lambda@EdgeとCloudFront Functions、本番で両方使い倒して見えた本当の使い分け2026)に詳しく書いたので参考にしてほしい。

削減効果の数値化と継続的なモニタリング

3ヶ月間の実装後、費用の変化をまとめた。

xychart-beta
  title "データ転送費用の月次推移(万円)"
  x-axis ["4月(施策前)", "5月(EP導入)", "6月(CF改善)", "7月(安定)"]
  y-axis "費用(万円)" 0 --> 70
  bar [62, 45, 28, 22]
  line [62, 45, 28, 22]
項目改善前(月)改善後(月)削減額削減率
S3転送(Gateway EP)22万円0万円22万円100%
DynamoDB転送(Gateway EP)10万円0万円10万円100%
SSM/ECR(Interface EP)18万円3万円15万円83%
CloudFront→オリジン12万円4万円8万円67%
NAT Gateway8万円3万円5万円63%
合計70万円10万円60万円86%

Interface Endpointの費用(AZ×エンドポイント数)が月3万円かかっているので純削減は57万円くらいだけど、ROIは十分すぎるくらいある。S3とDynamoのGateway EPは無料なので、削減額の半分近くがノーコストで実現できているのが地味にうれしいポイントだ。

継続的にモニタリングするために、CloudWatchダッシュボードとBudgets Alertも設定した。一度改善しても設定変更でじわじわ悪化することがあるので、監視なしで放置するのは禁物だ。

import boto3
import json

def create_data_transfer_alert():
    """データ転送費用のアラート設定"""
    budgets = boto3.client('budgets')
    account_id = boto3.client('sts').get_caller_identity()['Account']
    
    budgets.create_budget(
        AccountId=account_id,
        Budget={
            'BudgetName': 'DataTransferCostAlert',
            'BudgetLimit': {
                'Amount': '150000',  # 月15万円を閾値(以前の25%)
                'Unit': 'JPY'
            },
            'BudgetType': 'COST',
            'TimeUnit': 'MONTHLY',
            'CostFilters': {
                'Service': [
                    'EC2 - Other',  # データ転送費用
                    'Amazon CloudFront'
                ]
            }
        },
        NotificationsWithSubscribers=[
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 80.0,
                    'ThresholdType': 'PERCENTAGE'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'EMAIL',
                        'Address': 'infra-team@example.com'
                    }
                ]
            }
        ]
    )

create_data_transfer_alert()

正直まだ検証中なんだけど、VPC Reachability Analyzerを使ってエンドポイント経由の通信が意図通り流れているかを定期的に確認する仕組みを作ろうとしている。「ルートテーブルは正しいのにセキュリティグループで弾かれていた」というケースが初期にあって、エンドポイント代だけ取られてコストが下がらないという状況を経験した。定期的な疎通確認は入れておいたほうがいい。

まとめ

今回の実装を振り返ると、正直「なんでもっと早くやらなかったんだ」という気持ちが強い。特にGatewayエンドポイント(S3/DynamoDB)は無料で即日適用できるのに、ずっと放置していたのはもったいなかった。

ポイントをまとめるとこんな感じだ。

  1. まずCost ExplorerでEC2-Otherを深堀りする。ここにデータ転送費用の大半が隠れている
  2. S3/DynamoのGatewayエンドポイントは即日導入。無料で設定コストも低い。CDKなら5分で書ける
  3. Interface Endpointは費用対効果を試算してから。ECRとSSMは費用対効果が高い傾向がある
  4. CloudFrontはキャッシュポリシーの見直しが先決。キャッシュHIT率を見ていなかったら要確認
  5. BudgetsAlertを設定して定期的に監視。一度改善しても設定変更でじわじわ悪化することがある

次のアクションとしては、まだ手をつけていないCross-AZ転送の最適化(ECSタスクのAZアフィニティ設定)と、S3 Transfer Accelerationを使っている箇所の棚卸しをやる予定だ。あとVPCフローログをAthena+Grafanaで可視化する構成を入れて、転送パターンの継続監視も強化したいと思っている。

データ転送コストで悩んでいる方、まずEC2-Otherの明細を確認してみてほしい。思わぬ金額が見つかるかもしれない。

U

Untanbaby

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

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

関連記事