こんにちは。
チーム内で週一で勉強会をやっているのですが、気付いたら3回に2回は僕がやってます。
「こりゃいかんぞ!Outputこそ最大の学びの場やぞ!」と思ったのですが、なんか自分から次はあなたねと指名するのもプレッシャー与えちゃうかも...と悩んでいました。
その仕事、Slackで。
ということで、Slackのslash commandに次の発表者を決めてもらうことにしました。
SlackにはBoltというnode.jsで動作するSlack app frameworkがあるので、今回はそれをDocker上で動作させ、最終的にはGCPのサーバーレスコンテナランタイムCloud Runでslash commandを提供するところまでやってみます。
アウトプットとしては、Slackのチャンネルで/kiminikimeta
とコマンドを打つと、Botが「@xxxxx、キミに決めた
」とそのチャンネルのユーザーから1人をランダムで選出してくれる感じをイメージしてます。
やること
- Slack appの作成
- Bolt on Dockerの実装
- ngrokでローカル環境を公開
- slash commandの作成
- ローカル環境で動作確認
- GCRへDocker imageを登録
- Cloud Runにサービスを登録
- slash commandのリクエストURLをCloud Runに向き先変更
- 本番環境で動作確認
並べてみると手順多いですね。一個一個はそんななのではりきっていきましょう!
Slack appの作成
まずはSlack appを作成します。GUIで。
Slack appはSlack apiのYour Appsのページから「Create New App」をクリックすれば作成することができます。
App Name
とDevelopment Slack Workspace
の入力・選択が求められるので教えてあげます。App Name
は今回は『test』にしてますが、本番利用時は『Pokemon Master』にする予定です。
これでひとまずSlack appの作成は完了です。以下のようにSlack appの詳細ページに遷移していると思います。
次にSlack appに情報へのアクセス権限を与えてあげます。
左のサイドメニューから『OAuth & Permissions』を選択します。ここでSlack botのSlackのワークスペースやチャンネルの情報へのアクセス権限を付与することができます。以下の権限を追加してあげます。
付与した権限は以下の通り。
- commands -> slash commandを使える権限
- chat:write -> slackにコメントを投稿できる権限
- channels:read -> パブリックチャンネルの情報の閲覧権限。あとでslash commandが入力されたパブリックチャンネルのユーザー一覧を取得するために必要。
- groups:read -> プライベートチャンネルの情報の閲覧権限。あとでslash commandが入力されたプライベートチャンネルのユーザー一覧を取得するために必要。
- im:read -> ダイレクトメッセージの情報の閲覧権限。あとでslash commandが入力されたダイレクトメッセージのユーザー一覧を取得するために必要。
- mpim:read -> グループダイレクトメッセージの情報の閲覧権限。あとでslash commandが入力されたグループダイレクトメッセージのユーザー一覧を取得するために必要。
これが完了したら、同じページの上の方に「Install App to Workspace」ボタンがあるのでクリックします。(クリック後に認可画面が出ますが「Allow」です。
これでSlack appの作成とWorkspaceへの登録が完了しました。
Bolt on Dockerの実装
次にBoltでSlack appの本体(slash commandを受け取った後の処理)を作っていきます。
まず、Boltの環境を整えたいのですが、Boltはnode.jsで動作するのでそれ用のDockerfileを用意します。
FROM node:13.7.0-stretch
ENV HOME="/app"
WORKDIR $HOME
COPY . $HOME
EXPOSE 3000
CMD ["node", "app.js"]
Dockerfileの詳細は省きますが、最終的にapp.js
を起動させるようにしてます。
また環境変数とかをいちいちオプション指定するのも面倒なのでDocker Composeも使っていきます。
version: '3'
services:
app:
build: .
environment:
- SLACK_SIGNING_SECRET=XXXXXXXXXX
- SLACK_BOT_TOKEN=XXXXXXXXXX
volumes:
- .:/app
ports:
- 3000:3000
ここでSLACK_SIGNING_SECRET
とSLACK_BOT_TOKEN
の二つの環境変数を指定しています。
これらは先ほどSlack apiページで作成したSlack appの値で以下のところで確認することができます。
-
SLACK_SIGNING_SECRET
: 「Basic Information」の「App Credentials」の「Signing Secret」 -
SLACK_BOT_TOKEN
: 「OAuth & Permissions」の「OAuth Tokens & Redirect URLs」の「Token for Your Workspace」の「Bot User OAuth Access Token」
上のdocker-compose.yml
のXXXXXXXXXX
の部分はそれぞれの値に置き換えてください。
また、docker imageの中にDockerfile
とdocker-compose.yml
は不要なので含まないように.dockerignore
を記述しておきます。
.dockerignore
Dockerfile
docker-compose.yml
ここで一度docker imageをbuildしておきます。
$ docker-compose build
次にnpm
でBoltをインストールします。
$ docker-compose run app npm init
$ docker-compose run app npm install @slack/bolt
npm init
時にアプリ情報を聞かれますので、いい感じに答えてあげてください。
これでBoltのインストールが完了したので、実際にアプリを書いていきます。
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
app.command('/kiminikimeta', async ({ command, ack, say, context }) => {
ack();
try {
// slash commandが叩かれたChannelに所属する全ユーザー情報を取得する
const result = await app.client.conversations.members({
token: context.botToken,
channel: command.channel_id
});
const members = result.members;
// 取得した全ユーザーから今回作成しているSlack appのBotを除外する
const bot_idx = members.indexOf(context.botUserId);
if (bot_idx > -1) {
members.splice(bot_idx, 1);
}
// Bot以外のユーザーから1つをランダムで選び、Channelに投稿する
const member = members[Math.floor(Math.random() * members.length)];
say(`<@${member}>、キミに決めた!`);
} catch (error) {
console.log(error);
}
});
(async () => {
await app.start(process.env.PORT || 3000);
console.log('⚡️ Bolt app is running!');
})();
app.command()
以外の部分はBoltの型みたいなものなので無心で。app.command()
のところは少し補足をしていきます。
まず、/kiminikimeta
のslash commandを受け取れるようにapp.command
の引数に指定してます。
最初にack()
を呼び出してます。これはお作法みたいです。Boltアプリがcommandを受け取ったことをSlack appに教えてあげています。これをしないとタイムアウトとかになってしまうのでお忘れなく。
この後はtry~catchでslack web apiを使っています。
conversations.members
APIでcommandが送信されたチャンネルのユーザーを取得します。
// slash commandが叩かれたChannelに所属する全ユーザー情報を取得する
const result = await app.client.conversations.members({
token: context.botToken,
channel: command.channel_id
});
const members = result.members;
conversations.members
を利用するにはtoken
とchannel
のパラメータが必須項目です。
token
はBot User OAuth Access Token
のことですが、これはcontext.botToken
で取得することができます。
channel
はslash commandが入力されたチャンネルを設定したいのですが、これはcommand.channel_id
で取得することができます。
最後にレスポンスの内容からメンバーのIDの配列をmembers
に格納しています。
// 取得した全ユーザーから今回作成しているSlack appのBotを除外する
const bot_idx = members.indexOf(context.botUserId);
if (bot_idx > -1) {
members.splice(bot_idx, 1);
}
次に先ほど取得したslash commandが入力されたチャンネルのユーザー一覧から今回のBot用のユーザーを除外します。BotのIDはcontext.botUserId
で取得ができまして、上のコードで先ほどのmembers
からBotのIDと一致する文字列を除去しています。
// Bot以外のユーザーから1つをランダムで選び、Channelに投稿する
const member = members[Math.floor(Math.random() * members.length)];
say(`<@${member}>、キミに決めた!`);
Botを除外したなかからランダムで1つのUser IDを取得してmember
変数に代入しています。
最後にsay
メソッドを使ってslash commandの結果をチャンネルにポストします。
ここで<@${member}>
とすることで、選ばれたユーザーにメンションをつけることができます。
ここまでがslash commandを入力されたチャンネルでランダムに1ユーザーが選ばれる処理の内容になります。結構シンプルですね。
ngrokでローカル環境を公開
ここまでで大体コーディングは完了です。実際に期待通りに動くか、まずはローカル環境で確認してみましょう。
といったものの、Slack appからローカルのパソコンにアクセス手段は基本的にありません。
そこでSlackの公式でもおすすめされている方法として、ngrokを使ってローカルで動くアプリにインターネット経由でアクセスできるようにトンネルしてあげます。
Slackでも使い方がこちらの記事で紹介されています。
簡単にまとめると、
- ngrokのダウンロードページで自分のクライアントにあったファイルをダウンロードする
- ダウンロードしたファイルをunzipする。
Downloadsディレクトリにダウンロードされた場合は、$ unzip ~/Downloads/ngrok.zip
-
ngrok
が解凍されるので、$PATHにファイルを移動。
通常/usr/local/bin
とかだと思うのでその場合は、$ mv ~/Downloads/ngrok /usr/local/bin
- コマンドが使えるようになるためにターミナル再起動かshellの再読み込み。bashを使っている場合は多分
$ source ~/.bashrc
をやります。こうするとngrok
コマンドを実行できるようになります。
今、DockerコンテナはPort 3000でやりとりができるようにしているので、以下のコマンドを実行してインターネットからDockerコンテナにアクセスできるようにします。
$ ngrok http 3000
Session Status online
Session Expires 7 hours, 59 minutes
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000
Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000
なんだか色々出てきますが、これだけでhttp://xxxxxxxx.ngrok.io
またはhttps://xxxxxxxx.ngrok.io
でアクセスするとhttp://localhost:3000
またはhttps://localhost:3000
にフォワーディングされるようになっています。
おっと、Dockerコンテナをまだ立ち上げていませんでしたね。
$ docker-compose up
はい。これでローカル側のテスト準備は完了しました。
slash commandの作成
次にSlack appの方でslash commandを作成していきます。slash commandを作成するときにコマンドを投げる先のURL(Bolt appのURL)が必要になるので先にngrokの設定をしていたのです。
slash commandはSlack appの管理画面の「Slash commands」メニューから「Create New Command」を選択することで作成できます。
「Create New Command」のモーダルが表示されますので適宜情報を入力しますが、
- Command:
/kiminikimeta
(Bolt側で指定したslash commandと同じであればOK) - Request URL: https://xxxxxxxx.ngrok.io/slack/events
- Short Description: 適当に何か
- Usage Hint: 入れたければ適当に何か
といった感じに入力してください。注意が必要な点としてはRequest URLは先ほどngrok
で作られたURLの後ろに/slack/events
のパスを追加してください。Boltがこのパスでリクエストを受け取るようになっているからです。
入力がおわったら「Save」してslash commandの作成完了です。
ローカル環境で動作確認
ここまで行けばテスト準備万端です。
Slack appをインストールしたSlack Workspaceに行ってみましょう。
そして何か適当なテスト用チャンネルなどに今作ったSlack appを「Add App」して呟いてみましょう。
ということでこれでSlackからSlack appがslash commandを受け取り、ローカルで起動させているBolt on Dockerの処理結果をSlackにポストできていることが確認できました!
あとはこのコンテナをどこかにあげればいいだけですね。
コンテナとngrok
はCtrl+C
で一度落としておきましょうか。
GCRへDocker imageを登録
今回は本番環境のアーキテクチャとしてGCPのCloud Runを選択しました。
コンテナをサーバーレスで動かしてくれるマネージドサービスです。ちょちょちょっと設定すれば、アクセスがあったときだけコンテナをデプロイして動いてくれるやつです。
選んだ理由はイケている気がしたからです!(こういうの大事だと思っている)
まず、GCPのコンテナレジストリであるGCR(Google Container Registry)に今回作ったコンテナイメージをアップロードしていきたいと思います。
GCP及びgcloud
コマンドはすでに使える状態という前提でお話します。 => gcloud
コマンドがまだのかたはこちら。
GCRではコンテナイメージは[HOSTNAME]/[PROJECT-ID]/[IMAGE]
の形式でpushすることができます。
HOSTNAME
はリージョンによって異なりますが、アジアの場合は「asia.gcr.io」を使うことができます。
PROJECT-ID
は各々のGCP PROJECTの名前で、IMAGE
は好きにつけてOKです。今回はIMAGE
はkiminikimeta
にしましょう。PROJECT-ID
は仮にで「sample」とします。
つまりDocker imageのイメージ名はasia.gcr.io/sample/kiminikimeta
になるということです。
Docker composeでビルドした際にこの名前のイメージができるように、docker-compose.yml
を更新します。
version: '3'
services:
app:
build: .
image: asia.gcr.io/sample/kiminikimeta #追加
environment:
- SLACK_SIGNING_SECRET=xxxxxxxxxx
- SLACK_BOT_TOKEN=xxxxxxxxxx
volumes:
- .:/app
ports:
- 3000:3000
更新が完了したらbuildします。
$ docker-compose build
Successfully tagged asia.gcr.io/sample/kiminikimeta:latest
ちゃんと指定したイメージ名でDocker imageが作成されましたね。
後はこれをGCRにpushします。
$ docker push asia.gcr.io/sample/kiminikimeta:latest
Cloud Runにサービスを登録
Docker imageのpushがおわったらCloud Runの設定をしていきます。これはGUIでやってみます。
メニューから「Cloud Run」を選択しコンソールに移動したら「サービスを追加」を選択します。
ここで各種設定をしていくのですが、以下のように設定しました。
- コンテナイメージのURL: 「選択」から先ほどpushしたイメージを選択する
- 「Cloud Run」 or 「Cloud Run for Anthos」: 「Cloud Run」
- Cloud Runのリージョン: Tokyo
- サービス名: kiminikimeta
- 認証: 未認証の呼び出しを許可
- コンテナポート: 3000
- 環境変数:
- SLACK_SIGNING_SECRET: xxxxxxxxxx
- SLACK_BOT_TOKEN: xxxxxxxxxx
「コンテナポート」と「環境変数」は「オプションのリビジョン設定を表示」をクリックしないと出てこないので見つけにくいかもしれないですね。その他の値はデフォルトのままでOKです。
ここまでできたら「作成」を選択してCloud Runサービスを作成します。
サービスの作成が完了すると以下のような画面になっていると思います。
右上の「URL」という部分がこのCloud Runサービスが動くトリガーとなるエンドポイントです。
slash commandのリクエストURLをCloud Runに向き先変更
Slack appのslash commandはまだngrok
のURLにリクエストを飛ばすように設定されているので、最後にエンドポイントをCloud RunのURLに更新します。
Slack appのslash commandsメニューの画面をみると、こんな感じに今登録されているslash commandを確認することができます。この鉛筆マークを押すと、そのslash commandの詳細を変更できるので、ここからURLを先ほどのCloud Runで作成したサービスのURLに更新しましょう。
このとき、/slack/events
をパスに追加することをお忘れなきよう。
(Cloud RunのサービスのURLがhttps://xxxxxxxxxxx.run.app
の場合、https://xxxxxxxxxx.run.app/slack/events
と更新する。)
本番環境で動作確認
さて、ここまでで全ての作業がおわりました。最後にもう一度、Slackで/kiminikimeta
を試してみましょう。
ちゃんとランダムにユーザーが選ばれていますね!!
おわりに
今回はBolt on Docker + Cloud RunでSlash commandを作ってみました。
Dockerは元々使っていたのであれですが、Bolt & Cloud Runは初めましてだったにもかかわらず意外とスムーズにできた印象です。
Boltはドキュメントがそれなりにまとまっているし、console.logでデバッグしながらやればいろいろなことができそうですね。
Cloud Runはかなり直感的に使うことができるとわかりました。お値段次第ですが、これはけっこうおすすめかもしれない。
今回のOutput
今回のBolt on DockerのアプリはGithubにあげてます。気になる方は参考にしてください!