謎の「EC2-Other」150万円と格闘した3ヶ月|FinOpsで月次コストを30%削減した実装記録

「このクラウド費用、何とかならないのか」と言われた日から始まったコスト改革。誰も説明できなかった謎の請求項目を起点に、FinOpsの仕組みを整備して500万円→350万円にした実話です。

月500万円の請求書を開いたら、謎の「EC2-Other」が150万円あった

正直に言う。うちのチームがAWSコストと本気で向き合い始めたのは、CFOから「このクラウド費用、何とかならないのか」と言われた日からだ。当時の月次請求が500万円を超えていて、しかも内訳を説明できるエンジニアが誰もいない。Cost ExplorerでEC2を開くと「EC2-Other」という謎の項目が150万円。誰もその正体を知らないまま半年が経過していた。

そこから3ヶ月でFinOpsの仕組みを整備して、月次コストを約30%削減できた。FinOpsって概念論が多くて「で、実際何をすればいいの?」ってなりがちなので、実際に実装した内容をそのまま書く。タグ戦略の整備やRI/Savings Plansの購入判断についてはRIカバレッジ分析を自動化したら月120万円削減できた話Savings Plans vs Reserved Instancesが詳しいので、合わせて読んでほしい。

FinOpsサイクルの回し方と2026年の現実

FinOpsの教科書的な説明は省く。実務で大事なのは「Inform → Optimize → Operate」のサイクルを実際に回せるチームを作ることで、それには組織的な仕組みが必要だということを最初に痛感した。

flowchart TB
    subgraph FinOps_Cycle["FinOpsサイクル"]
        A["📊 Inform\n可視化・タグ整備\nコスト配賦"] --> B["🔧 Optimize\nRI/SP購入\n右サイズ化\nUnused削除"]
        B --> C["⚙️ Operate\nバジェットアラート\n異常検知\n定期レビュー"]
        C --> A
    end

    subgraph AWS_Tools["AWSツール群"]
        D["Cost Explorer"]
        E["Cost Anomaly Detection"]
        F["AWS Budgets"]
        G["Compute Optimizer"]
        H["Trusted Advisor"]
    end

    A --- D
    A --- E
    C --- F
    B --- G
    B --- H

うちがハマったのは「Inform」フェーズだった。タグがバラバラで、誰のコストか分からない。Environment: prodenv: productionが混在していて、コスト配賦レポートがまともに機能しない。まずここを整備するだけで1ヶ月かかった。

タグ戦略の整備(正直しんどかった)

2026年現在、AWS Organizations + Tag Policiesを使うのが定番になってきた。SCP(Service Control Policy)でタグなしリソースの作成を禁止する組み合わせが効く。

{
  "tags": {
    "CostCenter": {
      "tag_value": {
        "@@assign": ["engineering", "platform", "data", "ml", "infra"]
      },
      "enforced_for": {
        "@@assign": [
          "ec2:instance",
          "rds:db",
          "lambda:function",
          "eks:cluster"
        ]
      }
    },
    "Environment": {
      "tag_value": {
        "@@assign": ["prod", "staging", "dev"]
      },
      "enforced_for": {
        "@@assign": [
          "ec2:instance",
          "rds:db"
        ]
      }
    },
    "Owner": {
      "tag_key": {
        "@@operators_allowed_for_child_policies": ["@@none"]
      }
    }
  }
}

このTag Policyを導入した直後、既存リソースで非準拠のものが3,000件以上あることが判明した。これを修正するのが地味に一番しんどかった。自動化スクリプトを書いて対応したが、完全に整備されるまで2週間かかった。

Cost Explorerを使いこなすための実践テクニック

Cost ExplorerはUIが豊富すぎて、最初は何を見ればいいのか分からない。3ヶ月使い込んで「これを週次で見れば十分」という構成にようやく落ち着いた。

週次ダッシュボードの構成

xychart-beta
    title "サービス別週次コスト推移(万円)"
    x-axis ["Week1", "Week2", "Week3", "Week4", "Week5", "Week6", "Week7", "Week8"]
    y-axis "コスト(万円)" 0 --> 180
    line [150, 155, 148, 162, 145, 138, 132, 125]
    bar [62, 65, 60, 68, 58, 54, 52, 48]

Cost Explorer APIを使ってSlackに自動投稿する仕組みを作った。週次で「先週比±X%」が分かるだけで、チームの意識が変わった。個人的にはこれが一番コスパの高い施策だったと思う。

import boto3
import json
from datetime import datetime, timedelta
from typing import Optional

def get_weekly_cost_summary(
    start_date: Optional[str] = None,
    end_date: Optional[str] = None
) -> dict:
    """
    Cost Explorer APIで週次コストサマリーを取得
    """
    ce = boto3.client('ce', region_name='us-east-1')
    
    if not end_date:
        end_date = datetime.now().strftime('%Y-%m-%d')
    if not start_date:
        start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
    
    response = ce.get_cost_and_usage(
        TimePeriod={
            'Start': start_date,
            'End': end_date
        },
        Granularity='DAILY',
        Metrics=['UnblendedCost', 'UsageQuantity'],
        GroupBy=[
            {'Type': 'DIMENSION', 'Key': 'SERVICE'},
            {'Type': 'TAG', 'Key': 'CostCenter'}
        ],
        Filter={
            'Dimensions': {
                'Key': 'RECORD_TYPE',
                'Values': ['Usage', 'SavingsPlanCoveredUsage']
            }
        }
    )
    
    # サービス別にコストを集計
    service_costs = {}
    for result in response['ResultsByTime']:
        for group in result['Groups']:
            service = group['Keys'][0]
            cost = float(group['Metrics']['UnblendedCost']['Amount'])
            service_costs[service] = service_costs.get(service, 0) + cost
    
    # コスト上位10サービスを返す
    sorted_services = sorted(
        service_costs.items(),
        key=lambda x: x[1],
        reverse=True
    )[:10]
    
    total = sum(v for _, v in sorted_services)
    
    return {
        'period': f'{start_date} ~ {end_date}',
        'total_usd': total,
        'top_services': [
            {'service': k, 'cost_usd': v, 'ratio': v/total*100}
            for k, v in sorted_services
        ]
    }


def detect_cost_anomalies(
    threshold_percent: float = 20.0
) -> list[dict]:
    """
    前週比でコスト異常を検出
    """
    today = datetime.now()
    this_week_end = today.strftime('%Y-%m-%d')
    this_week_start = (today - timedelta(days=7)).strftime('%Y-%m-%d')
    last_week_start = (today - timedelta(days=14)).strftime('%Y-%m-%d')
    
    current = get_weekly_cost_summary(this_week_start, this_week_end)
    previous = get_weekly_cost_summary(last_week_start, this_week_start)
    
    anomalies = []
    prev_costs = {s['service']: s['cost_usd'] for s in previous['top_services']}
    
    for service_data in current['top_services']:
        service = service_data['service']
        curr_cost = service_data['cost_usd']
        prev_cost = prev_costs.get(service, 0)
        
        if prev_cost > 0:
            change_pct = (curr_cost - prev_cost) / prev_cost * 100
            if change_pct > threshold_percent:
                anomalies.append({
                    'service': service,
                    'current_cost': curr_cost,
                    'previous_cost': prev_cost,
                    'change_percent': change_pct
                })
    
    return sorted(anomalies, key=lambda x: x['change_percent'], reverse=True)


if __name__ == '__main__':
    summary = get_weekly_cost_summary()
    print(f"今週のAWSコスト: ${summary['total_usd']:,.2f}")
    print("\n上位サービス:")
    for s in summary['top_services'][:5]:
        print(f"  {s['service']}: ${s['cost_usd']:,.2f} ({s['ratio']:.1f}%)")
    
    anomalies = detect_cost_anomalies(threshold_percent=15.0)
    if anomalies:
        print("\n⚠️ コスト異常検出:")
        for a in anomalies:
            print(f"  {a['service']}: +{a['change_percent']:.1f}% (${a['current_cost']:,.2f})")

このスクリプトをEventBridgeで毎週月曜9時に動かして、Lambdaを経由してSlackに投稿する。detect_cost_anomaliesの閾値は最初30%で設定していたが、ノイズが多かったので15%に下げた。ここは好みが分かれるかもしれない。

EC2-Otherの正体を解明する

最初に問題だった「EC2-Other」について。これはデータ転送費、EBS、NAT Gateway、Elastic IPなどが含まれる複合カテゴリで、Cost Explorerで「Usage type group」の粒度を変えると内訳が見える。

def analyze_ec2_other_breakdown(
    start_date: str,
    end_date: str
) -> list[dict]:
    """
    EC2-Otherの内訳を分析する
    """
    ce = boto3.client('ce', region_name='us-east-1')
    
    response = ce.get_cost_and_usage(
        TimePeriod={'Start': start_date, 'End': end_date},
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[{'Type': 'DIMENSION', 'Key': 'USAGE_TYPE'}],
        Filter={
            'Dimensions': {
                'Key': 'SERVICE',
                'Values': ['Amazon Elastic Compute Cloud - Compute']
            }
        }
    )
    
    breakdown = []
    for result in response['ResultsByTime']:
        for group in result['Groups']:
            usage_type = group['Keys'][0]
            cost = float(group['Metrics']['UnblendedCost']['Amount'])
            # NatGateway、DataTransfer関連を特定
            category = 'Other'
            if 'NatGateway' in usage_type:
                category = 'NAT Gateway'
            elif 'DataTransfer' in usage_type:
                category = 'Data Transfer'
            elif 'EBS' in usage_type:
                category = 'EBS'
            elif 'ElasticIP' in usage_type:
                category = 'Elastic IP'
            
            breakdown.append({
                'usage_type': usage_type,
                'category': category,
                'cost_usd': cost
            })
    
    return sorted(breakdown, key=lambda x: x['cost_usd'], reverse=True)

うちの場合、EC2-Otherの内訳を見たらNAT Gatewayのデータ処理費が70万円あった。NAT Gatewayを経由するトラフィックがある程度あることは知っていたけど、こんなに高いとは思っていなかった。VPCエンドポイント導入で大幅に削減できた話は月80万円のデータ転送費をVPCエンドポイント導入で削減した実装記録に書いたので参照してほしい。

Cost Anomaly Detectionの設定と実際の効果

2026年のAWS Cost Anomaly Detectionは機械学習モデルが改善されて、誤検知がかなり減った。正直、2024年頃は使い物にならないレベルのノイズがあったけど、今は実用的になってきた印象だ。

graph TB
    subgraph Production_Account["本番アカウント"]
        subgraph VPC_Main["VPC (10.0.0.0/16)"]
            subgraph AZ_1a["AZ: ap-northeast-1a"]
                EC2_1["EC2 Auto Scaling\nGroup"]
                RDS_Primary["RDS Aurora\nPrimary"]
            end
            subgraph AZ_1c["AZ: ap-northeast-1c"]
                EC2_2["EC2 Auto Scaling\nGroup"]
                RDS_Replica["RDS Aurora\nRead Replica"]
            end
            NAT["NAT Gateway"]
            VPCEndpoint["VPC Endpoints\n(S3, DynamoDB, ECR)"]
        end
        Lambda_Notify["Lambda\nCost Notifier"]
        EventBridge["EventBridge\nScheduler"]
    end

    subgraph Cost_Tools["コスト管理サービス"]
        CostExplorer["Cost Explorer"]
        AnomalyDetect["Cost Anomaly\nDetection"]
        Budgets["AWS Budgets"]
        ComputeOpt["Compute Optimizer"]
    end

    subgraph Notification["通知チャネル"]
        SNS["SNS Topic"]
        Slack["Slack\n#aws-cost-alert"]
        Email["Email\n(CFO + Engineering Lead)"]
    end

    CostExplorer --> Lambda_Notify
    AnomalyDetect --> SNS
    Budgets --> SNS
    EventBridge --> Lambda_Notify
    SNS --> Slack
    SNS --> Email
    Lambda_Notify --> Slack
    EC2_1 & EC2_2 --> NAT
    EC2_1 & EC2_2 --> VPCEndpoint
    ComputeOpt -.->|推奨| EC2_1
    ComputeOpt -.->|推奨| EC2_2

AnomalyDetectionの設定はCDKで管理している。

# CDKでCost Anomaly Detectionを設定
from aws_cdk import (
    Stack,
    aws_ce as ce,
    aws_sns as sns,
    aws_chatbot as chatbot,
    CfnOutput
)
from constructs import Construct

class CostAnomalyStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)
        
        # Slack通知用SNSトピック
        alert_topic = sns.Topic(
            self, 'CostAnomalyAlerts',
            topic_name='cost-anomaly-alerts',
            display_name='AWS Cost Anomaly Detection'
        )
        
        # サービス別モニター(上位5サービスを個別監視)
        services_to_monitor = [
            'Amazon Elastic Compute Cloud - Compute',
            'Amazon Relational Database Service',
            'AWS Lambda',
            'Amazon Simple Storage Service',
            'Amazon Elastic Kubernetes Service'
        ]
        
        for i, service in enumerate(services_to_monitor):
            monitor = ce.CfnAnomalyMonitor(
                self, f'Monitor{i}',
                monitor_name=f'service-monitor-{i}',
                monitor_type='DIMENSIONAL',
                monitor_dimension='SERVICE'
            )
            
            # 閾値: 絶対額$500以上 OR 前日比20%以上
            ce.CfnAnomalySubscription(
                self, f'Subscription{i}',
                subscription_name=f'cost-anomaly-sub-{i}',
                monitor_arn_list=[monitor.attr_monitor_arn],
                subscribers=[
                    ce.CfnAnomalySubscription.SubscriberProperty(
                        address=alert_topic.topic_arn,
                        type='SNS'
                    )
                ],
                threshold_expression=json.dumps({
                    'Or': [
                        {
                            'Dimensions': {
                                'Key': 'ANOMALY_TOTAL_IMPACT_ABSOLUTE',
                                'MatchOptions': ['GREATER_THAN_OR_EQUAL'],
                                'Values': ['500']  # $500以上
                            }
                        },
                        {
                            'Dimensions': {
                                'Key': 'ANOMALY_TOTAL_IMPACT_PERCENTAGE',
                                'MatchOptions': ['GREATER_THAN_OR_EQUAL'],
                                'Values': ['20']  # 20%以上
                            }
                        }
                    ]
                }),
                frequency='IMMEDIATE'
            )

この設定で導入後最初の1ヶ月に7件のアラートが飛んできた。そのうち真のコスト異常は3件で、残り4件はデプロイによる想定内のスパイクだった。IMMEDIATEじゃなくDAILYに変えることも検討したが、即時検知のほうが初動が早いので今はIMMEDIATEのままにしている。

3ヶ月で実現したコスト削減の実績と次のアクション

3ヶ月で実施した施策と削減額をまとめるとこうなった。

施策削減前(月額)削減後(月額)削減額実施期間
タグ整備 + コスト配賦可視化のみ1ヶ月目
VPCエンドポイント導入¥700,000¥210,000¥490,0001〜2ヶ月目
EC2右サイズ化(Compute Optimizer活用)¥1,200,000¥780,000¥420,0002ヶ月目
Savings Plans購入(EC2・Lambda)¥1,500,000¥990,000¥510,0002〜3ヶ月目
未使用EBS・Elastic IP削除¥180,000¥30,000¥150,0003ヶ月目
合計¥5,000,000¥3,490,000¥1,510,0003ヶ月

削減額として一番デカかったのはSavings Plansで、次にVPCエンドポイントだった。未使用EBS削除は金額は小さいけど、「誰も使っていないリソースにお金を払い続けていた」という事実がチームに刺さったので心理的な効果は大きかった。

正直まだ最適化の余地はある。RDS Aurora Serverless v2への移行や、EKSのSpot Instancesカバレッジ向上など、着手できていない項目がいくつかある。EKSコスト最適化についてはEKS コスト最適化2026が参考になるはずだ。

xychart-beta
    title "3ヶ月のコスト削減推移(月額・万円)"
    x-axis ["Before", "Month1", "Month2", "Month3"]
    y-axis "コスト(万円)" 0 --> 600
    bar [500, 480, 410, 349]
    line [500, 480, 410, 349]

継続運用のための週次レビュー体制

毎週月曜の朝会(30分)でFinOpsレビューを実施している。アジェンダはシンプルで、①先週比コスト増減のサービス確認、②Cost Anomaly Detectionのアラート振り返り、③Savings Plans / RIカバレッジの確認、④翌週の施策確認、の4つだけだ。

最初は「週次は多すぎない?」という声もあったけど、コストに対する意識がチームに根付くまでは週次がちょうどいい。意識が定着したら隔週か月次に落としてもいいと思う。

まとめ

3ヶ月のFinOps実践で学んだことを整理する。

要点まとめ:

  1. 「Inform」フェーズのタグ整備が最も地味で最も重要 — タグがないとコスト配賦できず、何から手をつけるか分からない。Tag Policies + SCP強制が2026年の現実解

  2. Cost Explorerは「週次で同じビューを見続ける」ことが大事 — 毎週同じ粒度で見ることで異常検知の感度が上がる。APIで自動化して定例に組み込むのが継続の鍵

  3. EC2-Otherの内訳分析は必ず実施 — NAT Gateway、データ転送費が意外と大きい。VPCエンドポイントで即改善できる項目が多い

  4. Cost Anomaly Detectionは2026年時点でかなり使える — 閾値の設定(絶対額 + 相対%のOR条件)が重要。最初は敏感すぎる設定から始めて調整

  5. FinOpsは組織の問題 > 技術の問題 — ツールを整えても週次レビューの文化がないと形骸化する。CFOやPMを巻き込む定例設計が長期継続のカギ

次のアクション:

  • Tag Policiesを今週中に1つ設定してみる
  • Cost ExplorerのAPIを使ったSlack通知を実装する
  • Cost Anomaly Detectionのモニターを3サービス分追加する

みなさんのチームはFinOpsの仕組みどれくらい整備できてますか?「EC2-Otherが謎のまま放置されてる」という状況、あるある過ぎると思うので、まずはそこの内訳分析から始めるのをおすすめする。

U

Untanbaby

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

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

関連記事