🌟

ChatGPT が回答する Discord Bot をほぼ0円運用できるように作った

2023/03/12に公開

こういう個人開発する時って限りなく0円に近い価格で運用したくありませんか?
特にDiscordBotは色々制約がある上意外と作るのが難しかったので、知見を共有します
あとChatGPTの話はあんまり出てきません

※この記事にはオーバーエンジニアリングを含みます

DiscordBotの制約を知っておく

結論

  1. WebSocketを常時Listenするのが一番簡単に作れるがサーバー費用がかさむ
  2. InteractionをHTTPで受け取るようにすればWebSocketほど自由度はないがFaaSの載せられる
  3. HTTPのInteractionは大体3秒以内に返答しないとタイムアウトになってしまうため、重めの処理は工夫する必要がある

作り方の制約

まず第一に、DiscordBotを作るならEC2なりVPSなりでサーバーを建ててそこで実行するのが一番簡単に作れます
これはDiscordの仕様によるもので、 チャットや通話、スタンプなどのイベントをWebSocketで受け取る のが一番自由で何でもできるからです
そのイベントを簡単に受け取れるライブラリとして、既に discord.pydiscord.js がありとても便利です

ただし、WebSocketを受け取るということは、 サーバーを常時起動させておく ということになります
その場合はGCPやAWSの何かの無料枠などを使ったり、 render.comrailway などの激安PaaSを使えば1000円以内くらいでできると思います

別の選択肢としては、WebSocketとは別にDiscordからの Botコマンドのみ をHTTPで受け取れる Interaction という方式もあります
ということは、常時起動してなくていいですしFaaSに載せられ、めちゃくちゃお財布に優しくなります
注意点としては、受け取るイベントがBotコマンドのみとなってしまうため細かい操作や、例えばVoiceチャンネルに誰か入ってきたみたいなイベントは取れません
https://discord.com/developers/docs/interactions/receiving-and-responding

Interactionの制約

HTTPのInteractionは大体3秒以内(体感)にリクエストを返答しないとDiscord側がタイムアウトとして判断して、エラーになってしまいます
今回のBotはOpenAIApiにリクエストを投げる必要があったため、試作段階の時点で9割方タイムアウトしてしまいました
ということは、Botへの返答はさっさとしておいて、メインの処理は非同期にするなりなんなりする必要があるということです

今回作ったアプリケーションの機能概要

  1. /talk question:? コマンドで質問を送信し、回答が表示される また、会話はキャッシュし継続して会話できるようにする
  2. /talk question:? voice:True を質問と送信するとボイスチャンネルで回答を読み上げてくれる 上記と同様にキャッシュする
  3. /voice-talk コマンドを送信するとBotと音声でインタラクティブに会話することができる

実装方針を決める

制約と機能概要がわかったことで、選択肢を絞ります

  1. お金をかけたくないのでできるだけFaaSで作る
  2. 音声の生成や加工をするとなると CloudflareWorkers でやるのは無理(ffmpeg使ったりするため)
  3. Dockerを2つ動かしたい
    a. メインの処理をするサーバー
    b. 音声合成をするサーバー(Voicevoxを使わせて頂きました)
  4. 非同期の処理をうまく処理する必要がある
  5. キャッシュの機構を入れる必要がある

(CloudflareWorkersでDiscordBotを作る記事は過去に書いてるのでこちらもぜひ)
https://zenn.dev/suzuesa/articles/1de733b59f1441

使った技術解説

結論

  • インフラ
    • AzureContainerApps
      • API (Node.js)
      • Voicevox
  • PubSub
    • AzureServiceBus
  • Redis
    • Upstash
  • CDNEdge
    • CloudflareWorkers

AzureContainerApps って何?

サーバーレスコンテナをいい感じにしてくれるサービスです
後ろではKubernetesが動いていますが、開発者はその存在を意識しなくても、コンテナオーケストレーションの旨味だけを感じられるようになっています
個人開発でKubernetesは流石にやりすぎだろ……ってなりますが、ContainerAppsなら全然そんなことないと思います
イメージ的にはGAEやAWSEBとかと同じような操作感で使えます かなり手軽に使えます

AzureContainerAppsを構成するDapr

このAzureContainerAppsを使う最大のメリットとしてDaprがビルトインしているという点があります

Daprは、Kubernetes上で動作するサイドカーランタイムです
https://dapr.io/

主に各コンテナー間やPubSubやキャッシュなどのアプリケーションの外側の異なるサービスの通信をプロキシーしてくれます
これの何が嬉しいかというと、「アプリケーションからリクエストするのは1つのホストだけで良い」「外部のサービスが変更されたとしても、アプリケーションからリクエストするエンドポイントのインターフェースはDaprが策定した規格で統一されるため影響しない」というところです

例えばPubSubをトピックを1つ作りたいとしたら、以下のようにリクエストするだけで良いです

fetch(`http://localhost:3500/v1.0/pubsub/{コンポーネント名}/{トピック名}`, {
  method: 'POST',
  body: /* payload */
})

※コンポーネントとはDaprがプロキシする外部サービス1つのことを指します

Daprはたくさんのサービスに対応していますが、例えばPubSubだとCloud Pub/Subでも、AWSSQSでも、AzureServiceBusでも、RedisStreamでも↑のエンドポイントは一切代わりません
更にこの先の接続先情報はコンポーネントとして予め定義しておくことができます

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: {コンポーネント名}
spec:
  type: state.redis
  version: v1
  metadata:
    - name: redisHost
        value: {ホスト名}
    - name: redisPassword
        value: {パスワード}

AzureContainerAppsと併用する場合はAzureのダッシュボードから設定すればOKです

この様に色々なサービスとの通信をDaprがプロキシしてくれているため、今回作ったアプリケーションからは localhost:3500 しか叩いてません これは本番でも同様です
Daprはサイドーカーランタイムのため、必ずアプリケーションのDockerと同じホストのあるポートをHTTPまたはgRPCでListenしています
このエンドポイントにアクセスできれば何でもつながる、という世界になっています
ネットワークのどうのこうのとかIPがどうとかを考えなくて良いため、とても簡単にサービスメッシュを組むことができます

DaprはOSSのサイドカーランタイムなので、GKEなりにデプロイするにはKubernetesを意識する必要があります
ですが、AzureContainerAppsはDaprをビルトインしているため、DaprのPod管理などは全く気にしなくて大丈夫です

AzureServiceBus

PubSubに関しては正直何でも良かったですが、最初からAzureを使用することを想定していたためServiceBusを選びました
DaprはPubSubの接続先としてRedisStreamも選べるので、自分でRedisサーバーを建てるのも選択肢にありましたが、ServiceBusの方が料金が圧倒的に安そうだったのでこちらにしてます

Upstash

UpstashはサーバーレスRedisサービスです
とにかくクソ安くて便利で、何も考えずに使えます
今Redisをキャッシュとして使うなら、ここ一択な気がします
https://upstash.com/

CloudflareWorkers

ContainerApps作ってるのにCloudflareWorkersも結局使っちゃってるんですが、
これはContainerAppsのコールドスタートを避けるために使っています
DiscordのInteraction先をContainerAppsに向けてしまうと、コールドスタートからの復帰中にDiscordがタイムアウトさせてしまいエラーが出ます
ですので、InteractionをCloudflareWorkersに作ったハンドラに向けて、そこからServiceBusにTopicを飛ばしています
Topicを飛ばしてしまえばあとはContainerAppsがSubscribeしてるので、勝手に処理してくれます

今回の気付きとして、こういったシンプルにリクエストを受け取ってキューを飛ばすだけみたいな処理にCloudflareWorkers向いてるな〜って感じました(FaaSだから当たり前か)
もっと非同期処理が当たり前な世界になってほしいです

結局いくらぐらいかかってるの?

1日200~300リクエストくらいしかこないし、自分と身内でしか使ってないのですが(公開はしてるけど使ってくれる人がいない)
1週間くらい使ってこんな感じです
UpstashとCloudflareWorkersは0円です


ContainerAppsは頻繁にコールドスリープしてくれてるため、無料枠の範囲内に収まっています コンピューティングリソースよりログ保管料金の方が高いってマジ……!?
一番高いのはOpenAIになっちゃいましたね それでも過疎Botなので無料枠内ですが……

おわりに

今回CotainerAppsやUpstashを初めて使ったのですが、自分の中での技術選択肢が増えました
個人的に、技術選定の選択肢のフローとしては1番にCloudflareWorkersが、EdgeFunctionが要件的にダルそうならContainerAppsを選ぶことになりそうです

もう終始本当に便利でした 意外とDaprが薄くて使いやすいです もしContainerAppsがダメでも、Daprだけは残してGKEに載せると思います
CloudRunとかも良いですがサービスの連携が面倒になりがちなので、ちょっとでも複雑なアプリケーションを作るならContainerAppsを最初に考えても良さそうです

あくまで個人開発なら、の前提付きですがこんな感じで色んなサービスを駆使することで安く運用できるサービスを作れました
みなさんも変なPaaS面白サービス便利OSS作ったアプリなどあればコメントやTwitterで教えて下さい

Discussion