🤯

AWSの月初からの請求額を毎日Slackに通知する仕組み

2025/03/07に公開

はじめに

社内で色々やってる梶原です。
受託開発をやっている人や社外のプロジェクトに参加している人が、検証・テストのためにAWSを使いたいニーズがあるので、シスマックには会社としてのAWS環境があります。
で、自分がAWS環境の管理者をやっているんですが、コストの管理もしっかりする必要があります。
予算を設定したり、事前に設定した一定の金額を超えるとアラートのメールが飛ぶようにしているんですが、急激にコストを発生させている場合はすぐに対応しないといけないので結構心臓に悪いです。
もう少しリアルタイムに現在の請求額を知れたら良いのですが、毎日コストの画面を見に行くのは面倒くさいのでSlackに通知する仕組みを作ろうと思って作りました。

手順

大まかな流れは以下です。
基本的にコンソールで設定をいじります。

  1. Slack Incoming Webhookを設定
  2. AWS Lambdaで関数を作成
    • 環境変数設定
    • レイヤーの設定
    • IAM権限付与
  3. CloudWatch Eventsでスケジュール設定

Slackの設定

チャンネルの作成

まずは通知を送る先のチャンネルを作成します。今回記事を作成するにあたり新規で#aws-dailycostsを作成しました。

Slackアプリの追加

チャンネルの設定画面から→「インテグレーション」→「アプリを追加する」の順で選択します。

アプリの追加画面でIncoming Webhookを検索し、「表示」→「設定」ボタンの順で選択します。

ブラウザに遷移するので「Slackに追加」→チャンネルの選択→「Incoming Webhookインテグレーションの追加」の順で操作をします。

表示されたWebhook URLは、後で作成するLambdaの関数で使用するので控えておきます。

AWS Lambdaで関数を作成

ここからはAWSのコンソールで設定していきます。Lambdaのページにアクセスします。

関数の作成」をします

一から作成」し、以下の設定をします

  • 関数名:なんでもOK
  • ランタイム:Python
  • ロール:新しいロールを作成
  • ロール名:なんでもOK

Lambda関数の作成

関数の設定画面に遷移するので、以下のコードを記入します。
AI(Claude)に作成してもらいました。
雑にお願いしたのですが、サービスごとにまとめてくれたりトップ5を大きい順にソートしてくれていたりとよしなに作ってくれています。プログラマー大失業時代はすぐそこ。

import boto3
import json
import os
import datetime
import requests
from dateutil.relativedelta import relativedelta

def lambda_handler(event, context):
    # 現在の月の初日と今日の日付を取得
    today = datetime.datetime.now()
    start_of_month = today.replace(day=1)
    
    # 日付をフォーマット(AWS Cost Explorerの形式YYYY-MM-DD)
    start_date = start_of_month.strftime('%Y-%m-%d')
    end_date = today.strftime('%Y-%m-%d')
    
    # AWS Cost Explorer clientの初期化
    ce = boto3.client('ce')
    
    # 今月の請求額を取得
    response = ce.get_cost_and_usage(
        TimePeriod={
            'Start': start_date,
            'End': end_date
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {
                'Type': 'DIMENSION',
                'Key': 'SERVICE'
            }
        ]
    )
    
    # トータルコストを計算
    total_cost = 0
    service_costs = []
    
    for group in response['ResultsByTime'][0]['Groups']:
        service_name = group['Keys'][0]
        amount = float(group['Metrics']['UnblendedCost']['Amount'])
        currency = group['Metrics']['UnblendedCost']['Unit']
        
        if amount > 0.1:  # 0.1ドル以上のサービスのみ表示
            service_costs.append({
                'service': service_name,
                'cost': amount,
                'currency': currency
            })
        
        total_cost += amount
    
    # コスト順にソート
    service_costs.sort(key=lambda x: x['cost'], reverse=True)
    
    # Slack通知用のメッセージを作成
    message = f"*AWS 月初({start_date})からの請求額 ({end_date}現在)*\n"
    message += f"*合計: ${total_cost:.2f}*\n\n"
    
    # トップ5サービスのコスト内訳
    message += "*コスト内訳 (トップ5)*:\n"
    for i, service in enumerate(service_costs[:5], 1):
        message += f"{i}. {service['service']}: ${service['cost']:.2f}\n"
    
    # Slack Webhook URLを環境変数から取得
    webhook_url = os.environ['SLACK_WEBHOOK_URL']
    
    # Slackにメッセージを送信
    slack_data = {
        "text": message,
        "username": "AWS Cost Monitor",
        "icon_emoji": ":aws:"
    }
    
    response = requests.post(
        webhook_url,
        data=json.dumps(slack_data),
        headers={'Content-Type': 'application/json'}
    )
    
    return {
        'statusCode': response.status_code,
        'body': 'Notification sent to Slack!'
    }

環境変数の設定

SlackのWebhook URLを変数化しているため、設定タブ→「環境変数」→「編集」の順に操作し、環境変数を追加します。

IAM権限付与

続いてIAMロールの設定を行います。Lambdaから一旦離れコンソールからIAMサービスに移動します。

  • ナビゲーションメニューから「ロール」を選択します。

  • 検索ボックスに先ほど作成したロール名を入力して、Lambdaで使われているロールを見つけます。

  • そのロールを選択して詳細ページを開きます。

  • 「許可を追加」ボタン→「インラインポリシーを作成」の順に選択します。

  • JSON形式で、以下のポリシーを貼り付けます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ce:GetCostAndUsage"
      ],
      "Resource": "*"
    }
  ]
}

「次へ」を選択し、適当なポリシー名を設定して「ポリシーの作成」をします。

Lambda基本設定

Lambdaのコンソールに戻ります。
デフォルトのタイムアウト時間が3秒なので30秒にしておきます。
Lambdaの「設定」タブ→「一般設定」→「編集」の順で選択し、設定します。

Lambda関数のデプロイ

ここまで作成したらデプロイを実行します。
デプロイボタンを押しましょう。

テスト

試しに実行します。
Lambdaの「テスト」タブ→「テスト」ボタンを選択します。
(JSONの内容は空{}でもなんでも大丈夫です)

なんとエラーが出ました🤯

これはAWS Lambda 上で requests モジュールが見つからないことが原因です。
Lambda の環境にはデフォルトでは requests は含まれていないため、自分でパッケージをデプロイする必要があります。

レイヤーの追加

具体的にどうするかというと、requests を含む Python パッケージを zip でまとめて Lambda Layer にアップロードします。

以下のコマンドを実行して、自分のPC上にzipファイルを作成します。
Macの場合

# フォルダ作成
mkdir python
# requestsパッケージをインストール
pip install requests -t python/
# ZIPファイルの作成
zip -r requests-layer.zip python

Windowsの場合(PowerShell)

# フォルダ作成
mkdir python
# requestsパッケージをインストール
pip install requests -t python/
# ZIPファイルの作成
powershell Compress-Archive -Path python -DestinationPath requests-layer.zip

zipファイルができたら、Lambdaのコンソールから「レイヤー」を選択し「レイヤーの作成」を選択します。

zipファイルをアップロードし、レイヤーを作成します。

  • 名前:なんでもOK
  • 作成したZIPファイルをアップロード
  • 互換性のあるランタイム:使用している Python バージョンを選択

最後にLambda関数にレイヤを追加します。

  • 関数の「コード」タブの「レイヤー」セクションに移動→「レイヤーの追加」を選択
  • カスタムレイヤーから先ほど作成した名前のレイヤーを選択し「追加」をクリック

再度テストを実行すると、無事成功!
こんな通知がSlackに届くはずです
(アイコンがAWSになっているのはawsという名前でカスタムステッカーを設定しているからです)

CloudWatch Eventsでスケジュール設定

定期的に実行するためにAmazon EventBridge(旧称:CloudWatch Events)に設定を施します。

  • AWS コンソールで Amazon EventBridge に移動
  • ナビゲーションから「スケジュール」を選択し「スケジュールを作成」をクリック

  • スケジュール名:なんでもOK(DailyAWSCostNotificationWeekdaysとかなんとか)
  • 頻度:定期的なスケジュール
  • スケジュールの種類:cronベースのスケジュール
  • cron式
    • 平日の午前9時に実行したいので以下のように埋めます。
    • 0 9 ? * MON-FRI *
  • フレックスタイムウィンドウ:オフ

ここまで設定したら「次へ」を選択

  • 「ターゲット」セクションで「Lambda」を選択
  • 「Lambda 関数」ドロップダウンから作成したLambda関数を選択
  • あとは特に追加の設定せず「次へ」で進めて「スケジュールを作成」で完了です。

終わりに

こんな感じで朝9時にSlackに通知が来るようになりました。

サービスごとにコストをまとめると、消したと思ったサービスがコストを発生させていたりと、色んな気づきがありますね。

あと、AWSのコンソールはしょっちゅう見た目やサービス名称、できることが変わるので、定期的に色んなサービスを触っておかないといけないな、とも思いました。

参考

https://dev.classmethod.jp/articles/notify-slack-aws-billing/

株式会社Sysmac

Discussion