AWS Step Functions(JSONata)でAWS料金をSlackへ通知【Lambda無し】

AWS Step Functions(JSONata)でAWS料金をSlackへ通知【Lambda無し】

Clock Icon2025.04.07

「直近1週間ぐらいにAWSアカウント内で掛かったコスト」を Slack通知させる仕組みを作ってみました。
アーキテクチャと通知内容サンプルはこちら(↓)。

sc_2025-04-05_17-14-45_12147
AWSコスト監視くん

とってもシンプルですね。 シンプルに済んでいるのは Step Functions 内で JSONata が裏側で頑張っているためです。

JSONata はJSONデータ用の軽量なクエリおよび変換言語です。 複雑なクエリを表現でき、豊富な組み込み演算子と関数も備えています。 2024/11 より Step Functions にて、この JSONata が使えるようになりました。

https://aws.amazon.com/jp/blogs/news/simplifying-developer-experience-with-variables-and-jsonata-in-aws-step-functions/

本ブログは「JSONataのキャッチアップ」と 「良い感じのAWSコスト通知」 をモチベーションに書きました。

さっそくアーキテクチャの展開方法とJSONataについて説明します。

アーキテクチャを展開する

事前準備#1: Slackクライアントの設定

Amazon Q Developer in chat applications(旧: AWS Chatbot) にてSlackクライアントを設定しておきます。 ※すでに設定済みであればスキップください。

sc_2025-04-01_15-40-41_21073
[新しいクライアントを設定] > [設定]

sc_2025-04-01_15-41-26_28030
対象のワークスペースを選択して [許可する]

sc_2025-04-05_17-28-30_8008
設定済みクライアントにあることを確認

事前準備#2: Q Developer アプリを追加

Amazon Q Developer アプリを通知させたいSlackチャンネルに追加しておきます。

sc_2025-04-01_15-51-39_18269
[インテグレーション] > [アプリを追加する]

sc_2025-04-01_15-52-09_21542
Amazon Q Developer アプリを [追加]

CloudFormationスタックの作成

テンプレートファイルは GitHub Gist のコチラ をアップロードしてください。

参考:Gistの内容

パラメータは以下のとおり。

パラメータ 備考
Project 主にリソース名のプレフィクス。デフォルト: aws-cost-watcher
SlackWorkspaceID SlackワークスペースID。マネコンで確認可能
SlackChannelID SlackチャンネルID。チャンネルを右クリックして「リンクをコピー」 → URLの末尾部分
AngryThreshold お怒りメッセージ 通知になるしきい値。単位は USD

展開後のリソースはこんな感じです。

sc_2025-04-05_17-49-51_24492
Project=aws-cost-watcher の場合

動作確認

作成した Step Functions を実行してみましょう。

aws stepfunctions start-execution --state-machine-arn ${ステートマシンのARN}
# {
#     "executionArn": "arn:aws:states:ap-northeast-1:111111111111:execution:aws-cost-watcher:86d685fb-a076-4c82-9882-751cf2d4e866",
#     "startDate": "2025-04-04T17:52:51.336000+09:00"
# }

sc_2025-04-05_17-53-59_19756
コストが通知されました

しきい値(AngryThreshold)を超えると「お怒りメッセージ」になります。

sc_2025-04-05_17-55-07_27055
「お怒りメッセージ」例

JSONataについて

JSONataは主に以下のタスクで活用しました。

Cost Explorer API 実行時の「開始日/終了日」を決定する
Start: |-
  {%
    /* 7日前の日付(YYYY-MM-DD)を持ってくる */
    ($millis() - 86400000 * 7) ~> $fromMillis('[Y0001]-[M01]-[D01]')
  %}
End: |-
  {%
    /* 今日の日付(YYYY-MM-DD)を持ってくる */
    /* ※CEの最新コスト反映にはラグがあることに注意 */
    $millis() ~> $fromMillis('[Y0001]-[M01]-[D01]')
  %}
API実行結果から「コストの合計」と「コストになっているサービス」を集計する
CostSum: |-
  {%
    /* 全グループアイテムのコストを取得して、合計を求める */
    $states.result.ResultsByTime[].Groups[].Metrics.UnblendedCost.Amount.$number()
    ~> $sum() ~> $round(1)
  %}
CostSorted: |-
  {% (
    /* 全グループアイテムの「サービスとコストのペア」を取得 */
    $all_entries := $map(
      $zip(
        $states.result.ResultsByTime[].Groups[].Keys[0],
        $states.result.ResultsByTime[].Groups[].Metrics.UnblendedCost.Amount.$number()
      ),
      function($v) { {"Service": $v[0], "Amount": $v[1]} }
    );

    /* サービス名の重複を排除したリストを作っておく */
    $services := $all_entries.Service ~> $distinct();

    /* サービス名ごとのコストを計算する */
    $cost_per_service := $map(
      $services,
      function($s){
        {
          "Service": $s,
          "Total": $all_entries[Service=$s].Amount ~> $sum() ~> $round(1)
        }
      }
    );

    /* 降順でソート */
    $sort($cost_per_service, function($l, $r){ $l.Total < $r.Total });
  ) %}
Slack通知のメッセージを作成する
title: |-
  {%
    /* コスト合計がしきい値を超えたら「お怒りメッセージ」にする */
    $states.input.CostSum > $AngryThreshold ?
      ":serious_face_with_symbols_covering_mouth: コスト監視くんはお怒りです"
      : ":simple_smile: コスト監視くんは平常心を保っています"
  %}
description: |-
  {%
    "ここ1週間ぐらいのコストは "
    & $string($states.input.CostSum)
    & " USD です。"
    & "\n"
    & "\n:one: "   & ( $states.input.CostSorted[0].Service ~> $replace(/^(AWS|Amazon)\s*/,"") ) & ": " & $states.input.CostSorted[0].Total & " USD"
    & "\n:two: "   & ( $states.input.CostSorted[1].Service ~> $replace(/^(AWS|Amazon)\s*/,"") ) & ": " & $states.input.CostSorted[1].Total & " USD"
    & "\n:three: " & ( $states.input.CostSorted[2].Service ~> $replace(/^(AWS|Amazon)\s*/,"") ) & ": " & $states.input.CostSorted[2].Total & " USD"
    & "\n:four: "  & ( $states.input.CostSorted[3].Service ~> $replace(/^(AWS|Amazon)\s*/,"") ) & ": " & $states.input.CostSorted[3].Total & " USD"
    & "\n:five: "  & ( $states.input.CostSorted[4].Service ~> $replace(/^(AWS|Amazon)\s*/,"") ) & ": " & $states.input.CostSorted[4].Total & " USD"
  %}

JSONataには便利な組み込み関数がたくさんあります。 それらを多く活用しました(以下、今回使った関数たち)。

カテゴリ 関数 できること
String Functions $replace() 文字列内のパターンを指定された置換文字列に置き換える
Numeric Functions $number() 数値にキャストする
Numeric Functions $round() 指定された小数点以下の桁数に四捨五入する
Aggregation Functions $sum() 数値の合計を計算する
Date/Time Functions $millis() 現時刻のUNIX時刻(単位: ミリ秒)を返す
Date/Time Functions $fromMillis() UNIX時刻を指定フォーマットのタイムスタンプに変換する
Array Functions $zip() 複数の配列をグループ化して新しい配列を作成する
Array Functions $distinct() 配列から重複する値を除去する
Array Functions $sort() 配列を指定された基準に従ってソートする
Higher Order Functions $map() 配列の各要素に対して指定した関数を適用し、新しい配列を作成する
Higher Order Functions $filter() 指定された条件に一致する配列要素のみを抽出する

おわりに

AWSコスト通知の仕組みを JSONata芸で作成してみました。

JSONataに入門してみましたが、非常に便利に感じました。 JSONPathと比べて表現できる幅が格段に上がりますね。 ちょっとした加工や集計であれば、 Lambda無しでもなんとかなりそうです。

以上、参考になれば幸いです。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.