Slackbotをserverlessで動かしたかった


目的

これはSlackbotをserverlessで動かしたかったのだが、失敗した記録である。
こうやったらできるよなどがあったら教えて欲しい。

要件

  • Slackのイベント(新規ユーザの追加/チャンネル追加)時にbotがpostして欲しい
    • 将来的には機能を追加したい
  • Slackはフリープラン
  • bot運用にできるだけコスト(お金)をできるだけかけたくない
  • 運用にかかるコスト(OSやパッケージのアップデートなど)をできるだけかけたくない

フリープランでなければWorkflowが選択肢になる。

構成

SlackとApp間はEvent経由(HTTPS、WebSocketでない)で通信する。
Slack空のHTTPS通信はAPI Gatewayで受け、Lambdaに流す。
Lambdaで応答メッセージを生成し、レスポンスする。

Slack botのフレームワークにはSlack Boltを使用する。何となく公式がいいのではという以上の理由はない。
また、API Gateway / Lambdaの作成にはServerless frameworkを使用する。 sls deploy コマンドを実行するだけでAPI Gateway / Lambda / CloudWatch Logsを作ってくれ、アプリをデプロイしてくれる。便利。
GitHubで管理し、masterブランチへのコミット(マージ)時にGitHub ActionsでAWSにデプロイしてくれるという構成。

構築

Slack App/Bolt

新規にSlack Appを作成する。
Bolt 入門ガイド に沿って作業していくとSlack Appの作成から簡単なbotの作成までを一通り学べる。

API Gateway / Lambda とBoltの連携

seratch/serverless-slack-bolt-aws をテンプレートとして使用した。

$ serverless create \
  --template-url https://github.com/seratch/serverless-slack-bolt-aws/tree/master \
  --path hello-bolt

を実行するだけでServerless FrameworkでデプロイできるSlack Boltのbotができる。便利。

https://dev.classmethod.jp/report/developers-io-2019-slack-bolt/ の人が作っていたみたい。

Slack AppでEventを有効にし、channel_createdteam_join を有効にする。

channel_createdteam_join イベントをハンドルするコードを書いた。

app.event('team_join', async ({ event, context }) => {
  const teamJoinNotifyChannel = process.env.TEAM_JOIN_NOTIFY_CHANNEL || '#general';
  try {
    //send welcome message
    const welcomeMessage = await app.client.chat.postMessage({
      token: context.botToken,
      channel: teamJoinNotifyChannel,
      text: `ようこそ <@${event.user.id}>! 🎉 自己紹介をしてみましょう!`
    });
    console.log(welcomeMessage);
  }
  catch (error) {
    console.error(error);
  }
});

app.event('channel_created', async ({ event, context }) => {
  const channelCreatedNotifyChannel = process.env.CHANNEL_CREATED_NOTIFY_CHANNEL || '#general';
  try {
    const result = await app.client.chat.postMessage({
      token: context.botToken,
      channel: channelCreatedNotifyChannel,
      text: `#${event.channel.name} チャンネルが作られたよ!🎉`,
      link_names: true
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

ローカル環境で検証

sls offline でローカル実行し、もう一枚のターミナルで ngrok http 3000 する。
ngrokの画面に表示される https://1a2b3c4d.ngrok.ioSlack Appの設定
Event Subscriptions→New Request URL に設定する。

ここでSlackのチャンネルを作成するとbotが通知してくれる。やったね。

AWSにデプロイ

AWSにデプロイする。
せっかくなので、GitHub Actionsを使ってmasterブランチにマージされたタイミングでデプロイする。

こんな感じで GitHub Actionsの設定を追加。

.github/main.yml
name: CI

on:
  push:
    branches:
      - master
jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: install node.js
      uses: actions/[email protected]
      with:
        node-version: '12.x'
    - run: npm install
    - run: npx serverless deploy --conceal --stage prd
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
        CHANNEL_CREATED_NOTIFY_CHANNEL: ${{ secrets.CHANNEL_CREATED_NOTIFY_CHANNEL }}
        TEAM_JOIN_NOTIFY_CHANNEL: ${{ secrets.CHANNEL_CREATED_NOTIFY_CHANNEL }}

GitHubのリポジトリ→Settings→Secretsに環境変数を指定する。

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • CHANNEL_CREATED_NOTIFY_CHANNEL
  • SLACK_BOT_TOKEN
  • SLACK_SIGNING_SECRET
  • TEAM_JOIN_NOTIFY_CHANNEL

AWSは専用のユーザを作成し、アクセスキーを払い出した。
ユーザにattachするpolicyは以下を参考にした。
Customize the Serverless IAM Policy

"A simple IAM Policy template" セクションを見たら、IAMの全権限を許可してて、やってますなーという気分になるので、"An advanced IAM Policy template" を参考に変数部分を置換するのがおすすめ。

GitHub Actionsに慣れてないのでちょっと迷いながらもデプロイのフローを作れた。

ここまでは何とか進めた。

API Gateway経由でBotを実行

結論から言うと、ここがうまくいかなかった。
チャンネルを作成してもうんともすんとも言わないのだ。

SlackはAppを実行してから3秒以内にackを返さないとエラーとなる。
Handling user interaction in your Slack apps

Boltを紹介するスライドに書いてあったが、どうにかなるだろうと思ったらどうにもならなかった。
また、レスポンスはIncoming Webhookを使用するというworkaroundもあるのだが、Slack Appで払い出せるWebhookのURLはChannelを指定できない。channel_createdteam_joinであれば問題はないが、将来的に任意のpostに対してreplyしたいのでこの制限は辛い。

また、チャンネルを指定できるLegacyなWebhookのURLを払い出せるのだが、できれば1つのAppで完結したいのでこの案はボツにした。

やりたいことは大体検証できたし、他の方法を試してみたいと言うのもある。

次の案

Hubotを使用し、FargateなどのDocker環境に構築する予定。仕事でHubot使ってるので感じはつかめてるし、WebSocketのクライアントなのでインターネットにつながればどうにかなるはず。Fargateはまだ手をつけてないのでやってみたい。
Savings PlansFargate Spotも試してみたい。

ソース