AWS夜中のコストアラートで飛び起きた話|Budgets・Cost Anomaly Detection実運用3ヶ月の知見

夜中2時にEC2コストが前日比3倍になって飛び起きた経験、ありませんか?AWS BudgetsとCost Anomaly Detectionを本腰入れて設定し直した3ヶ月で見えた「本当に効いた設定」と「やらかしたミス」を正直に書きます。

先日、夜中の2時にSlackから「AWS請求アラート」の通知が飛んできて、飛び起きた経験がある。見てみたら、前日比でEC2コストが3倍に膨らんでいた。原因はバッチ処理のInfinite Loopだったんだけど、それより恐ろしかったのは「1日気づくのが遅れていたら追加で100万円近く飛んでいた」という事実だ。

その経験をきっかけに、うちのチームでAWS BudgetsとCost Anomaly Detectionを本腰を入れて設定し直した。それから約3ヶ月、実運用で見えてきた「本当に効いた設定」と「やらかしたミス」を正直に書いておこうと思う。

AWSのコスト管理は設定しっぱなしになりがちな分野だし、皆さんのチームでも似たような悩みがあれば参考にしてほしい。なお、Reserved InstancesやSavings Plansの最適化についてはすでにSavings Plans vs Reserved Instances|2026年AWSコスト最適化判断基準で書いているので、この記事ではリアルタイムの異常検知側にフォーカスする。

うちのAWS環境とコスト監視の全体像

まず前提を共有しておく。うちは本番・ステージング・開発の3環境を別アカウントで管理していて、AWS Organizationsで1つの管理アカウントに統合している構成だ。月の請求は大体400〜600万円の間で、ピーク時はEC2とデータ転送費が主なコストドライバーになっている。

以下がコスト監視の全体アーキテクチャだ。

graph TB
    subgraph ManagementAccount["管理アカウント (Organizations)"]
        BillingConsole["AWS Billing Console"]
        CostExplorer["Cost Explorer"]
        Budgets["AWS Budgets"]
        CAD["Cost Anomaly Detection"]
    end

    subgraph ProdAccount["本番アカウント"]
        EC2Prod["EC2 / ECS"]
        RDSProd["RDS Aurora"]
        S3Prod["S3"]
    end

    subgraph StgAccount["ステージングアカウント"]
        EC2Stg["EC2 / ECS"]
        S3Stg["S3"]
    end

    subgraph DevAccount["開発アカウント"]
        EC2Dev["EC2"]
        Lambda["Lambda"]
    end

    subgraph AlertFlow["アラートフロー"]
        SNS["SNS Topic"]
        Lambda2["Lambda (フィルタリング)"]
        Slack["Slack #aws-cost-alert"]
        PagerDuty["PagerDuty (重大アラート)"]
        Email["Email (週次レポート)"]
    end

    ProdAccount --> ManagementAccount
    StgAccount --> ManagementAccount
    DevAccount --> ManagementAccount

    Budgets --> SNS
    CAD --> SNS
    SNS --> Lambda2
    Lambda2 --> Slack
    Lambda2 --> PagerDuty
    Lambda2 --> Email

ポイントは「管理アカウントからの一元監視」と「アラートの重み付け」だ。全部のアラートをSlackに流すと通知疲れで誰も見なくなる——これはGuardDuty導入3ヶ月の地獄と脱出戦略でも同じ失敗をやらかした経験があって、今回はそこを最初から設計に織り込んだ。

AWS Budgets設定の「正解と失敗」

最初に失敗した設定パターン

最初のころ、こんな設定をしていた。

項目内容
月間予算600万円
アラート閾値80% / 100% の2段階
対象アカウント全体

これ、まったく役に立たなかった。月半ばの15日時点で請求が50%を超えたとしても、「順調に消費してるね」なのか「このまま行くと予算オーバーするね」なのかが分からない。月末まで待たないと本当の問題に気づけない設計だったわけだ。

2026年現在のBudgetsには「Forecasted Amount」ベースのアラートが使えるので、これを絶対に使うべきだ。正直、この機能に気づいてから世界が変わったと思っている。

2026年版の推奨設定

実際に今動かしている設定をCDKで書いた実装例を示す。

import * as budgets from 'aws-cdk-lib/aws-budgets';

// Budget設定: 月次予算の予測ベースアラート
const monthlyBudget = new budgets.CfnBudget(this, 'MonthlyBudget', {
  budget: {
    budgetName: 'prod-monthly-total',
    budgetType: 'COST',
    timeUnit: 'MONTHLY',
    budgetLimit: {
      amount: 5000000, // 500万円
      unit: 'JPY',
    },
    costFilters: {
      LinkedAccount: ['123456789012'], // 本番アカウント
    },
  },
  notificationsWithSubscribers: [
    {
      // 実際の使用量が70%を超えたとき
      notification: {
        notificationType: 'ACTUAL',
        comparisonOperator: 'GREATER_THAN',
        threshold: 70,
        thresholdType: 'PERCENTAGE',
      },
      subscribers: [
        { subscriptionType: 'SNS', address: snsTopicArn },
      ],
    },
    {
      // 予測ベースで100%超えそうなとき (これが重要)
      notification: {
        notificationType: 'FORECASTED',
        comparisonOperator: 'GREATER_THAN',
        threshold: 100,
        thresholdType: 'PERCENTAGE',
      },
      subscribers: [
        { subscriptionType: 'SNS', address: snsTopicArn },
        { subscriptionType: 'EMAIL', address: 'engineering-team@example.com' },
      ],
    },
  ],
});

サービス別の予算も設定しておくとさらに細かく追える。EC2が暴走したときに「EC2だけ」でアラートを拾えるのが地味に便利だった。

// EC2専用予算
const ec2Budget = new budgets.CfnBudget(this, 'EC2Budget', {
  budget: {
    budgetName: 'prod-ec2-monthly',
    budgetType: 'COST',
    timeUnit: 'MONTHLY',
    budgetLimit: {
      amount: 2000000, // EC2だけで200万円が上限
      unit: 'JPY',
    },
    costFilters: {
      LinkedAccount: ['123456789012'],
      Service: ['Amazon Elastic Compute Cloud - Compute'],
    },
  },
  notificationsWithSubscribers: [
    {
      notification: {
        notificationType: 'ACTUAL',
        comparisonOperator: 'GREATER_THAN',
        threshold: 90,
        thresholdType: 'PERCENTAGE',
      },
      subscribers: [
        { subscriptionType: 'SNS', address: snsTopicArn },
      ],
    },
  ],
});

設定しているBudgetsの一覧とそれぞれの役割はこの通りだ。

Budget名対象月次上限アラート閾値通知先
prod-monthly-total本番全体500万円70%(実績), 100%(予測)SNS + Email
prod-ec2-monthlyEC2のみ200万円90%(実績)SNS
prod-rds-monthlyRDSのみ100万円90%(実績)SNS
prod-data-transferデータ転送80万円80%(実績), 100%(予測)SNS + PagerDuty
stg-monthly-totalステージング全体80万円100%(実績)Email
dev-monthly-total開発全体30万円100%(実績)Email

データ転送費だけPagerDutyに繋いでいるのは、トラフィック系の異常はビジネスインシデントに直結しやすいから。このあたりの優先度設定はインシデント対応の最新ベストプラクティス2026での経験が活きている。

Cost Anomaly Detection、本当に使えるのか

Budgetsだけだと「月次の上限管理」しかできない。日次や時間単位で急激にコストが跳ね上がった場合に気づけないんですよね。そこで活躍するのがCost Anomaly Detectionだ。

最初に正直に言うと、導入当初は「機械学習で自動検知してくれる」という触れ込みに期待しすぎた。最初の2週間は誤検知が多くて「これ使えないじゃないか」と思ったのが本音だ。ただ3週間を過ぎたあたりから急激に精度が上がった。モデルの学習期間が必要という話は本当だった。

Monitor設定のコツ

import boto3

ce_client = boto3.client('ce', region_name='us-east-1')

# サービスごとのモニター設定
response = ce_client.create_anomaly_monitor(
    AnomalyMonitor={
        'MonitorName': 'ServiceMonitor-Production',
        'MonitorType': 'DIMENSIONAL',
        'MonitorDimension': 'SERVICE',
    }
)
monitor_arn = response['MonitorArn']
print(f"Monitor ARN: {monitor_arn}")

# サブスクリプション設定
# 重要: ThresholdExpressionで絶対値と相対値の両方を設定する
subscription_response = ce_client.create_anomaly_subscription(
    AnomalySubscription={
        'MonitorArnList': [monitor_arn],
        'Subscribers': [
            {
                'Address': 'arn:aws:sns:ap-northeast-1:123456789012:cost-alert',
                'Type': 'SNS',
            },
        ],
        'ThresholdExpression': {
            'And': [
                {
                    'Dimension': {
                        'Key': 'ANOMALY_TOTAL_IMPACT_ABSOLUTE',
                        'MatchOptions': ['GREATER_THAN_OR_EQUAL'],
                        'Values': ['50000'],  # 5万円以上の絶対値変化
                    }
                },
                {
                    'Dimension': {
                        'Key': 'ANOMALY_TOTAL_IMPACT_PERCENTAGE',
                        'MatchOptions': ['GREATER_THAN_OR_EQUAL'],
                        'Values': ['30'],  # かつ30%以上の相対変化
                    }
                },
            ]
        },
        'Frequency': 'IMMEDIATE',  # 検知即時通知
        'SubscriptionName': 'production-immediate-alert',
    }
)

print(f"Subscription created: {subscription_response['SubscriptionArn']}")

ここで重要なのは ThresholdExpression の AND 条件だ。絶対値だけにすると「100円が150円になった」でもアラートが飛ぶ。相対値だけにすると「200万円が300万円になった」のに引っかからないことがある。両方の条件をAndで繋ぐのがベストプラクティスだと痛感した——やらかしてから気づくのが悔しいんですよね、これ。

実際の検知性能

3ヶ月間の検知数と精度を記録していた。

xychart-beta
    title "Cost Anomaly Detection 月別アラート精度"
    x-axis ["1ヶ月目", "2ヶ月目", "3ヶ月目"]
    y-axis "件数" 0 --> 50
    bar [38, 18, 12]
    line [5, 10, 11]

棒グラフが総アラート数、折れ線が実際の異常だったもの。1ヶ月目はアラート38件中、実際に対処が必要だったのは5件だけ。誤検知率が高すぎて正直しんどかった。ただ2ヶ月目以降は急速に改善して、3ヶ月目は12件のアラートのうち11件が本当の異常だった。これは驚いた。学習期間さえ乗り越えれば、かなり信頼できるツールだと思っている。

Lambda関数でアラートを「賢く」ルーティングする

SNS → Lambdaの間に挟むフィルタリングLambdaが地味に効いた。全部のアラートを同じチャンネルに流していた時期は、深夜に軽微なアラートで起こされることが何度もあって精神的にきつかった。

import json
import urllib.request
import os

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
PAGERDUTY_KEY = os.environ['PAGERDUTY_INTEGRATION_KEY']

def lambda_handler(event, context):
    for record in event['Records']:
        message = json.loads(record['Sns']['Message'])
        
        # Cost Anomaly Detectionからのアラート
        if 'anomalyDetails' in message:
            anomaly = message['anomalyDetails']
            impact = float(anomaly.get('totalImpact', {}).get('totalActualSpend', 0))
            service = anomaly.get('rootCauses', [{}])[0].get('service', 'Unknown')
            
            # 100万円以上はPagerDutyに飛ばす
            if impact >= 1000000:
                send_pagerduty_alert(service, impact)
                send_slack_alert(service, impact, severity='critical')
            # 10万円以上はSlackのみ
            elif impact >= 100000:
                send_slack_alert(service, impact, severity='warning')
            # それ以下はSlackに流すだけ (夜中は起こさない)
            else:
                send_slack_alert(service, impact, severity='info')
        
        # Budgetsからのアラート
        elif 'AlarmDescription' in message or 'budgetName' in message:
            budget_name = message.get('budgetName', 'Unknown')
            threshold = message.get('thresholdExceeded', 0)
            
            if threshold >= 100:
                send_pagerduty_alert(budget_name, threshold, alert_type='budget')
            send_slack_alert(budget_name, threshold, severity='warning', alert_type='budget')

def send_slack_alert(service, value, severity='warning', alert_type='anomaly'):
    color_map = {'critical': '#FF0000', 'warning': '#FFA500', 'info': '#36a64f'}
    
    if alert_type == 'anomaly':
        title = f'コスト異常検知: {service}'
        text = f'異常なコストスパイクが検出されました。影響額: ¥{value:,.0f}'
    else:
        title = f'予算アラート: {service}'
        text = f'予算閾値に到達しました: {value}%'
    
    payload = {
        'attachments': [{
            'color': color_map[severity],
            'title': title,
            'text': text,
            'footer': 'AWS Cost Monitoring',
        }]
    }
    
    data = json.dumps(payload).encode('utf-8')
    req = urllib.request.Request(
        SLACK_WEBHOOK_URL,
        data=data,
        headers={'Content-Type': 'application/json'}
    )
    urllib.request.urlopen(req)

def send_pagerduty_alert(service, value, alert_type='anomaly'):
    # PagerDuty Events API v2
    payload = {
        'routing_key': PAGERDUTY_KEY,
        'event_action': 'trigger',
        'payload': {
            'summary': f'AWS Cost Anomaly: {service} - ¥{value:,.0f}',
            'severity': 'critical',
            'source': 'aws-cost-monitoring',
        }
    }
    
    data = json.dumps(payload).encode('utf-8')
    req = urllib.request.Request(
        'https://events.pagerduty.com/v2/enqueue',
        data=data,
        headers={'Content-Type': 'application/json'}
    )
    urllib.request.urlopen(req)

これで「夜中2時に1000円の異常で起こされる」地獄から解放された。閾値は自チームの状況によって調整が必要だけど、「絶対金額でエスカレーションを分ける」という設計思想は汎用性があると思う。

Cost Anomaly Detectionが実際に検知したケース

3ヶ月間で本当に助かったケースを3つ紹介しておく。どれも「Budgetsだけだったら気づくのが遅れていた」ケースだ。

ケース1: EC2のInfinite Loop(夜中のやつ)

冒頭に書いたやつだ。バッチ処理のバグでEC2が何台も自動起動し続けていた。Cost Anomaly Detectionが起動30分後にアラートを出してくれたので被害を最小限に抑えられた。Budgetsだけだったら翌日の朝まで気づかなかった可能性が高い。

ケース2: 開発アカウントでのGPUインスタンス放置

チームのメンバーが検証用に立てたp3.8xlargeを止め忘れていた。3日間で約42万円のコスト。Cost Anomaly Detectionが「通常の開発アカウントコストの800%増」として検知してくれた。このケースはBudgetsの設定値(開発は30万円上限)も引っかかっていたんだけど、Cost Anomaly Detectionの方が半日早く気づいてくれた。個人的には「Budgetsは月次の防衛ライン、Cost Anomaly Detectionは日次の早期警戒」というイメージで使い分けている。

ケース3: NAT Gatewayのデータ転送費突然増加

これが一番謎だったケース。アプリケーションのコードは変更していないのにNAT Gatewayのコストが前週比3倍になっていた。調査したらS3のVPCエンドポイントの設定が一部のリソースで効いていないことが判明。月間で80万円以上の無駄なコストが発生していたことになる。月80万円のデータ転送費をVPCエンドポイント導入で削減した実装記録でも触れた問題に近い構造だった。

月次コスト推移の改善効果

監視体制を整えてから3ヶ月で、コストの推移はこんな感じだ。

xychart-beta
    title "AWS月次コスト推移(万円)"
    x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
    y-axis "コスト(万円)" 0 --> 600
    line [580, 610, 520, 490, 470, 460]

3月に監視体制を整えてから右肩下がりで改善している。4月以降はアラートのおかげで無駄なリソースへの早期対応ができた結果だ。正直まだ最適化の余地はあると思っているので、継続して追いかけていく予定。

運用3ヶ月でわかったTipsと注意点

Cost Anomaly Detectionの学習期間問題

モデルの学習には最低2〜3週間かかる。「導入したのにアラートが来ない!」という状態はほぼこれ。逆に言うと、本番投入して最初の1〜2週間は誤検知が多くても諦めないこと。うちはこの期間に誤検知でのPagerDutyアラートが何度か飛んで、チームのモチベーションが下がりかけた。最初から「2週間は慣らし期間」と宣言しておいた方がいい。

タグ戦略との連携

Cost Anomaly Detectionのモニターには「タグベース」もある。うちはサービスタグ(service: payment-apiservice: batch-processorなど)を全リソースにつけていて、それを使ったモニターが一番有効だった。タグが揺れているとモニターも機能しなくなるので、まずタグポリシーを整備することを強くお勧めする。これはAWS Organizationsのタグポリシー機能で強制できる。

BudgetsのActionsを使った自動制御

2026年時点でBudgets Actionsを使うと、予算超過時に自動でIAMポリシーを適用したりEC2インスタンスをStopしたりできる。ただしこれ、設定をミスると本番が止まるリスクがあるので「開発・ステージング環境専用」で使うのが現実的だと思う。うちは開発アカウントで「月30万円を超えたら新規EC2起動を禁止するSCPをアタッチする」設定を入れている。

flowchart LR
    A["Budgets Actionトリガー"] --> B{"アカウント種別"}
    B --> |"本番"| C["SNS通知のみ\n自動制御なし"]
    B --> |"ステージング"| D["SNS通知\n+ Slack DM"]
    B --> |"開発"| E["SNS通知\n+ EC2新規起動禁止SCP"]
    E --> F["翌朝に自動解除"]

RI/Savings Plansのコストへの影響

地味に混乱したのが、RIやSavings Plansで割引が効いているインスタンスのコスト表示だ。Cost Anomaly DetectionはデフォルトではNet Amortized Costベースで計算するが、設定によってはOnDemandコストと混在して見える。BudgetsとCost Anomaly Detectionで見ているコストの「単価」が異なることを理解しておかないとアラートの意味を誤解する。このあたりの詳細はRIカバレッジ分析、3年間ずっと間違えてた話と2026年の正解が参考になる。

まとめ

3ヶ月の実運用でわかったことを整理するとこうなる。

ポイント内容
BudgetsはFORECASTEDアラートが肝実績70% + 予測100%の組み合わせで設定する
学習期間を見込む最初の2〜3週間の誤検知で諦めないことが大事
フィルタリング層は必須金額ベースでPagerDuty / Slack / メールとルーティングを分ける
AND条件でアラート精度を上げる絶対値・相対値どちらか一方だと誤検知か見逃しになる
タグ戦略を先に整備するタグが揺れているとサービス別モニターが機能しない

次のアクションとしては、まずOrganizationsの管理アカウントでBudgetsを1個作ってFORECASTEDアラートを試してみてほしい。設定自体は30分もかからないし、今日から始められる。Cost Anomaly Detectionのモニター設定も同時に入れておけば、3週間後には「異常検知してくれた!」という体験ができるはずだ。

チームのAWS費用が気になっているエンジニアに刺さればうれしい。何か疑問や別の設定パターンがあれば、ぜひ教えてください。

U

Untanbaby

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

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

関連記事