AWS SQSの罠：Slackから使うBedrock画像生成でうっかり300ドルを溶かした教訓😭

## はじめに

生成AIの話題に事欠かない毎日ですが、
自分も何か作ってみたいと思い、まずはSlackからAmazon BedrockのAPIを叩いて画像を生成するアプリを作ってみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/362b3461-cae6-49cb-bb1a-1ed0efe5a4e1.png)

コードはこちら。
AWS CDKでインフラ構築できるように実装しています。画像生成はSDXLを使ってみました。

https://github.com/smallriver0316/slack2bedrock-image-generator

Slackから生成AIのAPIを呼び出して試す実装例はネット上に同様の実装例は既にたくさんあって、自分も他の方の記事を大いに参考にさせていただきました。
やってることは以下の記事と同じです。

https://dev.classmethod.jp/articles/amazon-bedrock-slack-chatbot-generate-image/

しかし、開発中に自分のミスで300ドル以上の請求を食らってしまいました！
本記事では、開発中に経験したコスト問題とその対策をまとめておこうと思います。

## システム構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/abb59398-d202-466f-8cea-879f3fcb31d3.png)

構成としてはほぼ上記の記事と同じです。
SQSを使用しているのは、Bedrockからのレスポンスの遅延に対処するためです。
Slack Events APIはレスポンスを受け取るのに3秒以上かかると、リクエストを再送する仕様になっています。そのため、処理時に時間がかかると同じメッセージが複数回Slack上に表示される現象が見られます。
SQSを使うことで、Bedrockとのやり取りを非同期に実行できるようにし、この問題を防いでいます。

しかし、これがあだとなって300ドルをどぶに捨てる結果となりました。

## 原因

巷にあるAWSで2桁、3桁万円の請求を食らった事例と比較すると霞んでしまいますが、個人でなおかつお試しで作ったものに5万円ちかく溶かしてしまったのは悔しいところでした😭

原因はSQSをサブスクライブするLambda関数でSlackに画像をポストした際の失敗を放置してしまったこと。

Lambda内で処理が失敗すると、SQSにはメッセージが残ることになります。
そして、これに対してリトライが何度も走ります！
Slackへ画像をポストする際の画像データのフォーマットの変換を試行錯誤していて、リトライが走っていることに気づかず1日放置したら、Bedrock APIへの画像生成リクエストが繰り返し実行され、コストが300ドルを突破していました。

## 対策

SQSでこのあたりのことを調べると、どうしても如何にスケーリングさせるかという話が多く出てきます。つまりは、サブスクライブするLambda関数を複数同時起動するとか、失敗しても如何にリカバリーするかとかです。

しかし今回のユースケースではそんなことは期待していなくて、キューにメッセージが詰められたら、一つずつ処理して、失敗した処理は即捨てられればいいのです。

これを実現する方法はそれほど多くなく

* 失敗したらメッセージを削除する
* 失敗したメッセージをDead Letter Queueに送る

ぐらいになります。

ちなみに、LambdaのEventSourceMappingの設定でリトライを制御できるかと検討しましたが、SQSからLambdaへのトリガーにおいてはリトライ回数を制限できませんでした。

```
Resource handler returned message: "Invalid request provided: Unsupported MaximumRetryAttempts parameter for given event source mapping type. (Service: Lambda,
Status Code: 400, Request ID: f29c4f49-af16-4541-a957-637806cffde8)" (RequestToken: b1bdd047-db15-0080-6fdd-34f4b1375d1e, HandlerErrorCode: InvalidRequest)
```

杞憂でしたが同時実行数の制御も1は指定できません。

```
Error: maxConcurrency must be between 2 and 1000 concurrent instances
```

### エラー時にもSlackに通知がいくようにしておく

知らないうちにリトライしまくるのを検知する一番の方法は、失敗した際にSlackに通知を出すようにしておくことです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/8c3021e2-0cb2-45e5-8ffa-1c65e2f7b812.png)

```python
    except Exception as e:
        logger.error(e)

        sqs.delete_message(
            QueueUrl=SQS_QUEUE_URL,
            ReceiptHandle=event["Records"][0]["receiptHandle"],
        )

        slack_client.chat_postMessage(
            channel=channel_id,
            text=f"Error occurred: {e}",
        )
```

### AWSコンソールで「コストと使用状況」を見えるようにしておく

自分の怠惰が原因ですが、これまでプライベートで使っているAWSアカウントについて「コストと使用状況」をIAMユーザーからでも見れるようにしていませんでした。

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/22735611-bb9c-4a4e-9f4e-f9f9fb7b300e.png)

Admin権限を付けているにも関わらず見れないのは、何か特別なロールを割り当てる必要があるからだろうと思っていましたが、違いました。
「コストと使用状況」を見るには、一度ルートユーザーでサインインし、アカウント画面から「IAMユーザーおよびロールによる請求情報へのアクセス」を有効にする必要があります。
これに加えてIAMロールで、Billingへのアクセス権限があれば見れるようになります。

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/ef80cda4-d9a0-46f5-9eca-a73260f35ca6.png)

個人的にはコストを見えづらくするAWS側の悪意？を勘ぐってしまいました。

### そもそもSQSいらなくない？

SQSを使う理由は、レスポンスの遅延に対するSlackのリトライ処理を防ぐためでした。
この対策として実行中のリトライを無視するという方法を選んでいる記事もありました。

https://qiita.com/Static_Novice/items/05a568fa381d2a9d00d6

しかし、もっと良い方法があります。
Lambda Invokeで直接別のLambdaを非同期実行すればいいのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/2aa6782b-0879-48e6-bb90-877c1380cb1f.png)

確かにスケーラビリティを考えて教科書通りに設計すれば、SQSが出てくるのでしょう。
でも内々でBedrockの機能を試すだけならこれで十分じゃないですか？

## おわりに

今回の対策をまとめると

* SQSを使う場合、メッセージの失敗処理をちゃんと入れておく
* 失敗したときもSlackに通知を送るようにしておく
* AWSコンソールで「コストと使用状況」をIAMユーザーからでも見れるようにしておく
* 非同期処理はSQSを使わなくてもできる

まあ今回のケースでは、「デプロイする前にローカル環境でちゃんとテストしておけ！」というのが一番の対策かもしれません。

生成AIは自然言語でやり取りするので、どうしてもチャットのインターフェースが必要になる場合が多いかと思います。
Streamlitなどを使ってチャットのUIを簡単に作ることもできて実際に試してみましたが、バックエンドとのLLMエンジンから生成されたメッセージをストリーミングで受け取るため、フロントエンド側もサーバーレスで運用できないのがコスト的に気になるところでした。

https://github.com/smallriver0316/my-bedrock-simple-chat

そう考えたとき、Slackがなかなか良い選択肢に思えてきました。
簡単にパブリッククラウドと連携でき、個人利用なら無料で十分フレキシブルに使えます。

今回のトラブルは高い授業料だと思って、次はエージェントを構築してSlackからアクセスできるようにしてみたいです。