【Slack APIタイムアウト対策】Cloud FunctionsでSlackへの返答とCloud Pub/Subへの追加を実装

Google Cloud

2024.4.3

Topics

はじめに

こんにちは、フクナガです。
先日SlackからVertex AIをCloud Functions経由で呼び出す生成AIチャットボットを実装しました。その際、Slackには返答に対するタイムアウトがあり、何かしらの方法でそれを回避する必要があることを知りました。

スラッシュコマンド は、メッセージ投稿フォームからアプリの機能を呼び出すことができる機能です。

スラッシュコマンドの実行への応答は、とてもよくあるユースケースです。Bolt アプリは Slack API サーバーからのリクエストに対して 3 秒以内に ack() メソッドで応答する必要があります。3 秒以内に応答しなかった場合、コマンドを実行したユーザーに対して Slack 上でタイムアウトした旨が通知されます。
出典:スラッシュコマンド

正確には、「タイムアウトがあるって聞いたことがあるけど、初回レスポンスと処理の受け渡しってどうすればよいんだろう」と悩みました。
今回の記事では、Slack APIからCloud Functionsへリクエストを飛ばす際、どのようにして初回レスポンスを返すか、またその後どうやって処理を進めるか、についてご紹介できればと思います。

利用サービス

今回の想定アーキテクチャは下記です。

本アーキテクチャで利用する各サービスについてそれぞれご説明します。
知ってるよ!という方は「実装」から読み始めていただけるとよいと思います。

(1) Cloud Functions

Google Cloud Functions は、クラウド サービスの構築と接続に使用するサーバーレスのランタイム環境です。Cloud Functions を使用すると、クラウドのインフラストラクチャやサービスで生じたイベントに関連する、シンプルで一義的な関数を作成できます。対象のイベントが発生すると、Cloud Functions がトリガーされ、コードがフルマネージドの環境で実行されます。インフラストラクチャをプロビジョニングする必要はなく、サーバーの管理に悩まされることもありません。
出典:Cloud Functions の概要

Cloud Functionsでは様々なランタイムを利用できますが、今回はPython(3.11)を利用します。
今回は、下記二つの関数を実装します。
・Slackからメッセージを受け取り初回レスポンスを実施する
・Cloud Pub/Subに渡されたリクエストを受け取り、Slackへメッセージを送信

(2) Cloud Pub/Sub

Pub/Sub は、メッセージを生成するサービスを、それらのメッセージを処理するサービスと切り離す、非同期のスケーラブルなメッセージング サービスです。
Pub/Sub を使用すると、サービスが 100 ミリ秒程度のレイテンシで非同期に通信できます。
出典:Pub/Sub とは

今回は、1つ目のCloud Functionsからメッセージを受け取り、2つ目のCloud Functionsへ渡す役割を担います。

(3) Slack apps

Slackでは、スラッシュコマンドを利用したAPI実行や、Webhooksの利用が可能です。
今回は、Cloud Functionsへのリクエスト実施とCloud Functionsからのメッセージ送信の役割を担います。

参考:Introduction to Slack apps

実装

(1) Cloud Pub/Subの構築

本アーキテクチャでは、SlackからのリクエストをCloud Functions経由でCloud Pub/Subへ送り、それをトリガーとしてSlackへ応答するCloud Functionsを実行します。
それに用いるCloud Pub/Subトピックを作っていきます。

1. Cloud Pub/Subコンソールへ遷移し、「トピック」>「トピックを作成」を押下

2. 「トピックID」を入力し、その他はデフォルトのままで「作成」を押下

3. 作成が完了したことを確認する

(2) 【パブリッシャー】Cloud Functionsの構築

Cloud Pub/Subへのメッセージングを担当しているもののことをパブリッシャーと表現しております。
なんか、「パニッシャー」みたいで強そうですね。

そんなことはさておき、Slackからリクエストを受け取り、初期レスポンスを返す、そしてCloud Pub/SubへのメッセージングをするCloud Functionsを作成していきます。

1. Cloud Functionsコンソールへ遷移し、「ファンクションを作成」を押下

2. 下記パラメータを入力・変更し、画面下部の「次へ」を押下

  • 関数名
    任意の値を入力

  • リージョン
    好きなリージョンでよいが、今回は「asia-northeast1(東京)」を選択

  • トリガー > トリガーのタイプ
    「HTTPS」

  • トリガー > 認証
    「未認証の呼び出しを許可」

  • ランタイム > インスタンスの最大数
    デフォルトで「100」となっているので、極端にスケールしすぎないよう「3」に設定
    ※必須の設定項目ではありませんが、事故を予防するため、最小限にしておくことを推奨します。


3. 「ランタイム」を「Python3.11」に設定し、下記コードをそれぞれ入力する

①main.py

import functions_framework
from google.cloud import pubsub_v1
import requests
import json

@functions_framework.http
def hello_http(request):

  # Google CloudプロジェクトIDを記載する
  project_id = ""
  # (1)で作成したトピックIDを記載する
  topic_id = "fukunaga-slack-topic"

  question = request.form.get('text')
  user_id = request.form.get('user_id')

  publisher = pubsub_v1.PublisherClient()
  # The `topic_path` method creates a fully qualified identifier
  # in the form `projects/{project_id}/topics/{topic_id}`
  topic_path = publisher.topic_path(project_id, topic_id)
  data = question.encode("utf-8")
  future = publisher.publish(topic_path, data)
  print(future.result())

  msg = {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": f"<@{user_id}> さん、質問を受け付けました。"
        }
      }
    ]
  }

  return msg

②requirements.txt

functions-framework==3.*
google-cloud-pubsub
requests==2.25.1

4. 画面下部「デプロイ」を押下する

※本コードはSlackからのAPI実行を前提としているため、この段階での動作確認はできません。

(3) Slackアプリケーションの作成

1. 下記URLへアクセス

https://api.slack.com/apps/

2. Create New Appを押下

3. 「From Scratch」を押下

4. 下記を入力して「Create App」を押下

  • App Name
    任意の値を入力

  • Pick a workspace to develop your app in:
    本アプリを追加するSlackワークスペース

(4) Slash Commandsの作成

今回のボットは「/」コマンドを利用して実装します。
Slack上で「/sendmassage」と打ってから投稿したい文を記載し、Cloud Functionsに送るSlash Commandsを作成します。

1. 左側メニュー「Slash Commands」を選択し、「Create New Command」を押下

2. 下記を入力し、「Save」を押下

  • Command
    メッセージを送る際に文頭に記載するコマンドを指定

  • Request URL
    パブリッシャー用Cloud Functionsのトリガーとして設定されているHTTPS URLを記載
    ※Cloud Functionsで対象関数を選択し、「トリガー」タブを押下すると確認できる

  • Short Description
    このコマンドの役割や用途を記載

※場合によっては、作成したコマンドを有効化するために再インストールが必要である旨が画面上部に表示されますので、指示どおり対応してください。

(5) Incoming Webhooksの作成

Cloud Functionsからメッセージを受け取るために、Incoming Webhooksを作成します。

1. 左側メニュー「Incoming Webhooks」を押下し、Activate Incoming Webhooksの右側にあるOffと書かれたボタンを押下

2. 画面下部の「Add New Webhook to Workspace」を押下

3. 送信先のチャンネルを指定し、「許可する」を押下

4. Webhook URLが発行されていることを確認し、手元にコピーする

(6) 【サブスクライバー】Cloud Functionsの構築

Cloud Pub/Subからのメッセージを受け取って処理を行う関数を「サブスクライバー」と表現しています。

2. Cloud Functionsコンソールへ遷移し、「ファンクションを作成」を押下

3. 下記パラメータを入力・変更し、画面下部の「次へ」を押下

※下記画像のようにサービスアカウントに権限を付与するか確認する警告が出るので、「すべて付与」を押下

  • 関数名
    任意の値を入力

  • リージョン
    好きなリージョンでよいが、今回は「asia-northeast1(東京)」を選択

  • トリガー > トリガーのタイプ
    「Cloud Pub/Sub」

  • トリガー > Cloud Pub/Subトピック
    先ほど作成したCloud Pub/Subトピックを指定する

  • ランタイム > インスタンスの最大数
    デフォルトで「100」となっているので、極端にスケールしすぎないよう「3」に設定
    ※必須の設定項目ではありませんが、事故を予防するため、最小限にしておくことを推奨します。


4. 「ランタイム」を「Python3.11」に設定し、下記コードをそれぞれ入力する

①main.py

import base64
import functions_framework
import requests
import json

# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def hello_pubsub(cloud_event):

  # 取得したWebhook URLを入力する
  WEB_HOOK_URL = ""
  # Cloud Pub/Subから取得したデータを変数に入れる
  question = base64.b64decode(cloud_event.data["message"]["data"]).decode()

  requests.post(WEB_HOOK_URL, data=json.dumps({
    #メッセージ
    "text" : "元の質問は:" + question,
    }))

  return question

②requirements.txt

functions-framework==3.*
google-cloud-pubsub
requests==2.25.1

検証

今回のアプリは、Slack上でした質問の頭に「元の質問は:」とつける、とてもシンプルなものです。(余分な要素を省くため)
実際に動くかどうか試してみましょう!!

こんな感じでメッセージを送ってみます。

すぐに受付メッセージが来ました。

最終的に、こういった返答が返ってきました。

期待通り、タイムアウトを起こさずにSlackチャットボットの実装ができました。これを応用することで、Cloud Functions側で様々な処理を加え、情報を提供することができそうですね。

まとめ

今回の記事では、Slack APIからCloud Functionsへリクエストを飛ばす際、どのようにして初回レスポンスを返すか、またその後どうやって処理を進めるか、についてご紹介しました。それぞれのリソースの関係性や、実装方法を理解することで、様々な情報を提供するチャットボットとして実装ができそうです。
本記事をもって興味を持った方がいらっしゃいましたら、ぜひぜひ実装してみてください!

フクナガ

インフラエンジニア歴5年のフクナガです。AWS(特にBedrockとCodePipeline)が得意分野。休日はベースを弾いてます。技術力と発信力を高め、Top Engineerを目指しています。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら