SlackアプリはSlackからのリクエストに対して3秒以内に応答がない場合は、タイムアウト扱いにされるという仕様があります。
これが原因でBoltJS+LambdaでハマったのでSQSで解決した件について。
3秒ルールとは?
Slackアプリは一般的にはBoltフレームワークを使って作られますが、Boltでは重い処理を実行する前にack()
すれば、先にレスポンスを返すので、タイムアウトの心配は無くなるとされています。
しかし、残念なことにBoltJSで作ったアプリをAWS Lambdaで運用しようとすると(というかFaaS全般?)、ack()
によるレスポンスを先に送出することができず、すべての処理を3秒以内に完了させないといけないという縛りが発生します。
その理由はLambdaには『レスポンスが返された時点で関数が終了する』という仕様が存在する為、下記のリンクで言及されているとおり意図的にレスポンスの返却を待機させているからです。
https://jsshowcase.com/question/slack-bolt-await-ack-asynchronous(リンク切れ)
Bolt-pythonではLazy Listener
という機能で重い処理は別プロセスで対応できるらしいのですが、BoltJSでは不可能のようです。(ほんとか?)
そこで、Serverlessの手軽さを生かしてAmazon SQSを利用することで重い処理はキュー経由で別インスタンスで実行するようにして、レスポンスを3秒以内に返却するという解決方法が編み出されています。
それについての組み込み方を説明します。
前提
まず、ServerlessでBoltJSによるSlack BotをLambdaにデプロイして動作確認するまでの手順を下記Boltのドキュメントに従って完了しているものとします。
今回はこれに対して、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
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
を使って回避することが可能のようです。
コメント