🚨

【AWS】EventBridgeを使って毎日の課金額をSlackに通知する

2021/11/30に公開

概要

今回はAmazon EventBridgeで時間指定で「CostExplorerからAWSの利用料金を取得しSlackに通知するLambda」を起動する方法についてまとめます。

下記にある通り、同様の試みをされている例はいくつかあります。
それらの内容も参考にして、自分なりに解釈した方法の紹介になります。

https://qiita.com/ymktmk_tt/items/14bbdf2562bde2fa3a1d

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

https://zenn.dev/ytk6565/articles/aws-billing-daily-report

途中Lambdaが登場しますが、今回メインとなるのはあくまでAmazon EventBridgeなので、そちらを中心として紹介になります。ご了承ください。

使用技術の確認

まずは、今回コアとなるAWSの機能について紹介します。

AWS CostExplorerとは

https://aws.amazon.com/jp/aws-cost-management/aws-cost-explorer/

AWS Cost Explorer の使いやすいインターフェイスでは、AWS のコストと使用量の経時的変化を可視化し、理解しやすい状態で管理できます。

その名の通りAWSのコストを参照できます。
今回はCost Explorer APIを叩きますが、リクエストごとに0.01ドルかかるため闇雲に叩くのは禁物です。

参考
https://aws.amazon.com/jp/aws-cost-management/pricing/

AWS Lambda

https://aws.amazon.com/jp/lambda/

AWS Lambda は、サーバーレスでイベント駆動型のコンピューティングサービスであり、サーバーのプロビジョニングや管理をすることなく、事実上あらゆるタイプのアプリケーションやバックエンドサービスのコードを実行することができます。200 以上の AWS のサービスやサービス型ソフトウェア (SaaS) アプリケーションから Lambda をトリガーすることができ、使用した分だけお支払いいただきます。

言わずと知れたコンピューティングサービスで、各種コードをサーバレスで実行できます。
今回はNodeで動作するコードを使用します。

Amazon EventBridge

https://aws.amazon.com/jp/eventbridge/

Amazon EventBridge はサーバーレスイベントバスであり、アプリケーション、統合された Software-as-a-Service (SaaS) アプリケーション、および AWS のサービスから生成されたイベントを使用して、イベント駆動型アプリケーションを大規模に構築することを容易にします。EventBridge は、Zendesk や Shopify などのイベントソースから AWS Lambda やその他の SaaS アプリケーションなどのターゲットにリアルタイムデータのストリームを配信します。ルーティングルールを設定して、データの送信先を決定し、イベントパブリッシャーとコンシューマーが完全に疎結合化された状態でデータソースにリアルタイムで反応するアプリケーションアーキテクチャを構築できます。

昔からAWSに触れられている方にはCloud Watch Eventsといった方が馴染みがいいかと思います。
バッチ処理の完了などの各種イベントを起点として、Lambdaなどを起動することができます。

今回はcronを用いて、Lambdaを毎日特定日時に起動するバッチのように使いました。

前準備

AWSで作業に取り掛かる前に準備することがあります。
Slack Botの作成です。

人によってはSlack Webhookを使っていますが、今回は「特定のチャンネルにメッセージを通知するBot」を作成しました。

これについては以下の記事の通りになります。

https://auto-worker.com/blog/?p=825

あまり本筋ではないので割愛しますが、作成したBotTokenを控えておきましょう。

Lambdaの作成

Lambdaを作成していきます。
コードは以下の通りです。
なお、リージョンはus-east-1とします。
環境変数にSlack BotTokenと対象チャンネルのIDを設定しましょう。

const AWS = require('aws-sdk');
const axios = require('axios');
const ce = new AWS.CostExplorer({region: 'us-east-1'});

exports.handler = async (event) => {
  
    // 昨日と今日のDateインスタンスを作成
    const today = new Date();
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    
    // YYYY-MM-DD形式でstartとendを作成
    const start = `${yesterday.getFullYear()}-${('0' + (yesterday.getMonth() + 1)).slice(-2)}-${('0' + yesterday.getDate()).slice(-2)}`;
    const end = `${today.getFullYear()}-${('0' + (today.getMonth() + 1)).slice(-2)}-${('0' + today.getDate()).slice(-2)}`;
    

    // CEに投げるパラメータ
    const params = {
      Granularity: 'DAILY',
      TimePeriod: {
        Start: start,
        End: end,
      },
      Metrics: ['UnblendedCost'],
      GroupBy: [{
        Type: 'DIMENSION',
        Key: 'SERVICE',
      }],
    };
    
    // コストデータの結果格納用
    let costResult = [];
    // CEからコストデータを取得
    const cost = await ce.getCostAndUsage(params).promise();
    // 取得した結果をresultに格納していく
    cost?.ResultsByTime?.forEach((c) => {
      //console.log(c);
      c?.Groups?.forEach((g) => {
        if(g?.Metrics?.UnblendedCost?.Amount !== '0') {
          costResult.push({
            key: g?.Keys?.[0] ?? 'Any Service',
            amount: g?.Metrics?.UnblendedCost?.Amount
          })
        }
      })
    });
    
    // Slack投稿用の本文を作成
    let markdown = `*_AWS Billing_* \r\n`;
    // resultには各リソースごとの名称と金額が入っているのでリスト形式で出力
    costResult.forEach((r) => {
      markdown += `* ${r.key} : $${r.amount} \r\n`
    });
    
    // Slackに投稿
    const res = await axios.post("https://slack.com/api/chat.postMessage", {
      channel: process.env.SLACK_CHANNEL_ID,
      mrkdwn: true,
      text: markdown
    }, {
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        "Authorization": "Bearer " + process.env.SLACK_TOKEN
      }
    });

    const response = {
        statusCode: 200,
        body: JSON.stringify('Success!'),
    };
    return response;
};

また、ライブラリとしてaxiosなどを利用しています。従ってLayersを用いることをお勧めします。
LambdaのデプロイやLayersの作り方については自分の過去記事(下記)で解説しています。

https://zenn.dev/nekoniki/articles/10ac0c37957cc9

https://zenn.dev/nekoniki/articles/6a30b75da0fac5

注意点ですが、Lambdaの実行ロールに以下のポリシーを追加する必要があります。
用途はCostExplorerからの値の読み取りです。

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

EventBridge側の設定

やることは大きく分けて3つあります。

  1. ルールを追加
  2. Lambdaにリソースポリシーを追加し、1で作成したルールからLambdaを実行できるように
  3. 1のルールにターゲット(Lambda)を追加する

以下、順番にCLIから操作していきます。
なお、Lambdaの名称は$LAMBDA_FUNC_NAMEに格納されているものとします。

①ルールの追加

まずはルールの作成です。
今回は毎朝9時(UTCだと0時)に実行させるようにします。
指定方法はcronです。

aws events put-rule \
  --schedule-expression "cron(0 0 * * ? *)" \
  --name costAlert

レスポンスとしてルールのARNが返ります。
この値も$RULE_ARNの値に格納されたものとします。

②リソースポリシーの追加

Lambdaのリソースポリシーを追加します。
リソースポリシーはざっくり説明すると、通常の管理ポリシーとは真逆で「リソースに対して◯◯がアクセス可能」といった感じで記載します。
ここではLambada関数をEventBridgeのルールが実行できるようにポリシーを定義しています。

aws lambda add-permission \
  --function-name $LAMBDA_FUNC_NAME \
  --statement-id daily-cost-alert \
  --action 'lambda:InvokeFunction' \
  --principal events.amazonaws.com \
  --source-arn $RULE_ARN

③ターゲットの設定

最後にターゲット設定です。
指定の時間になったら動作するルールが、「何を」するのかを決めます。

以下のようなtargets.jsonを作成しましょう。

targets.json
[
    {
      "Id": "1", 
      "Arn": "【Lambda関数のARN】"
    }
]

このJSONの値を先程作成したルールに紐付けます。

aws events put-targets \
  --rule costAlert \
  --targets file://targets.json

以下のようなレスポンスが返ります。

    "FailedEntryCount": 0,
    "FailedEntries": []
}

あとはcronで指定した時刻になったらSlackの対象チャンネルに通知が届いていれば成功です。
手堅くやるなら、Lambdaを作った段階で手動実行してみてSlackに通知が届くかをチェックしておくと、権限の設定漏れなどがないのでいいかなと思います。

うまくいくと以下のような通知が届きます。

まとめ

今回はEventBridgeから定期的にLambda関数を呼び出し、Lambda関数からCostExplorerで日次の課金額を取得してSlackチャンネルに通知するまでの一連の処理を紹介しました。

システムに絡むAWSのサービスが多くなってくるほど課金額を肌感で意識できなくなるので、目に見える形で通知する仕組みを用意しておくと事故を防げていいかなと思います。

今回の内容が役立ちましたら幸いです。

Discussion