1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenSearch Serverlessで$90溶かしたので、AWSコストを毎日通知する仕組みを作った

Last updated at Posted at 2026-01-14

きっかけ

別件のお試しで初めて Amazon OpenSearch に触れて、複数のPowerPointの内容を検索するシステムを構築しようとしていました。その過程でコストを抑えたくてCopilotに相談したところ、OpenSearch Serverlessの利用を提案されました。

👇こんな風に料金試算もしました。
image.png

だけど、実際には OpenSearch Serverless は最低限の検索・インデックス容量(OCU)が常時確保され、常時課金で、PoCとしては逆にコストが非常にかかる結果となりました。

これに気が付いたのが OpenSearch Serverless の collection を作ってから6日後のこと。
時すでに遅しで、$90もの料金が発生してしまっていました。。。
image.png

こんなことがまた起きないようにAWSのコストを毎日Teamsに通知する仕組みを作ることにしました。

アーキテクチャ

以下のようなアーキテクチャでTeamsに通知します。

1.AWS Cost Explorer APIで毎日の利用料金を取得

・リージョン別の料金を取得(※)するため、GetCostAndUsage APIを利用。
・集計粒度は「日次」、グループ化は「リージョン」。

※このAWSは私が管理している部内のAWSアカウントであり、
 利用者はリージョンごとに区分けされているため、リージョン別の料金を取得します。

2.AWS Lambdaでスケジュール実行

・Amazon EventBridgeで毎日(例:午前9時)トリガー。
・LambdaがCost Explorer APIを呼び出し、結果を整形。

3.メール通知

・Amazon SNS(Simple Notification Service)を使って、メールで通知することにしました。
 →当初は Teams Webhook を想定していましたが、個人向け通知で十分だったため、SNSのメール通知に変更しました。

SNSトピック作成

メール通知するためのSNSトピックをまず作成します。

  1. AWS マネジメントコンソール → Amazon SNS を開く
  2. 左ペイン「トピック」 → 「トピックの作成」
  3. タイプ:スタンダード → 「トピックの作成」

SNS サブスクリプション作成

次に送信手段であるサブスクリプションを作成します。

  1. 左ペイン「サブスクリプション」 → 「サブスクリプションの作成」
  2. 「トピック ARN」で先ほど作成したトピックを選択
  3. 「プロトコル」に「E メール」を選択
  4. 「エンドポイント」に送付先のメールアドレスを入力 → 「サブスクリプションの作成」

エンドポイントで指定したメールアドレスに「AWS Notification - Subscription Confirmation」のタイトルでメールが届きます。
メール内のリンクの「Confirm subscription」をクリックします。

自動で配信停止される場合があります!

会社等でメールサーバにスパムフィルターがある場合、「Confirm subscription」をクリックしても、自動配信停止が走ります!
その場合は、サブスクリプションを作成後、AWS CLIから以下のように subscribe してください。

aws sns confirm-subscription --region ap-northeast-1 --topic-arn arn:aws:sns:ap-northeast-1:YOUR_ACCOUNT_ID:YOUR_TOPIC_NAME --token (メールに記載のトークン) --authenticate-on-unsubscribe true

Lambda実行用 IAM ロール作成

CostExploereは有効化されていたので、そのステップは省略してまずLambdaを実行するIAMロールを作成します。

  1. AWS マネジメントコンソール → IAM を開く
  2. 左ペイン 「ロール」 → 「ロールを作成」
  3. 「信頼されたエンティティの種類」:AWS のサービス
  4. ユースケースの選択:Lambda を選択して 「次へ」
  5. アクセス権限の追加:ここでは一旦スキップ(後でカスタムポリシーを付与します)
    もしくは CloudWatchLogs 標準ポリシー(AWSLambdaBasicExecutionRole)だけ先に付けてもOK
  6. ロール名:lambda-cost-report-role(任意)
    タグ(任意)を設定 → 「ロールを作成」

更に、インラインポリシーで CostExploere の読み取り権限と SNSの権限を付与します。

  1. ロール詳細画面 → 「アクセス権限」タブ → 「インラインポリシーを追加」
  2. JSON タブを選択し、下記を貼り付け → 「保存」(ポリシー名例:CostExplorerAndSNSPolicy)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CostExplorerReadDaily",
      "Effect": "Allow",
      "Action": [
        "ce:GetCostAndUsage"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PublishToSpecificSNSTopic",
      "Effect": "Allow",
      "Action": [
        "sns:Publish"
      ],
      "Resource": "arn:aws:sns:ap-northeast-1:YOUR_ACCOUNT_ID:YOUR_TOPIC_NAME"
    }
  ]
}

※ Cost Explorer はリソースレベル制御ができないため "Resource": "*" になります

EventBridgeからLambda関数を実行する IAMロール作成

EventBridge の「スケジュールされたルール(レガシー)」から Lambda を実行する場合、
Lambda 側のリソースポリシーだけでは実行できないケースがあります。

これは、EventBridge が Lambda を呼び出す際に
「どの権限で実行するか」を示す IAM ロールが必要になるためです。

そのため、EventBridge 専用の実行ロールを作成し、
スケジュール設定時に指定します。

  1. IAM コンソール → ロール → ロールを作成
  2. 「信頼されたエンティティの種類」:カスタム信頼ポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

3.「許可ポリシー」:AWSLambdaRole → 「次へ」
4. 「ロール名」:EventBridgeLambdaInvokeRole

Lambda関数作成

次に CostExploere API を呼び出して、SNSでメール通知する Lambda関数を作成します。

ランタイムは Python 3.14、実行ロールは先ほど作成したIAMロールを指定します。

import boto3
import datetime
import os
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

TOPIC_ARN = os.environ.get("TOPIC_ARN", "arn:aws:sns:ap-northeast-1:YOUR_ACCOUNT_ID:YOUR_TOPIC_NAME")

def lambda_handler(event, context):
    # JSTで前日を求め、Cost Explorerに渡す期間はUTC日付(Endは翌日:排他的)
    jst = datetime.timezone(datetime.timedelta(hours=9))
    today_jst = datetime.datetime.now(jst).date()
    yesterday_jst = today_jst - datetime.timedelta(days=1)

    start = yesterday_jst.strftime('%Y-%m-%d')   # 例: 2026-01-13
    end   = today_jst.strftime('%Y-%m-%d')       # 例: 2026-01-14(排他的)

    logger.info(f"Querying Cost Explorer from {start} to {end}")

    # Cost Explorer は us-east-1
    ce = boto3.client('ce', region_name='us-east-1')

    try:
        resp = ce.get_cost_and_usage(
            TimePeriod={'Start': start, 'End': end},
            Granularity='DAILY',
            Metrics=['UnblendedCost'],
            GroupBy=[{'Type': 'DIMENSION', 'Key': 'REGION'}]
        )
    except Exception as e:
        logger.exception(f"Cost Explorer API failed: {e}")
        raise

    results = resp.get('ResultsByTime', [])
    if not results:
        logger.warning("No ResultsByTime returned")
        cost_data = []
        total = 0.0
    else:
        groups = results[0].get('Groups', [])
        cost_data = []
        total = 0.0
        for g in groups:
            region = g['Keys'][0]
            amount = float(g['Metrics']['UnblendedCost']['Amount'])
            cost_data.append((region, amount))
            total += amount

    # メッセージ整形
    lines = [f"AWSコストレポート({yesterday_jst.isoformat()}"]
    if cost_data:
        for region, amt in sorted(cost_data, key=lambda x: x[0]):
            lines.append(f"- {region}: ${amt:.2f}")
        lines.append(f"合計: ${total:.2f}")
    else:
        lines.append("データがありません(前日データが未確定の可能性)")

    message = "\n".join(lines)
    logger.info("Message to publish:\n" + message)

    # SNS クライアントはトピックのリージョンに合わせる
    sns = boto3.client('sns', region_name='ap-northeast-1')
    try:
        sns.publish(
            TopicArn=TOPIC_ARN,
            Message=message,
            Subject='AWS Daily Cost Report'
        )
    except Exception as e:
        logger.exception(f"SNS publish failed: {e}")
        raise

    return {"status": "ok", "published_to": TOPIC_ARN, "total": round(total, 2)}

環境変数にSNSのARNを設定します。

TOPIC_ARN=arn:aws:sns:ap-northeast-1:YOUR_ACCOUNT_ID:YOUR_TOPIC_NAME

イベントスケジュール作成

最後に、毎朝9時にメール通知されるように EventBridge でスケジュールを作成します。

  1. AWS マネジメントコンソール → EventBridge を開く

  2. 左ペイン 「ルール」 → 「ルールを作成」の右側の矢印をクリックして、「スケジュールされたルールを作成」をクリック
    image.png

  3. 「名前」:daily-cost-report-9am → 「次へ」

  4. 「スケジュールパターン」:「特定の時刻 (毎月第 1 月曜日の午前 8 時 (PST) など) に実行されるきめ細かいスケジュール。」

  5. 「Cron 式」:cron(0 0 * * ? *) → 「次へ」

    ※ EventBridge の Cron は UTC 基準です。
    cron(0 0 * * ? *) は JST 9:00 に相当します。

  6. 「ターゲット 1」「ターゲットタイプ」:「AWS のサービス」「Lambda 関数」

  7. 「関数」:先ほど作成したLambda関数

  8. 「許可」:実行ロールを使用 (推奨)にチェックを入れる

  9. 「実行ロール」:「既存のロールを使用」 → EventBridgeLambdaInvokeRole → 「次へ」

  10. タグ(任意)を設定 → 「ロールを作成」

メール本文

実際に動かしてみると、以下のようにコストレポートが届きます。

AWSコストレポート(2026-01-13)
- NoRegion: $-0.00
- ap-northeast-1: $9.32
- ap-northeast-2: $0.00
- ap-south-1: $0.00
- ap-southeast-1: $0.03
- ap-southeast-2: $1.99
- eu-west-1: $0.00
- eu-west-3: $0.00
- us-east-1: $0.05
- us-west-2: $0.01
合計: $11.40

最後に

OpenSearch Serverless は「Serverless」という名前から
使った分だけ課金される=PoC向き だと思い込んでいましたが、
実際には最低限の OCU が常時確保され、利用が少なくても課金され続けます。

今回のように
「とりあえず触ってみる」「数日放置する」
といった使い方をすると、想定以上のコストが発生する可能性があります。

その反省から、
「毎日コストを自分の目で確認できる仕組み」 を作ることにしました。
仕組み自体はシンプルですが、
・Cost Explorer は us-east-1 固定
・SNS サブスクリプションの自動配信停止
など、実際にやってみないと分からない落とし穴も多かったです。

同じように PoC や検証用途で AWS を使っている方は、
CloudWatch アラームや予算アラートに加えて、
日次でのコスト通知を一度自作してみる
のも良い勉強になると思います。

この記事が、
「Serverless=安いと思って油断した結果、請求額を見て青ざめる」
人を一人でも減らせたら幸いです。

追記(2026/01/15)

EventBridge のスケジュールされたルールから Lambdaを呼び出すIAMロールが必要でしたので、手順を追加しました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?