Lazy listeners を使用して Lambda で Slack Bot を動かしてみた

2021.11.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Boltとは

Boltとは、Slack APIのフレームワークです。
PythonやJSなどでサポートしており、Slack APIの機能を楽に使用することできます。
Boltでは、DjangoやFlaskなどのWebサーバフレームワークとの互換性もあり、非常に扱いやすいです。
Adaptersとして紹介されているので興味がある方はそちらを参考にしてみてください。

Lambdaで動かすために

前述した通り、Boltでは、Webサーバ上で動作させることが一般的です。
というのも、Slack APIの一部では非同期処理が必要であるため、LambdaといったFaaSで動作させることは以前までは少し面倒でした。
しかし、Boltでは、Lambdaでも非同期処理を実現できるような Lazy listeners と呼ばれる機能が存在します。

Lazy listeners

Lazy listeners では、非同期処理を別のLambdaへと割り当てる機能を提供します。 非同期処理は、関数として同じファイル内に記述をし、その関数をLazy listenersとして指定するだけで、あとのLambda周りのことはBoltがやってくれます。 下記のコードですと、ackで指定したrespond_to_slack_within_3_seconds関数で一旦レスポンスを返し、時間のかかる処理run_long_processは別のLambda上で実行させます。

def respond_to_slack_within_3_seconds(body, ack):
    text = body.get("text")
    if text is None or len(text) == 0:
        ack(f":x: Usage: /start-process (description here)")
    else:
        ack(f"Accepted! (task: {body['text']})")

import time
def run_long_process(respond, body):
    time.sleep(5)  # 5秒 sleepさせる
    respond(f"Completed! (task: {body['text']})")

app.command("/start-process")(
    # ackの関数は、内部でackを使用する
    ack=respond_to_slack_within_3_seconds,
    # 非同期にしたい関数をlazyに指定する
    lazy=[run_long_process]
)

実際にやってみる

実際に、Botのボタンクリックをトリガーとし、クリック直後と3秒経過後にメッセージが送信されるように作成してみます。

動作環境

以下の環境で行います。

Serverless Framework: 2.48.0 
slack-bolt: 1.9.4

Slackの初期セットアップ

BotのAPIを発行します。まずは、以下のリンクを開き、Botを作成します。

Permissons

Botに持たせる権限を設定します。今回の場合は、チャンネルのメッセージのサブスクライブとメッセージその送信を行うので、channels:historygroups:historyを指定します。 権限付与後、Slackのワークスペースにインストールが求められるので、そのままインストールも行います。

Tokenの取得

Bot User OAuth TokenSigning Secretを取得し、メモしておきます。

Serverless アプリケーションの作成

slsコマンドでアプリケーションの作成を行います。

$ sls
What do you want to make? AWS - Python - Starter
What do you want to call this project? hello-lambda

また、プラグインとしてserverless-python-requirementsとserverless-dotenv-pluginを使用するので、こちらもインストールします。

$ sls plugin install -n serverless-python-requirements
$ npm i -D serverless-dotenv-plugin

作成されたseverless.yamlをAPI GatewayとLambdaを呼び出せるように、以下のようにします。

service: hello-lambda

frameworkVersion: '2'


provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: ap-northeast-1
  iamRoleStatements:
     - Effect: "Allow"
       Action: 
          - "lambda:InvokeFunction"
          - "lambda:GetFunction"
       Resource: "*"



plugins:
  - serverless-python-requirements
  - serverless-dotenv-plugin
functions:
  bolthello:
    handler: handler.handler
    events:
      - httpApi:
          path: /
          method: "*"

環境変数は、.envに準備します。 先ほどメモしたBot User OAuth TokenSigning Secretをそれぞれ、SLACK_BOT_TOKENSLACK_SIGNING_SECRETとして指定します。

SLACK_BOT_TOKEN=xoxb-hogeguga111111111
SLACK_SIGNING_SECRET=abcd11223344eeff

使用するモジュールは、requirements.txtに記述します。

slack-bolt
python-lambda
python-dotenv

Pythonスクリプトの作成

今回は、helloというワードがチャンネルに流れた場合、反応し、ボタンを送信させます。 また、そのボタンをユーザがクリックした場合、クリックイベントを非同期で行うように実装しました。 非同期にしたい処理は、lazyにリスト形式で指定します。

import os
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
import time



app = App(process_before_response=True,
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)


@app.message("hello")
def message_hello(message, say):
    # イベントがトリガーされたチャンネルへ say() でメッセージを送信します
    say(
        blocks=[
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": "こんにちは!"},
                "accessory": {
                    "type": "button",
                    "text": {"type": "plain_text", "text":"ボタンを押す"},
                    "action_id": "button_click"
                }
            }
        ],
        text=f"Hey there <@{message['user']}>!"
    )

def action_button_click(body,ack,say):
    time.sleep(3)
    say("ボタンおして、3秒経過したよ!")

def demo1(body,ack,say):
    say("ボタン押した!")
    ack("ボタン押した!")

app.action("button_click")(
    ack=demo1,
    lazy=[action_button_click]
)


def handler(event, context):
    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

アプリケーションのデプロイ

下記コマンドを入力してデプロイを行なっていきます。

$ sls deploy

最後に、endpointsとしてURLが出力されます。

Slackにエンドポイントを追加

ここで、私はハマったのですが、2箇所エンドポイントを登録する必要があります。

Interactivity & ShortcutsのInteractivity

Interactivity & ShortcutsのInteractivityに先ほどのURLを追加します。

Event Subscriptions

続いて、Event Subscriptionsにも先ほどのURLを追加します。
さらに、メッセージを購読できるようにSubscribe to bot eventsにも権限を追加します。

検証

では、動くかどうかを検証してみます。メッセージでhelloと送信してみます。
すると、Botがメッセージとボタンが送信します。
このボタンを押してみると、「ボタン押した!」、そしてその3秒後に「ボタンおして、3秒経過したよ!」と正常に動作することが確認できます。

まとめ

Slack Botの非同期処理をLambdaで行ってみましたが、Slackの設定周りで苦労しました。
他にも、非同期処理ができるのであれば、WorkflowやShortcutなどもLambdaで動かせそうなので、またの機会にやってみたいです。