Slack AppをBoltというフレームワークを使って作成し、それをHerokuにホストして、エンドポイントのURLを公開せず動かしてみます。好奇心の実験です。ほとんどSlackのBoltことはじめガイドに書いてあり、基本的にはそれに従って作業していくだけです。例によってDisclaimerですが この記事に書かれている内容はすべて個人の意見・見解・実験・試行の記録であり、Heroku/Slack公式の内容ではありません。
事前準備
- Herokuアカウントが作成済みで、かつEcoサブスクリプションを利用中であること(要するにHeroku上でアプリを動かす準備が整っていること)
- Slack Workspaceを作成済であること(無料プランでもOKですが無料プランの場合Slack Appは10個までという制限があるので既に10個Slack Appをインストール済の場合は削除必要です)
- ローカル端末に
herokuCLIがインストール済であること
Slack側の準備
Slack Appの作成
https://api.slack.com/apps に移動して[Create New App]をクリック

左側のメニューから OAuth&Permissions を選択し、Bot Token Scopesまでスクロール、chat.write を追加

左側のメニューから Socket Modeを選択し、Enable Socket ModeのトグルをON

App-Level Tokenの発行が求められるので適当に名前をつけて[Generate] (ここでは"test"と入力)

App-Level Tokenが発行されるのでコピー ... (1)

ここでコピーをミスっても、Basic InformationページのApp-Level Tokensセクションで見れます

左側のメニューからEvent Subscriptionsを選択し、Enable Event SubscriptionのトグルをON

Slack Boltことはじめガイドに従い、message.channels、message.groups、message.im、message.mpimを追加

※既にアプリをワークスペースにインストール済だと、ここで再インストールを促されます。

左側のメニューからInstall Appを選択し、[Install to (ワークスペース名)]をクリック

[Allow]をクリック(※この画像はEvent Subscriptionで追加したパーミッションなどが欠けている状態のスクリーンショットです。Event Subscriptionでパーミッション追加していると右側のPermissionsの項がもう少し増えます)

OAuth Tokenが表示されるのでコピー ... (2)

Basic Informationのページに移動して Signing Secretを表示・コピー ... (3)

(1)(2)(3)は後程使うのでメモっておきます。
Slack Channelの準備
Slackのデスクトップアプリを開いて、適当にChannelを作成。publicでもprivateでもいいです。

Channelページの右上にあるワークスペースのアイコンをクリック

[Integrations]タブにある[Add an App]をクリック

先ほど作成したSlack App名を入力してフィルタリング・[Add]をクリック

これでSlack側の準備が整いました。
Herokuアプリの作成
https://dashboard.heroku.com/apps から[New]をクリック

[Settings] タブに移動し、Buildpacksの項でNode.jsのビルドパックを追加

Config Varsの項で以下の環境変数を設定
-
SLACK_SIGNING_SECRET= 上記の(3) -
SLACK_BOT_TOKEN= 上記の(2) -
SLACK_APP_TOKEN= 上記の(1)
ちなみにこの先動かすSlack AppはTypescriptのコードですが、Node.js 22以降はnodeコマンドによる.tsファイルのネイティブ実行が可能なので、その前提でtscによるトランスパイルを実施しません。通常tscはdevDependenciesに入り、かつHerokuはデフォルトでビルド中にdevDependenciesをPruningするので、ちゃんとやるなら(?)NPM_CONFIG_PRODUCTION=falseなどの環境変数の追加が別途必要です。(これは使用するパッケージマネージャーによって違います。詳しくはこちらをご確認ください。)私は面倒くさいのでやってないだけです…
[Deploy] タブに移動し、Slack Boltで開発したSlack Appを選択。とりあえずここでは私のサンプルコードを使います。

私のサンプルコードは、基本的にBoltことはじめガイドの実装をそのまま使ってるだけです。これはjavascriptで書かれているので、Typescriptで書き直してみようと思ってやってみたんですが、この範囲だと残念ながらあまり型の恩恵を得られないので、おとなしくjavascriptで書いた方が幾分かマシかもしれません。
ローカルで以下のコマンドを実行
$ heroku ps:scale web=0 -a (your heroku app name)
$ heroku ps:scale worker=1 -a (your heroku app name)
Herokuは最初のアプリの起動時、デフォルトでwebプロセスのみを立ち上げます。が、今回はwebプロセスを使わずworkerプロセスだけを使います。今回のHerokuアプリはPORT環境変数を使ってアプリのプロセスを公開しておらず、webプロセスの起動にそもそも失敗します。なのでwebは0にスケールして(=動かさず)、workerだけを動かす形に変えます。
うまく動いてればHerokuアプリのログに以下のようなメッセージが出力されているはず
Apr 09 22:40:33 (your heroku app name) app/worker.1 [INFO] bolt-app ⚡️ Bolt app is running!
ready to talk with Slack App!
Slack AppをインストールしたChannel上でhello @(slack app)の形でメンションしてみます。

と、いうことで、Slack Appが反応してくれました。
Slack App側で以下のようなログ仕込んでおけばHerokuアプリのログとしても確認できます。
app.message('hello' , async({message, say})=>{
console.log('debug:' + JSON.stringify(message)); // ←これ
if ('user' in message) {
await say(`Hey there <@${message.user||''}>!`);
}
});
Apr 09 22:17:24 hogehoge app/worker.1 debug:{"type":"message","user":"xxx",...,"channel_type":"group"}
ポイントと注意点
このアーキテクチャのポイントは、Herokuアプリ側をworkerプロセスにしていることで、つまりHerokuアプリのURLを使用しないでSlackと通信していることだと思っています。Boltことはじめガイドにもあるように、Socketモードだと、起動時にSlackとWebsocketで接続を確立し、以後はそれを使ってやり取りするため、クライアント側のエンドポイントを公開する必要がありません。クライアント側の視点で言えば、起動時にSlackとWebsocketで接続する際のOutboundの部分の通信さえできれば、他のInboundは閉じててもいいわけで(Outboundがステートフルである前提にはなりますが)、だとするとHeroku側はworkerプロセスみたいにバックグラウンドのバッチジョブみたいにして待ち構えるようにすればいけるんじゃね?と思ってやってみたら実際できてィヤッホォウ!!(?)ってのを確認するための記録記事でした。同じ考え方で、Herokuに限らず他のランタイムでも同じようなことはできそうです。(Cloudflare Workersとか、Edge Comuputingのランタイムだと若干話は違ってきそう)
一つ注意なのが、Slack Appを2つ以上のクライアントから起動すると、後から起動したほうで接続が上書きされるところです。HerokuでSlack Appを起動した後、ローカルで同じSlack Appを起動すると、つながるのはローカル側になり、最初に起動したHeroku側のほうは切断されるみたいです。「みたい」というのは、実際ログ等にも特にメッセージが出力されず、反応するのが後から起動したほうだけになったので、事象からそう切り分けしてるだけですが。サイレントに接続が切られてしまうと運用上は厄介かもしれませんね。あるSlack Appが起動中に別のクライアントから同じSlack AppにSocketModeで接続を試みた場合は接続を拒否、とかできたりするのかな??




