Bolt for PythonとAWS lambdaを使ってインタラクティブなSlackショートカットを作成する
はじめに
SREホールディングス株式会社 エンジニアの丸山です。
業務の中でエンジニアが依頼をもらい次第手動で行っていた作業を非エンジニアだけで行えるようにしたい需要があり、入力に応じてlambdaを使ってDB操作など含めた処理を行いたいと考えていました。
非エンジニアでも操作できるようにSlack起点でlambdaにパラメーターを渡して呼び出せるようにします。
また、この記事の中では簡略化してlambdaからlambdaを呼ぶ処理は省略してlambdaを使ってSlack上でインタラクティブにパラメーターを受け取るという部分に注目します。
対象読者
Slack起点で自動化を行いたい人
構成
- API Gateway
- Lambda
- dynamoDB
- 必要に応じて重複実行防止のため、本記事では省略
- Slackの設定
選定
日常業務を楽にする目的だったためAWSリソースは少なく抑え、費用も抑えたいと考え、AWS Lambdaを使うことを考えました。
しかしSlackはリクエストから3秒以内に ack()
を返してリクエストがハンドルされたと確認することを要求しています。
それに対してLambdaは何かしらレスポンスを返したあとに処理を続行することができません。
このユースケースに対応する方法としてBolt for PythonではLazyリスナーという仕組みを持っています。[1]
Lazy リスナー関数は、FaaS 環境への Slack アプリのデプロイを容易にする機能です。この機能は Bolt for Python でのみ利用可能で、他の Bolt フレームワークでこの機能に対応することは予定していません。
とあるため、この用途では Bolt for Python 一択でした。
また、この機能を利用する場合当該Lambdaのロールに lambda:InvokeFunction
の許可を要求されるため、Slackにレスポンスを返してそのLambdaは終了しつつ後続処理を実行するため自分自身を呼び出していると考えられます。
また、UIにはJSONで簡単に表現できるため、Slack Block Kit[2]を利用します。
実現するもの
この記事では簡略化してSlackから起動した後ユーザーの入力を受取り、それをLambaに渡してインタラクションを行えるという部分に集中します。
Slackのショートカットから起動したらまず入力フォームのあるモーダルを表示して、ユーザーが入力を完了してボタンを押すとその入力がLamdaに渡され、処理が完了したらまたSlackにその結果を投稿させます。
実装
コード
Slackとのインタラクション
まず、リクエストを受け取ったら先述の通り3秒以内にackを返す必要があります。
ackを返す関数を用意します。基本的にはただackを返せばよいですが、モーダルを閉じてほしいなど特別な処理をしてほしいときに、ackを区別することもできます。
def common_ack(body: dict, ack: Ack):
text = body.get("text")
ack(f"Accepted! (task: {text})")
def clear_modal_ack(ack: Ack):
ack({"response_action": "clear"})
コールバック関数
後続でバックグラウンド処理したり、Slackへ画面表示したりするためにコールバック関数を用意します。
ask_view = {
"type": "modal",
"callback_id": ask_modal_callback_id,
"title": {"type": "plain_text", "text": "テストアクション"},
"submit": {"type": "plain_text", "text": "送信"},
"blocks": [
{
"type": "section",
"text": {
"type": "plain_text",
"text": "アクションを行うIDを入力してください"
}
},
{
"type": "input",
"block_id": input_id_block_id,
"element": {
"type": "plain_text_input",
"action_id": input_id_action_id,
"placeholder": {
"type": "plain_text",
"text": "例: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
},
"label": {
"type": "plain_text",
"text": "UUID"
}
}
],
"notify_on_close": False,
}
def open_modal(body: dict, client: WebClient):
client.views_open(trigger_id=body["trigger_id"], view=ask_view)
block_id, action_id は入力値を拾うために使います。
callback_idはsubmitアクションを拾うために使います。
ask_viewで定義しているblock
が前述のblock kitでblock kit builder[3]を使うとブラウザ上でUIを確認できます。
入力されたIDを受け取ったあとの関数も用意しておきます。
今回は簡単のため、受け取ったものを出力するだけにします。
def show(body: dict, client: WebClient, request: BoltRequest):
input_id = body["view"]["state"]["values"][input_id_block_id][
input_id_action_id
]["value"]
response = client.chat_postMessage(
channel=channel_id, text=f"UUID: *{input_id}*\nが入力されました。"
)
リスナー
リスナーを用意していきます。
lambda_handlerは定型文でよく、リクエストを受取り分岐するのはリスナーになります。
後述のSlackスラッシュコマンドでopen_modal
というcallbackIDを送信するので、まずopen_modal
を受け取ったら上記open_modalを呼び出します。
app.shortcut("open_modal")(
ack=common_ack,
lazy=[open_modal]
)
前述の通りリクエストを受け取ったらまず3秒以内にackを返して返したあとに続ける関数をlazyに渡します。
モーダルには送信ボタンを用意しているので、IDを入力してボタンを押されたあとのリスナーを用意します。
app.view(ask_modal_callback_id)(
ack=clear_modal_ack,
lazy=[show]
)
モーダルを閉じるackを返して入力値をポストする関数をlazyに指定します。
ハンドラー
最後にlambdaの入口lambda_handlerを用意します。
def lambda_handler(event, context):
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context)
これは定型文でよいです。呼び出されたら必ず行いたい処理があればここに追加しても良いと思います。
インフラ
Lambda
上記コードを環境変数とともに設定してデプロイします。
API Gateway
API GatewayにSlackとのインタラクションを行うlambdaを統合します。
ルートはANYかPOST / でその統合をアタッチします。
エンドポイントを控えます
Slackの準備
アプリの「ビルド」から「Create New App」を押してアプリを作成します
interactivity & shortcut のrequest URLに後述のAPI Gatewayのエンドポイントを、指定したい呼び出し方式に応じてショートカットを設定します。
今回の例ではSlackのショートカットから実行できるようになります。ここで前述のCallbackIDも設定します。
動作確認
スラッシュコマンドから呼び出すとモーダルが開き、適当なUUIDを入力して送信ボタンを押すと投稿されることが確認できました。
まとめ
Bolt for PythonとAWS lambdaを使ってインタラクティブなチャットボットを作成しました。
同様にしてBlock KitでUIを作成して各種アクションをCallbackIDで受取り、より複雑な処理もできそうです。
AWS Chatbotでやるよりもより複雑でより対話的なチャットボットを作りたいときに活躍すると思います。
Discussion