AWS利用料金をSlackで確認！Step Functionsの新機能を使ったローコードサービス

## はじめに

今回はAWSのリソース消し忘れによる高額請求と、その再発防止策のお話です。

## 経緯

### 知ってたけど。

AWSのリソースの消し忘れで思わぬ高額請求にびっくりしたことありませんか？

私は、こういった意図せぬ高額請求に関連する記事やLTを何度か見てました。
その中には、AWS Budgetsの設定などの対策も紹介してくれていました。

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

「ふーん。そういうこともあるんだな！」で終わらせてませんか？

次はあなたの番かもですよ？

### やってきた、高額請求。

そして、2024年12月、遂に私のところにも2万円超の請求が来てしまいました。。

![スクリーンショット 2024-12-05 4.27.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/2fab4cb3-b547-f9dd-f4c5-530250fb6177.png)

私の場合は、Auroraの自動復旧による請求です。
DBクラスターは停止したのち、7日で自動復旧します。

きちんと警告してくれるので、私はこの仕様を理解はしてましたが、削除を失念しており結果的に高額請求を起こしてしまいました。

![スクリーンショット 2024-12-05 4.32.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/5b35a8e0-5cdc-581b-67d2-0c936a75eef0.png)

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/aurora-cluster-stop-start.html

### よし、再発防止だ！

すぐできる再発防止はAWS Budgetsを利用することです。

AWS Budgets閾値を設けて、それを超えた場合にメールや、SNS、Chatbotで通知をしてくれるサービスです。

ただ、それだけだと面白くないので、今回は最近発表された以下の機能を試しつつ、サーバレスな利用料金を検出するサービスを作ってみました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/11/aws-step-functions-variables-jsonata-transformations/

## どんなものを作ったか？

### 完成品

作ったものは以下に公開済みです。

https://github.com/SatoshiMoriyama/DailyAWSCostGuard

資材はCloudFormation用のテンプレートのみで、Lambda等の資材がありません。
今回の新機能である、JSONataを使った処理のみで実現しているのです！

![スクリーンショット 2024-12-05 4.44.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/ee27bab6-1b96-af25-2a31-1c168f4b5692.png)

機能は日々のコスト総額とその内訳をSlackで通知するといったものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/e790cd32-efe5-8543-d3cc-5e1987833d8c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/628b84e6-87a4-13fc-8446-eadad937ad22.png)


### 処理概要

構成は、EventBridge(Scheduler)→Step Functionsのシンプル構成。

ステートマシンは以下のようなフローです。

![スクリーンショット 2024-12-05 4.58.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/85e68771-1ca8-d4be-4d20-43430fdfcd85.png)

利用しているサービスは以下の通りです。

* AWS CostExplolerのGetCostAndUsage API
* Amazon Bedrock
* Amazon SNS
* AWS Chatbot

以降、各ステップの内容を紹介します。

#### SetTargetDates

コスト集計する対象日を決めています。

AWS CostExplolerへの反映は24時間必要であることから、前々日分を取得するようにしています。
また時差（JST-UTC）も考慮し、EventBridgeのトリガーは朝9時にしています。

「前々日」や「前日」といった日付は以下のようにJSONataの機能で取得しています。

`{% $fromMillis($toMillis($states.context.State.EnteredTime) - 48*60*60*1000,'[Y0001]-[M01]-[D01]') %}
`

また、この日付は新機能である「変数」を使って保持するようにしました。

```json
{
  "StartDate": "{% $fromMillis($toMillis($states.context.State.EnteredTime) - 48*60*60*1000,'[Y0001]-[M01]-[D01]') %}",
  "EndDate"  : "{% $fromMillis($toMillis($states.context.State.EnteredTime) - 24*60*60*1000,'[Y0001]-[M01]-[D01]') %}"
}
```

これまでは、以降のステップで利用したいデータは各ステップを経由させてデータを引き継いでいく必要がありましたが、名前の通り、グローバル変数のような利用ができ、非常に便利になりました。

![スクリーンショット 2024-12-05 5.23.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/80754a58-e256-8825-90f3-84a6819a9d9e.png)

#### GetCostAndUsage

GetCostAndUsage APIをコールし、SetTargetDatesで決定した日付の範囲でのコストをサービス別に取得します。

少し削っていますが、GetCostAndUsage APIのレスポンスJSONはこんな感じです。

```json
{
  "DimensionValueAttributes": [],
  "GroupDefinitions": [
    {
      "Key": "SERVICE",
      "Type": "DIMENSION"
    }
  ],
  "ResultsByTime": [
    {
      "Estimated": true,
      "Groups": [
        {
          "Keys": [
            "AWS Cost Explorer"
          ],
          "Metrics": {
            "UnblendedCost": {
              "Amount": "0.01",
              "Unit": "USD"
            }
          }
        },
        {
          "Keys": [
            "AWS Key Management Service"
          ],
          "Metrics": {
            "UnblendedCost": {
              "Amount": "0.010752688",
              "Unit": "USD"
            }
          }
        },
        {
          "Keys": [
            "CloudWatch Events"
          ],
          "Metrics": {
            "UnblendedCost": {
              "Amount": "0",
              "Unit": "USD"
            }
          }
        }
      ],
      "TimePeriod": {
        "End": "2024-12-04",
        "Start": "2024-12-03"
      },
      "Total": {}
    }
  ]
}
```

サービス別に集計すると、トータル額が取得できないところや、Amountが文字列として返ってくるところの対応をJSONataの`$sum`や、`$map`,`$number`を駆使し、対応しています。

変数`totalUnblendedCost`には、サービス別のAmountの総額を格納し、変数`serviceDetailsJSON`はサービス別コスト要約のため、JSONをそのまま格納してます。

```json
{
  "totalUnblendedCost": "{% $formatNumber($sum($states.result.ResultsByTime.Groups.Metrics.UnblendedCost.Amount ~> $map(function($v) {$number($v)})), '0.00') %}",
  "serviceDetailsJSON": "{% $states.result.ResultsByTime.Groups %}"
}
```

ちょっとした集計であれば、Lambda不要です。

#### CheckCostThreshold

Slackには毎日通知が行われるため、時間経過するとあまり見なくなることが予想されます。

そこで1日のコストの閾値を設定し、それを超えた場合はアイコンを変更するようにしています。

##### :warning:閾値を超えていない場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/e790cd32-efe5-8543-d3cc-5e1987833d8c.png)

##### :exclamation:閾値を超えた場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/b68e27dd-88e9-cfb7-1078-7023b8200a8e.png)

#### ServiceCostSummary

変数`serviceDetailsJSON`に格納したJSONを元にサービス別のコストの要約をBedrock(anthropic.claude-3-5-sonnet-20240620-v1:0)で実施しています。

「JSON形式のサービス使用状況を分析してください」といった単純なプロンプトなので、毎回回答の形式が異なりますが、いったん目的は満たせているのでよしとしています。

>このJSON形式のデータは、各種AWSサービスの使用状況と関連コストを示しています。以下に分析結果をまとめます：
>1. 最もコストがかかっているサービス：
>  - EC2 - Other: $0.0495483888
>2. 2番目にコストがかかっているサービス：
>  - AWS Key Management Service: $0.032258064
>3. コストが発生しているその他のサービス（金額順）：
>  - AWS Cost Explorer: $0.01
>  - Amazon Simple Storage Service: $0.0015537559
>  - Amazon Relational Database Service: $0.0006088047
>  - Amazon Route 53: $0.0000872
>  - AWS Secrets Manager: $0.000015
>4. コストが発生していないサービス（$0）：
>  - AWS Step Functions
>  - Amazon CloudFront
>  - Amazon DynamoDB
>  - Amazon Simple Notification Service
>  - Amazon Simple Queue Service
>  - AmazonCloudWatch
>  - CloudWatch Events
>5. 総コスト：
>  約$0.0940711134（全サービスの合計）
>6. 注目点：
>  - EC2関連のサービスが最も高額で、総コストの約52.7%を占めています。
>  - AWS Key Management Serviceも比較的高額で、総コストの約34.3%を占めています。
>  - 多くのサービスが無料または非常に低コストで使用されています。
>この分析から、コスト最適化の機会としては、EC2とAWS Key Management Serviceの使用状況を見直すことが考えられます。他のサービスは比較的低コストで運用されているようです。

#### SendSlackNotification

最後にSNS経由でChatbotをコールし、Slackに通知します。

ここはStepfunctionsとChatbotが統合されていると思ってたのですが、直接統合はされてない模様？

良い方法ではないかもですが、下記の記事を参考にさせて頂き、Slackへメッセージを送付しています。

https://dev.classmethod.jp/articles/send-message-to-slack-from-sns-by-manual/

## まとめ

とても簡単なサービスですが、Step Functionsの可能性を感じることができました。

これまでは一連のバッチ処理専用のサービスと思い込んでましたが、普通にサービス用途として利用できそうだなと感じました。

なお、高額請求なのですが、AWSのサポートに問い合わせた結果、今回限りで免除していただけました。

とても親切な対応に感謝すると共に、この経験を周りにアウトプットすることが私の再発防止策だなと思い、記事を書かせていただきました。