13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Slack】OOOoO、キミにきめた!Bolt on Docker + Cloud Runでユーザーをランダムで選んでくれるSlash commandを作った話

Last updated at Posted at 2020-01-29

こんにちは。
チーム内で週一で勉強会をやっているのですが、気付いたら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人をランダムで選出してくれる感じをイメージしてます。

やること

  1. Slack appの作成
  2. Bolt on Dockerの実装
  3. ngrokでローカル環境を公開
  4. slash commandの作成
  5. ローカル環境で動作確認
  6. GCRへDocker imageを登録
  7. Cloud Runにサービスを登録
  8. slash commandのリクエストURLをCloud Runに向き先変更
  9. 本番環境で動作確認

並べてみると手順多いですね。一個一個はそんななのではりきっていきましょう!

Slack appの作成

まずはSlack appを作成します。GUIで。
Slack appはSlack apiのYour Appsのページから「Create New App」をクリックすれば作成することができます。
image.png

App NameDevelopment Slack Workspaceの入力・選択が求められるので教えてあげます。App Nameは今回は『test』にしてますが、本番利用時は『Pokemon Master』にする予定です。

これでひとまずSlack appの作成は完了です。以下のようにSlack appの詳細ページに遷移していると思います。
image.png

次にSlack appに情報へのアクセス権限を与えてあげます。
左のサイドメニューから『OAuth & Permissions』を選択します。ここでSlack botのSlackのワークスペースやチャンネルの情報へのアクセス権限を付与することができます。以下の権限を追加してあげます。
image.png
付与した権限は以下の通り。

  • 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を用意します。

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も使っていきます。

docker-compose.yml
version: '3'

services:
  app:
    build: .
    environment:
      - SLACK_SIGNING_SECRET=XXXXXXXXXX
      - SLACK_BOT_TOKEN=XXXXXXXXXX
    volumes:
      - .:/app
    ports:
      - 3000:3000

ここでSLACK_SIGNING_SECRETSLACK_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.ymlXXXXXXXXXXの部分はそれぞれの値に置き換えてください。

また、docker imageの中にDockerfiledocker-compose.ymlは不要なので含まないように.dockerignoreを記述しておきます。

.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のインストールが完了したので、実際にアプリを書いていきます。

app.js
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が送信されたチャンネルのユーザーを取得します。

app.js(抜粋)
// slash commandが叩かれたChannelに所属する全ユーザー情報を取得する
const result = await app.client.conversations.members({
  token: context.botToken,
  channel: command.channel_id
});
const members = result.members;

conversations.membersを利用するにはtokenchannelのパラメータが必須項目です。
tokenBot User OAuth Access Tokenのことですが、これはcontext.botTokenで取得することができます。
channelはslash commandが入力されたチャンネルを設定したいのですが、これはcommand.channel_idで取得することができます。
最後にレスポンスの内容からメンバーのIDの配列をmembersに格納しています。

app.js(抜粋)
// 取得した全ユーザーから今回作成している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と一致する文字列を除去しています。

app.js(抜粋)
// 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でも使い方がこちらの記事で紹介されています。
簡単にまとめると、

  1. ngrokのダウンロードページで自分のクライアントにあったファイルをダウンロードする
  2. ダウンロードしたファイルをunzipする。
    Downloadsディレクトリにダウンロードされた場合は、$ unzip ~/Downloads/ngrok.zip
  3. ngrokが解凍されるので、$PATHにファイルを移動。
    通常/usr/local/binとかだと思うのでその場合は、$ mv ~/Downloads/ngrok /usr/local/bin
  4. コマンドが使えるようになるためにターミナル再起動か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」を選択することで作成できます。

image.png

「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」して呟いてみましょう。

/kiminikimeta
Group 5.png
うわぁ!俺かぁ!

ということでこれでSlackからSlack appがslash commandを受け取り、ローカルで起動させているBolt on Dockerの処理結果をSlackにポストできていることが確認できました!

あとはこのコンテナをどこかにあげればいいだけですね。
コンテナとngrokCtrl+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です。今回はIMAGEkiminikimetaにしましょう。PROJECT-IDは仮にで「sample」とします。
つまりDocker imageのイメージ名はasia.gcr.io/sample/kiminikimetaになるということです。

Docker composeでビルドした際にこの名前のイメージができるように、docker-compose.ymlを更新します。

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」を選択しコンソールに移動したら「サービスを追加」を選択します。
image.png

ここで各種設定をしていくのですが、以下のように設定しました。

  • コンテナイメージの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サービスを作成します。

サービスの作成が完了すると以下のような画面になっていると思います。
Group 6.png
右上の「URL」という部分がこのCloud Runサービスが動くトリガーとなるエンドポイントです。

slash commandのリクエストURLをCloud Runに向き先変更

Slack appのslash commandはまだngrokのURLにリクエストを飛ばすように設定されているので、最後にエンドポイントをCloud RunのURLに更新します。

image.png
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を試してみましょう。

/kiminikimeta
/kiminikimeta
Group 7.png

ちゃんとランダムにユーザーが選ばれていますね!!

おわりに

今回はBolt on Docker + Cloud RunでSlash commandを作ってみました。
Dockerは元々使っていたのであれですが、Bolt & Cloud Runは初めましてだったにもかかわらず意外とスムーズにできた印象です。
Boltはドキュメントがそれなりにまとまっているし、console.logでデバッグしながらやればいろいろなことができそうですね。
Cloud Runはかなり直感的に使うことができるとわかりました。お値段次第ですが、これはけっこうおすすめかもしれない。

今回のOutput

今回のBolt on DockerのアプリはGithubにあげてます。気になる方は参考にしてください!

Reference

13
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?