AWSJavaScript

SQSでBoltJS製Slackアプリの3秒ルールの壁を破る

SlackアプリはSlackからのリクエストに対して3秒以内に応答がない場合は、タイムアウト扱いにされるという仕様があります。
これが原因でBoltJS+LambdaでハマったのでSQSで解決した件について。

スポンサーリンク

3秒ルールとは?

Slackアプリは一般的にはBoltフレームワークを使って作られますが、Boltでは重い処理を実行する前にack() すれば、先にレスポンスを返すので、タイムアウトの心配は無くなるとされています。

Slack | Bolt for JavaScript
A framework that makes Slack app development fast and straight-forward. With a single interface for Slack’s Web API, Eve...

しかし、残念なことにBoltJSで作ったアプリをAWS Lambdaで運用しようとすると(というかFaaS全般?)、ack()によるレスポンスを先に送出することができず、すべての処理を3秒以内に完了させないといけないという縛りが発生します。
その理由はLambdaには『レスポンスが返された時点で関数が終了する』という仕様が存在する為、下記のリンクで言及されているとおり意図的にレスポンスの返却を待機させているからです。

https://jsshowcase.com/question/slack-bolt-await-ack-asynchronous(リンク切れ)

Bolt-pythonではLazy Listenerという機能で重い処理は別プロセスで対応できるらしいのですが、BoltJSでは不可能のようです。(ほんとか?)

Lazy listeners を使用して Lambda で Slack Bot を動かしてみた | DevelopersIO
Boltとは Boltとは、Slack APIのフレームワークです。 PythonやJSなどでサポートしており、Slack APIの機能を楽に使用することできます。 Boltでは、DjangoやFlaskなどのWebサー …

そこで、Serverlessの手軽さを生かしてAmazon SQSを利用することで重い処理はキュー経由で別インスタンスで実行するようにして、レスポンスを3秒以内に返却するという解決方法が編み出されています。
それについての組み込み方を説明します。

前提

まず、ServerlessでBoltJSによるSlack BotをLambdaにデプロイして動作確認するまでの手順を下記Boltのドキュメントに従って完了しているものとします。

Slack | Bolt for JavaScript
A framework that makes Slack app development fast and straight-forward. With a single interface for Slack’s Web API, Eve...

今回はこれに対して、10秒待機してからチャンネルにメッセージを投げる処理を追加することにします。

AWS SDKの導入

SQSクライアントをプロジェクトにインストールします。

$ npm install @aws-sdk/client-sqs

package.json

  "dependencies": {
    "@aws-sdk/client-sqs": "^3.87.0",
    "@slack/bolt": "^3.6.0",
  },

serverless.ymlの修正

今回使うキューの名前はHelloEventQueueとします。

provider.region

たぶん前提部分でデプロイしてる人はすでに書いているのではと思いますが、一応。

provider:
  region: ap-northeast-1

provider.environment

キューのURLを環境変数に追加。

provider:
  environment:
    SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET}
    SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN}
    HELLO_EVENT_QUEUE_URL: !Ref HelloEventQueue

provider.iam.role.statements

以下のように複数の権限を実行ロールに与えるようにします。

provider:
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - sqs:SendMessage
            - sqs:GetQueueUrl
          Resource: !GetAtt HelloEventQueue.Arn
        - Effect: Allow
          Action:
            - sqs:ListQueues
          Resource: arn:aws:sqs:${self:provider.region}:${aws:accountId}:*

resources.Resources

resources:
  Resources:
    HelloEventQueue:
      Type: "AWS::SQS::Queue"
      Properties:
        ReceiveMessageWaitTimeSeconds: 20
ReceiveMessageWaitTimeSecondsは必要に応じて設定してください。デフォルトのままだと待ち時間なしのポーリングが多数発生するので、料金コストが嵩んでしまいます(多分)

functions

SQS受信のイベントハンドラを追加。
ハンドル関数は delayHelloHandler という名前にします。

functions:
  async-hello:
    handler: app.delayHelloHandler
    timeout: 20
    events:
      - sqs:
          arn: !GetAtt HelloEventQueue.Arn

app.jsの修正

修正・追加箇所だけ記載します。

requireの追加

const { App, AwsLambdaReceiver } = require('@slack/bolt');
const { SendMessageCommand, SQSClient } = require('@aws-sdk/client-sqs');

SQSメッセージ送信部の追加

Delay HelloというメッセージをSQSで送ることにします。

// Listens to incoming messages that contain "hello"
app.message('hello', ({ message, say, context }) => {
  // 省略 say()
  const sqsClient = new SQSClient({
    region: process.env.AWS_REGION
  });

  const command = new SendMessageCommand({
    QueueUrl: process.env.HELLO_EVENT_QUEUE_URL,
    MessageBody: JSON.stringify({
      botToken: context.botToken,
      channel: message.channel,
      message: 'Delay Hello',
    }),
  });
  return sqsClient.send(command);
});

SQSメッセージ受信部の追加

ユーザーが発言したのと同じチャンネルに投稿します。
今回は10秒待機させて時間差で実行させます。
なお、レコードは配列で受け取るので、ループ処理が必要です。

module.exports.delayHelloHandler = async (event, context) => {
  // 10秒遅らせる
  await new Promise(resolve => setTimeout(resolve, 10000));

  const postMessageFunc = async record => {
    const params = JSON.parse(record.body);
    await app.client.chat.postMessage({
      token: params.botToken,
      channel: params.channel,
      text: params.message,
    });
  }
  return Promise.all(event.Records.map(postMessageFunc));
};

デプロイと動作確認

ローカルサーバーだとSQSメッセージが受信できません。AWSにデプロイします。

$ npx sls deploy

あとは従来と同じく、Slackボットが読み取れるチャンネルで hello と発言してみます。
その約10秒後に Delay Hello という投稿が出現すれば完了です。

以上!

余談

ちなみにBoltを使わなければ、callback を使って回避することが可能のようです。

Slack Bot をサーバーレスで運用する時の、タイムアウト対策【小技】 - Qiita
はじめにSlack Bot をサーバーレスで運用したい、という需要、それなりにあると思います。ここで Slack Bot と呼んでいるのは、例えば、こういうのです。…
スポンサーリンク
記事を書いた人

システムえんじにゃー🐈
趣味はエレキギター、自転車など。作曲したい。
World of Warshipsやってます。
記事に関する質問はお気軽にどうぞ。

surface0 (さーふぇす)をフォローする

コメント

タイトルとURLをコピーしました