#Slack Bolt for python で複数Slackアプリを1つのBoltでホスティングしてコスト削減してみた

これまで社内用アプリ、社外でもインストールできる公開用アプリと色々なアプリをSlackが提供するフレームワークである、Bolt for Pythonおよび、Pythonフレームワークの一つであるFlaskを使って作ってきました。
社内的な事情で、というかboltが出てきた当初はHerokuに簡単にデプロイする仕組みがあったりしたので、そのままHeroku上にアプリを走らせているのですが、アプリによっては活動時間が大したことなかったりしてちょっともったいないよなという状態になったりしていました。
そもそも負荷が高くないようなアプリであれば、一つのHeroku環境で処理してしまいたいなと思ったことはありませんか。僕はありました。
ということで、いつものSlack瀬良さんにヒアリングしてみたり、そもそもFlaskの書き方がよくわかってなかったなということもあってそのあたりのコードの補完をChatGPTにお願いするなどして完成させたので共有していきたいと思います。

まず、パスの定義の仕方です

``` bolt.py
@bolt.route("/<app_id>/slack/events", methods=["POST"])
def slack_events(app_id):
    if app_id not in handlers:
        return "Invalid app ID", 404
    return handlers[app_id].handle(request)

@bolt.route("/<app_id>/slack/install", methods=["GET"])
def install(app_id):
    if app_id not in handlers:
        return "Invalid app ID", 404
    return handlers[app_id].handle(request)

@bolt.route("/<app_id>/slack/oauth_redirect", methods=["GET"])
def oauth_redirect(app_id):
    if app_id not in handlers:
        return "Invalid app ID", 404
    return handlers[app_id].handle(request)

```

こんな形にしておきます。
通常であれば
/slack/events
でイベントAPIを受け取るのですが、これを
/app_id/slack/events
というパスで受け取るようにします。

app_id部分は任意の文字列で、Slackアプリのマニフェスト部分をそのような形式にしておきます。

``` app_manifest.json
    "settings": {
        "event_subscriptions": {
            "request_url": "https://任意のドメイン/app_id(任意のパス)/slack/events",
```

こうやってマニフェストのrequest_urlのドメインは同じ、パスをアプリ毎に変更するという形でどのアプリからの入力なのかを判定していく事になります。

同様に、redirect_urlsも変更して下さい

```app_manifest.json
    "oauth_config": {
        "redirect_urls": [
            "https://任意のドメイン/app_id(任意のパス)/slack/oauth_redirect"
        ],
```

その他、パス設定がある場合は、同じように変更しておきます。

## secret関連の書き方

環境変数を変数に格納しておきます
``` bolt.py
client_id_app1, \
client_secret_app1, \
signing_secret_appp1, \
client_id_app2, \
client_secret_app2, \
signing_secret_app2, \
      = (
    os.environ["SLACK_CLIENT_ID_APP1"],
    os.environ["SLACK_CLIENT_SECRET_APP1"],
    os.environ["SLACK_SIGNING_SECRET_APP1"],
    os.environ["SLACK_CLIENT_ID_APP2"],
    os.environ["SLACK_CLIENT_SECRET_APP2"],
    os.environ["SLACK_SIGNING_SECRET_APP2"],
)

installation_store_app1 = SQLAlchemyInstallationStore(
    client_id=client_id_app1, engine=engine, logger=logger,
)
installation_store_app2 = SQLAlchemyInstallationStore(
    client_id=client_id_app2, engine=engine, logger=logger,
)
oauth_state_store = SQLAlchemyOAuthStateStore(
    expiration_seconds=120, engine=engine, logger=logger,
)
```

## パスはリスト化しておきましょう
後で使います
``` bolt.py
app_path_list = [
    "app1",
    "app2",
]
```

## APPの生成

install_pathとredirect_uri_pathを設定すると、カスタムパスとして使えるようになります。
``` bolt.py
def get_bolt_app(app_path):
    install_path = "/" + app_path + "/slack/install"
    redirect_uri_path = "/" + app_path + "/slack/oauth_redirect"
    signing_secret = globals()[f"signing_secret_{app_path}"]
    installation_store = globals()[f"installation_store_{app_path}"]
    client_id = globals()[f"client_id_{app_path}"]
    client_secret = globals()[f"client_secret_{app_path}"]
    return App(
        logger=logger,
        signing_secret=signing_secret,
        installation_store=installation_store,
        client=WebClient(retry_handlers = retry_handlers),
        oauth_settings=OAuthSettings(
            client_id=client_id,
            client_secret=client_secret,
            state_store=oauth_state_store,
            scopes=os.environ.get("SLACK_SCOPES"),
            user_scopes=os.environ.get("SLACK_USER_SCOPES"),
            callback_options=callback_options,
            install_path=install_path,
            redirect_uri_path=redirect_uri_path
        ),
    )

apps = []
for path in app_path_list:
    apps.append(get_bolt_app(path))

```

## 各種イベントの呼び出し方

``` bolt.py

def handle_message_events(client, body, event, logger, context):
    # ここにメッセージイベントの処理を書く

handlers_list = []
for app in apps:
    handler = SlackRequestHandler(app) #ハンドラーの定義をする
    handlers_list.append(handler)
    app.event("message")(handle_message_events)
    #ここからしたにすべてのイベントを書き連ねる

```


## handlerの渡し方

``` bolt.py
handlers = {}
app_count = 0
for path in app_path_list:
    handlers[path] = handlers_list[app_count]
    app_count = app_count + 1
```

ここで書いたhandlersを最初に書いたパスの定義部分で参照しています

## まとめ
こんな形にしておけば、アプリを追加するときは以下の3つをやるだけで追加が可能です
- SECRET関連情報を追加する
- installation_store の設定を追加する
- app_path_listにパスを追加する

ということで、複数のSlackアプリを、一つのBoltアプリでホスティングする為の書き方でした。