AWS SAM を利用して Slack BOT を構築する

はじめに

会社で Slack の管理タスクやそれ以外のルーティンワークを自動化するために Slack bot を作りたくなりました。今回 AWS SAM から Amazon API Gateway x AWS Lambda を構築して、Slack bot を作成してみたので、手順を残します。

モチベーション

Slack のスラッシュコマンドを利用したかったため、何らかのエンドポイントを用意する必要がありました。会社で AWS を利用しており、可能な限り AWS 上で完結させたいと思いました。この要件を実現するために EC2 の利用が考えられますが、無料枠は別で使っていたためこのためにサーバー費がかかるのは避けたいと思いました。そのため API Gateway を利用したサーバーレス構成で Slack bot 作れば良さそうと考え、今回のアーキテクチャ構成を取ることにしました。

AWS SAM とは

あまり聞き馴染みのないサービスと個人的に思っているので、簡単に AWS SAM について紹介します。AWS Serverless Application Model (AWS SAM) とは、サーバーレスアプリケーションを構築しやすいよう、CloudFormation を拡張してくれているサービスです。AWS SAM 用に拡張されたインフラの定義ファイルを、AWS SAM CLI により CloudFormation テンプレートファイルに変換しデプロイすることができます。また、ローカルでの Lambda 関数のビルドや、今回は利用しませんがローカル環境での開発のために、API や Lambda 関数そのものをローカルで実行する機能を備えています。

手順

本章ではデフォルトの機能では提供されていない、入力をオウム返しする /echo スラッシュコマンドの作成を通じて手順を説明します。

構成の全体像

今回は以下のようなリソース構成を目指します。

architecture

Slack ユーザーによって、予め登録しておいたスラッシュコマンドが実行されると、Slack アプリが設定しておいたエンドポイントにリクエストを送信します。そのリクエストを API Gateway で受け取り、API Gateway が本処理へのルーティングの役割を担う Lambda 関数を起動します。この Lambda 関数がスラッシュコマンドに対応する本処理を担う別の Lambda 関数を呼び出します。ルーティング用の Lambda 関数は別の Lambda 関数を呼び出した後、その Lambda 関数の終了を待たず即座にリクエスターに 200 を返し終了します。

API Gateway がスラッシュコマンドの本処理に対応する Lambda 関数を直接呼び出さない理由としては、Slack スラッシュコマンドがタイムアウトしてしまう問題を解決するためです。 下記のドキュメントに記載の通り、Slack のスラッシュコマンドではエンドポイントから 3 秒以内にレスポンスを受け取らなかった場合にはタイムアウトとなります。

https://api.slack.com/interactivity/slash-commands#responding_basic_receipt

This confirmation must be received by Slack within 3000 milliseconds of the original request being sent, otherwise a Timeout was reached will be displayed to the user.

実際に API Gateway から呼び出された Lambda 関数内で本処理を行う構成を試したのですが、簡単な処理でも Lambda 関数がコールドスタートする場合には頻繁にタイムアウトが返却されることを確認しました。また、リクエストの検証や前処理をルーティング用の Lambda 関数に集約し共通化できるという点においても、API Gateway からリクエストを受け取る Lambda 関数を一つにすることは有用と考えました。

環境構築

まずはローカルに AWS SAM をセットアップします。基本的にこちらの手順を実施するだけです。

ドキュメントにも記載されていますが、注意点としては AWS SAM CLI においても AWS CLI と同様に .aws/ のクレデンシャルを利用するためクレデンシャルの設定が必要なこと、ローカルで Lambda 関数のビルドのために Docker のインストールが必要です。

Slack 側の設定

ボットを追加したい Workspace でスラッシュコマンドが利用できるよう、Workspace に Slack app を作成・設定します。こちらの URL の「Create New App」を選択することで作成できます。 作成した後、左ペイン Basic Information をクリックし「App Credentials」に記載の「Signing Secret」をメモしておきます。これは、後ほど Lambda 関数側で、Slack アプリから送られたリクエストが本当に自身の Workspace から発生したものかどうかを検証するために利用します。

API Gateway の構築

ソースコードは以下のようなディレクトリ構成で構築していきます。特段なにかのベストプラクティスに従っている訳ではないので構成は自由に変更して良いと思います。

  • src : Lambda にデプロイされるコード群
    • layers : Lambda 関数共通で利用されるコード
    • root : API Gateway からリクエストを直接受け取る Lambda 関数のコード
    • echo : /echo スラッシュコマンドに対応する本処理に対応する Labmda 関数のコード
  • template.yaml : AWS SAM 形式で作成されたインフラの定義ファイル (テンプレートファイル)

AWS SAM のテンプレートファイル

テンプレートファイルの全体像はこちらのファイルより参照ください。

template.yaml の共通設定

template.yaml を記載するにあたって、共通設定を説明していきます。

まずは Parameters セクションです。Parameters セクションは AWS SAM テンプレートからリソースを作成する際に引数として渡すことのできるパラメータを指します。 今回はパラメータとして、Slack アプリからのリクエストを検証する Signing Secret を渡せるようにします。

Parameters:
  SlackSigningSecret:
    Type: String

続いて、リソース毎の共通設定を Globals セクションに記載します。今回は Lambda 関数を複数個作成しますが、それぞれの Lambda のランタイムやタイムアウト時間をそれぞれに設定するのは冗長なので共通化します。

Globals:
  Function:
    Timeout: 3
    Runtime: nodejs14.x

Lambda Layers の作成

Lambda Layers を利用して、複数の関数で利用する util 関数を作成します。今回は Slack に関する操作を一つの Lambda Layer としてライブラリのように利用したいため、slackbot-layer を作成します。slackbot-layer レイヤーのディレクトリ構成は以下です。

slackbot-layer/
├── node_modules
├── package-lock.json
├── package.json
└── slackbot-utils
    ├── index.js
    └── package.json

slackbot-utils/index.js には 3 つの関数が定義されています。

  • verifySignature : API Gateway のから受け取ったリクエストイベントとシークレットを受け取り、リクエストが正しい Slack アプリから来ているかどうかを検証する関数
  • parsePayload : Slack アプリから受け取った、リクエストボディをパースする関数
  • responseMessage : 引数として受け取った Slack のレスポンス URL に対して、メッセージを返す関数

また、Lambda Layer のリソースの定義は下記の通りです。

SlackBotUtilLayer:
  Type: AWS::Serverless::LayerVersion
  Properties:
    ContentUri: src/layers/slackbot-layer
  Metadata:
    BuildMethod: nodejs14.x

slackbot-layer/ 配下は特殊なディレクトリ構成ですが、./slackbot-layer/package.json の中身を見れば分かる通り、目的は slackbot-utils の中身が node_modules に入るよう調整しています。

./slackbot-layer/package.json の中身

{
  "name": "slackbot-layer",
  "version": "0.0.0",
  "dependencies": {
    "slackbot-utils": "file:slackbot-utils"
  }
}

本筋とは脱線しますが設定の意図を詳細に説明します。 こちらのディレクトリ構造を AWS SAM CLI を通してビルドした後 Lambda Layer としてアップロードした際には下記のような構成になります。

nodejs/
├── node_modules/
├── package-lock.json
├── package.json
└── slackbot-utils
    ├── index.js
    └── package.json

下記のドキュメントの記載の通りこれが Lambda 関数の実行環境の /opt 配下に展開されるようです。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html#configuration-layers-path

Lambda extracts the layer contents into the /opt directory when setting up the execution environment for the function.

また、Lambda Layer では下記のドキュメントに記載の通り、ランタイム毎に読み込むパスが異なります。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html#configuration-layers-path

Node.js ランタイムでは /opt/nodejs/node_modules を読み込むとあります。 つまり、直でパスを指定しない限りは node_modules の中に読み込みたいライブラリを入れておかないといけないものと理解したため、./slackbot-layer/package.json にローカルの slackbot-utils を dependency として加え、node_modules に含まれるようにしました。これにより、Lambda Layer を読み込んだ Lambda 関数側で const { <関数名> } = require('slackbot-utils'); のように自作ライブラリをロードできるようになります (もっと素直な方法があれば乗り換えたい)

Lambda 関数の作成

API Gateway からリクエストイベントを受け取り本処理を行う Lambda 関数を呼び出すための Lambda 関数を作成していきます。 ソースコードはこちらです。

ポイントは以下です。

  • Lambda 関数を呼び出すために、本処理用の Lambda 関数の名前を環境変数から注入しています。
  • リクエストを受け取るとまず、slackbot-utils の verifySignature 関数を呼び出しリクエストを検証します。
  • Lambda 関数を呼び出す時には、InvocationType に Event を指定します。これにより、Lambda 関数を非同期に呼び出すことができ、呼び出した Lambda 関数の終了を待たずに本 Lambda 関数は終了できます。

また、リソースの定義は以下です。AWS::Serverless::Function という AWS SAM で利用できるリソースタイプを利用します。 インラインで解説を入れています。

RootFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: src/root/
    Handler: index.lambdaHandler
    Layers:
      - !Ref SlackBotUtilLayer # 前述した Lambda Layer を利用できるよう指定します。
    Policies:
      - LambdaInvokePolicy: # 他の Lambda 関数を利用できるよう権限を付与します。
          FunctionName: !Ref EchoFunction
    Environment: # 該当の Lambda 関数で利用する環境変数を定義します。
      Variables:
        ECHO_FUNCTION_NAME: !Ref EchoFunction
        SIGNINGS_SECRET: !Ref SlackSigningSecret
    Events:
      # Lambda 関数が呼び出されるイベントを指定します。/* でも問題ありませんが、不要に Lambda が呼び出されることを防ぐため受け取り可能なパスだけを指定します。
      Echo:
        Type: Api
        Properties:
          Path: /echo
          Method: post

AWS SAM によって本来の CloudFormation を利用する場合と異なり、サーバーレスアプリケーション用に簡易な記述で設定が可能となっています。 例えば、LambdaInvokePolicy によって、Lambda 関数の実行権限の付与に本来 IAM ロールを指定する必要がある部分を Lambda 関数のリソース名から自動生成してくれます。これは AWS SAM ポリシーテンプレートといい、他にも DynamoDB へのアクセスを簡易に定義するオプションが用意されています。

また、Events によって Lambda 関数と API Gateway の紐付けも用意となります。加えて、API Gateway のリソース定義を明記しなくても、該当のパスを持つ API Gateway が自動生成されます。

呼び出される側の Lambda 関数のリソース定義は簡単ですが、下記に記載します。

EchoFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: src/echo
    Handler: index.lambdaHandler
    Layers:
      - !Ref SlackBotUtilLayer

デプロイ

作成したテンプレートを元に実際に AWS 環境にデプロイします。まず、AWS SAM CLI の build コマンドを利用して、テンプレートファイルの変換と Lambda 関数のビルドを行います。

sam build

続いて、実際に AWS 環境にデプロイを行います。sam deploy コマンドを実行すればよいのですが、テンプレートファイルのパラメータや Lambda 関数を一時的に格納する S3 バケット等複数のコマンドライン引数を指定する複数指定する必要があり、少し面倒です。

解決策として sam deploy --guided を実行することによって、必要なコマンドライン引数をインタラクティブに回答していくことができます。一度実行するとデフォルトで samconfig.toml というファイルが生成され指定したコマンドライン引数を保存してくれます。再デプロイの際には ビルド後 sam deploy を実行するだけでデプロイができます。注意点としては samconfig.toml は秘匿情報が含まれるため、GitHub 等に push しないようにしてください。

デプロイに成功すると、API Gateway のエンドポイントが表示されます。

--------------------------------------------------------------------------------------
Outputs
--------------------------------------------------------------------------------------
Key                 ApiEndpoint
Description         Set this endpoint as Slack slash command endpoint
Value               https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
--------------------------------------------------------------------------------------

Successfully created/updated stack - slack-bot-template in ap-northeast-1

Slack コマンドの作成

エンドポイントのデプロイが完了したので、Slack 側にスラッシュコマンドを設定していきます。左ペイン「Basic Information」より Install your app から Workspace にアプリを追加します。また、同様に左ペインより「Slack Commands」を選択し、「Create New Command」をクリックします。表示されるフォームに下記のように設定を加えます。

  • Command : /echo (Slack のスラッシュコマンド名)
  • Request URL : https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/echo (AWS SAM にて作成したエンドポイント)

実行テスト

これにより、Slash コマンドの作成が完了しました。実際に Slack よりテストをしてみます。Slack アプリが参加しているチャンネルにて /echo test というメッセージを追加します。そのまま、メッセージが返却されます。

result

作成したコマンド

余談ですが、会社で作成したコマンドには以下があります。

  • /list-channel <Slack ユーザー名> : 引数に入力された Slack のユーザーが参加しているプライベートチャンネルを一覧します
  • /word <単語> : 予め登録されている単語帳より指定された単語を返します。
  • /word add : 単語帳に新しい単語を登録するフォームを表示します。

おわりに

前提知識は非常に多いと思いますが、AWS SAM を利用することで、比較的少ないコードで運用のしやすい Slack bot が作成できたと思います。

まだ、β 版ですが、AWS SAM で TypeScript を利用した Lambda 関数のビルドもサポートされたようです。 https://aws.amazon.com/jp/blogs/compute/building-typescript-projects-with-aws-sam-cli/

大きいプロダクトになると AWS CDK の方が運用しやすいと思いますが、個人的な感覚としては AWS CDK の場合はコード設計等、初期構築のオーバーヘッドが大きいかなと思うため、小さい Slack ボット程度なら AWS SAM で構築するのも有用かと感じました。