Serverless Frameworkでタロットカードを引くSlack botを作った


# はじめに

自分は昔、[たろっとさん](https://twitter.com/tarot3)というTwitter Botを作っていました。このたろっとさんを復活させたいなぁと思ったのですが、TwitterのAPI申請がめんどくさそうなので、まずはSlack Bot化してみました。

# 作ったもの

- [たろっとさん for Slack](https://tarot.ikemo3.com/apps/slack/)

まだ公式アプリではありません。

実際動いているコードから機密情報や、タロットカードを引くロジック(門外不出)を除いたものをGitHubに置いています。

- [ikemo3/serverless-slack-bot-example: Slack bot example by Serverless Framework, AWS Lambda, Ruby](https://github.com/ikemo3/serverless-slack-bot-example)

# システム構成

以下の図のような構成で作りました。

![slack-tarot3-構成.png](https://qiita-image-store.s3.amazonaws.com/0/45169/5a3185ea-4632-e4c9-dc2b-84ba0f100ae7.png)

- AWS Lambdaで4つ関数を作成
  - 認可エンドポイントへのリダイレクタ
  - OAuth 2.0のコールバックエンドポイント
  - Event Subscriber
  - Reply
- アクセストークン保存用にDynamoDBを使用
- Event SubscriberでAmazon SNSにpublishし、Replyをトリガーで呼び出し
- API GatewayとRoute 53でURLを独自ドメインに紐づけ

リダイレクタを作ったのは、クライアントIDやscopeが変わってもURLを変更する必要がなく管理が楽だからです。注意点として、リダイレクトする際はHTTPステータスコードは**302**にする必要があります。

- [Building a Direct Install URL redirector](https://api.slack.com/slack-apps#building_a_direct_install_url_redirector)

> All your server needs to do is build the same URL to slack.com/oauth/authorize you would build for your on-site "Add to Slack" URL and instead of presenting it in a button, send a HTTP 302 redirect to it instead.

Event Subscriberとリプライを分けたのは、Slackの[Events API][]には、3秒以内で応答する必要があるためです。Event Subscriberとリプライを分けることで、リプライ側の処理が複雑になっても安心です。

> Your app should respond to the event request with an HTTP 2xx **within three seconds.**

# 処理の流れ

処理の流れは以下のようになります。

![slack-tarot3-フロー.png](https://qiita-image-store.s3.amazonaws.com/0/45169/89f68c47-2b8b-b4fb-b538-a35ba72b44de.png)

詳細は以下のとおりです。1〜6までがOAuth 2.0による認可コードフロー、7〜12がタロットを引くところです。

1. リダイレクタにアクセス
2. Authorizeエンドポイントにリダイレクト
3. Slack上でユーザが認証 & 認可
4. コールバックエンドポイントにリダイレクト(アプリ管理画面のOAuth & Permissions　→　Redirect URLsに登録する必要あり)
5. [oauth.access](https://api.slack.com/methods/oauth.access)でcodeとアクセストークンを交換
6. 取得したアクセストークンをDynamoDBに保存
7. ユーザが `@tarot3` と呼ぶ
8. イベントが発生し、Events Subscriberが呼ばれる(アプリ管理画面のEvent Subscriptionsに登録する必要あり)
9. Amazon SNSに通知を投げる
10. Amazon SNSをトリガーとしてReply Lambda関数が呼び出される
11. DynamoDBからアクセストークンを取得
12. タロットカードを引いてリプライを送る

# ポイント

## まず画面で操作してみる

最終的には[Serverless Framework][]を使いましたが、開発の最初はLambdaの管理コンソールで直接コードを書き、API Gatewayを手動で設定していました。ある程度コンソールで慣れてからServerless Frameworkを使うことで、設定ファイルの書き方がスムーズに理解できました。

## scope=bot

scopeはいろいろ定義されているようですが、botだけで良さそうです。

- [Scopes and permissions | Slack](https://api.slack.com/scopes)

注意しないといけないのは、scope=botの場合は**ボット専用のアクセストークンが含まれる**ことです。

- [Bot user access tokens](https://api.slack.com/docs/oauth#bots)

## DNS設定

DNS設定には[serverless-domain-manager][]というプラグインを使ったのですが、証明書の指定でエラーになりました。

原因は、ACM証明書を東京リージョンで作ったためです。この場合は `endpointType: regional` の設定を付ける必要があります。

- 参考: [API GatewayをカスタムドメインでHTTPS化する ｜ DevelopersIO](https://dev.classmethod.jp/cloud/api-gateway-custom-domain-ssl/)

## Events APIの署名検証

Events APIを使う場合、Slackから来たメッセージであることの検証を行う必要があります。その方法は以下にかかれています。

- [Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack)

概要を書くと、以下のようになります。

1. 署名用のシークレットキーを取得(アプリ管理画面の "Basic Information" にある "Signing Secret")
2. HTTPヘッダ `X-Slack-Request-Timestamp` からリクエストした時刻を取得し、現時刻と大きくずれてないことを検証(例では5分以内の誤差はOK)。これはリプレイアタックを防止するためです。
3. 署名のベースとなる文字列を `sig_basestring = 'v0:' + timestamp + ':' + request_body` のようにタイムスタンプとリクエストボディから作成します。
4. この文字列とシークレットキーより、 HMAC-SHA256 の値を計算します。
5. 4で計算した文字列の最初に `v0=` を付けた文字列が HTTPヘッダ `X-Slack-Signature` と一致することを確認します。

## Serverless Frameworkが要求するメモリ

serverless.ymlのリファレンスによれば、メモリサイズ(memorySize)のデフォルトは1024です。もったいないので変更しましょう。最小の128MBで十分でした。

- [Serverless Framework - AWS Lambda Guide - Serverless.yml Reference](https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/)

## ロールは一括で定義(個別定義はプラグイン)

Serverless Frameworkでは、IAMのロールは一括で定義します。関数ごとに個別に定義したい場合は、[serverless-iam-roles-per-function][]というプラグインを使ってください(今回は未使用)

# 追記

Slackの審査通しました。

- [タロットカードを引くSlack botを審査を通すまでにやったこと - Qiita](https://qiita.com/ikemo/items/83e0bd2e43f2dcadf498)

[Events API]: https://api.slack.com/events-api
[oauth.access]: https://api.slack.com/methods/oauth.access
[Serverless Framework]: https://serverless.com/
[serverless-domain-manager]: https://github.com/amplify-education/serverless-domain-manager
[serverless-iam-roles-per-function]: https://github.com/functionalone/serverless-iam-roles-per-function