以下の内容はhttps://www.kwbtblog.com/entry/2019/03/21/000001より取得しました。


AWS Lambda + Serverless Framework でサクッと Slack Bot (ボット)を作る方法

AWS Lambda と Serverless Framework を使って、簡単なSlack Bot(ボット)を作ってみたので、その作業メモです。

Slack Botの作成は手順が多く手間なのですが、AWS Lambdaを使うとサーバー周りのことを気にしなくて済みます。

また、Lambdaの開発にServerless Frameworkを使うと、面倒なAWSコンソールでの作業が不要になるので、割と手軽に作成できてオススメです。

Slack Botの概要的な説明とGUI作業は別記事にしましたので、Slack Botが全く初めての場合は、先にこちらを読んだ方が分かりやすいかと思います。

www.kwbtblog.com

本記事は主に、実装方法についてのメモになります。

Slack Bot作成のための要件

Slack Botを作るには、いくつか満たさないといけない要件があるので、その要件と解決方法を中心に説明していきます。

URLとWebサーバーを用意する

Slack Botとはざっくり言うと、Slackのワークスペース上にいるBotユーザーに対して、メッセージ送信などの何らかのアクションを起こすと、Slackから事前に登録していたURLに、そのアクション内容を通知してくれる仕組みです。

つまり、Slack Botとは、BotがSlackでのイベント収集窓口となり、Slackで起こったイベントを、指定URLへWebhookする仕組みです。

URLはBot開発者が用意する必要があるのですが、ドメイン取得やWebサーバーの手配等は結構面倒です。

AWS Lambdaだと、URLはAmazonが発行してくれるし、サーバーの用意も不要です。しかも使っていない時はお金がかからないので、Lambdaを使ってSlack Botを作ることにしました。

URL確認を実装する

SlackにURLを登録する際、そのURLが本当にそのBot用のURLなのか調べるため、SlackがURLに確認メッセージを送ってきます。 そして、そのメッセージに対して正しいレスポンスが返せて初めて、そのURLがSlackに登録されます。

Slackが送ってくるメッセージは下記のようなものです。

{
    "token": "xxxx",
    "challenge": "xxxx",
    "type": "url_verification"
}

サーバー側では、下記のような処理を行います。

  • URLは外部にさらされることになるので、まず「token」がBotのものかをチェックして認証します。
  • SlackからはURLに対して送ってくるのは、確認のメッセージだけでなく、Botからのイベントも送られてきます。URL確認メッセージは「type」が「url_verification」となるので、チェックします。
  • それらが正しければ、「challenge」の値を応答します。

実装は下記のようになります。

index.ts

exports.handlerHttp = async (event: any, context: any) => {

    try {
        if (event.body.token !== SLACK_VERIFICATION_TOKEN) {
            throw new Error('ERROR : invalid verification token');
        }

        // url verify
        if (event.body.type === 'url_verification') {
            return { challenge: event.body.challenge };
        }

        console.log('END : do nothing');
    } catch (err) {
        console.error(err);
    }
};

注意:必ずコード200を返す

途中処理がエラーになった場合でも、catch()で受けてthrowはせず、必ずコード200で成功応答するようにしています。

SlackがURLにアクセスしてきて、コード200以外で応答した場合、Slackはアクセスに失敗したと認識し、リトライを行います。これはURL確認メッセージの場合のみでなく、イベント送信の場合も同様です。

そしてリトライが一定回数以上になると、それ以降はSlackからの送信が行われなくなります。

なので、URLにアクセスしてきた後の処理内容に関わらず、Slackには必ずコード200で返答するようにして、Slackからのイベント送信が停止しないようにします。

イベント処理を実装する

無事URL登録が済むと、BotからURLにイベントが送られるようになります。

送られてきた情報がイベントかどうかは「type」が「event_callback」かを見て判断します。

イベントには、どのアプリのイベントかを識別する「api_app_id」が付与されているので、「api_app_id」が正しいものかをチェックします。

実装は下記のようになります。

exports.handlerHttp = async (event: any, context: any) => {

    try {
        if (event.body.token !== SLACK_VERIFICATION_TOKEN) {
            throw new Error('ERROR : invalid verification token');
        }

        // url verify
        if (event.body.type === 'url_verification') {
            return { challenge: event.body.challenge };
        }

        if (event.body.api_app_id !== SLACK_APP_ID) {
            throw new Error('ERROR : invalid app id');
        }

        // slack event
        if (event.body.type === 'event_callback') {
            return;
        }

        console.log('END : do nothing');
    } catch (err) {
        console.error(err);
    }
};

ここではreturnだけ返して、イベントに対して何もしていません。

どんなメッセージが届こうとも、前述のとおりコード200の応答をするので、リトライは発生せず、Slackから同じイベントが2回以上届くことはないのです。

3秒以内に応答する

Slackはイベントを送って、3秒以内に応答がなかった場合、Slackはアクセスに失敗したと認識し、リトライを行います。そして前述のとおり、リトライが一定以上続くとイベント送信を停止してしまいます。

3秒なんて、ちょっとした処理を書いただけでもすぐに超えてしまいます…。なので、Slackからイベントが届いたら、まずSlackに返答だけしてしまって、本来のやりたかった処理は、返答した後でゆっくりやるようにする必要があります。

その仕組みとして、ここでは、AWSのSimple Queue Service(SQS)を使うことにしました。

AWS Simple Queue Service(SQS)とは?

AWS SQSとは、メッセージを一時的に保存してくれるサービスです。メッセージをSQSに保存して、後からそのメッセージを取りにいくことができます。

SQSとLambdaは簡単に連携して使うことができます。SQSとLambdaの関数を連携させると、Lambdaは定期的にSQSにメッセージが届いていないかチェックし、メッセージが届いていたら、そのメッセージを引数に入れて、連携しているLambda関数を呼びだしてくれます。

Slackからイベントが送られたら、イベント内容をSQSに保存して、まずSlackに応答して関数を終わります。その後、Lambdaは定期的にSQSをチェックしているので、先程保存したイベントを見つけて、Lambdaを呼び出だします。

これにより、Slackには応答して終了しつつ、処理を次のLambdaに非同期的に渡すことができるようになります。

実装は下記のようになります。

exports.handlerHttp = async (event: any, context: any) => {

    try {
        if (event.body.token !== SLACK_VERIFICATION_TOKEN) {
            throw new Error('ERROR : invalid verification token');
        }

        // url verify
        if (event.body.type === 'url_verification') {
            return { challenge: event.body.challenge };
        }

        if (event.body.api_app_id !== SLACK_APP_ID) {
            throw new Error('ERROR : invalidate app id');
        }

        // slack event
        // send sqs and response
        if (event.body.type === 'event_callback') {
            const params = {
                QueueUrl: AWS_SQS_URL,
                MessageBody: JSON.stringify(event.body)
            };
            return await AWS_SQS.sendMessage(params).promise();
        }

        console.log('END : do nothing');
    } catch (err) {
        console.error(err);
    }
}

exports.handlerSqs = async (event: any, context: any) => {
    try {
        const sqs_msg = JSON.parse(event.Records[0].body);
        return;
    } catch (err) {
        console.error(err);
    }
};

イベント内容をSQSに送ってそのままコード200で終了しています。

そうすると、SQSからLambda関数「handlerSqs()」が呼び出されるようになります。ここでは何もせず、returnを返して正常終了させています。

SQSから呼び出されたLambda関数が正常終了した場合は、SQSからメッセージが削除されるので、同じメッセージが何度も届くことはありません。

イベントに応答する

以上で、Slack Botに最低限必要な要件は満たせました。

後はSQSから呼び出されたLambda(handlerSqs())の中で、好きな処理を書くだけです。

Slackに対して返信等するのであれば、Slack APIを使って行います。Botからの返信っぽく見せるのならば、Botユーザーのtokenを使ってSlack APIを呼び出すとそれっぽくなります。

例えば、Botにメッセージを送って、Botからオウム返しさせるのは下記のようになります。

async function postMessage(msg: any) {

    const event = msg.event;

    if (!event.bot_id && event.text && event.channel_type === 'im') {

        const url = `https://slack.com/api/chat.postMessage`;
        await axios.request({
            headers: {
                authorization: `Bearer ${SLACK_BOT_TOKEN}`
            },
            url,
            method: 'POST',
            data: {
                channel: event.channel,
                text: event.text,
                as_user: true
            }
        });
    }
}

exports.handlerSqs = async (event: any, context: any) => {
    try {
        const sqs_msg = JSON.parse(event.Records[0].body);
        return await postMessage(sqs_msg);
    } catch (err) {
        console.error(err);
    }
}

AWS Lambda + Serverless Framework でサクッと Slack Bot (ボット)を作る方法

せっかくなのでBotアプリっぽく、Googleの翻訳APIを使って、話しかけると英語に翻訳して返事するようにしてみました。

async function translateMessage(msg: any) {

    const event = msg.event;

    if (!event.bot_id && event.text && event.channel_type === 'im') {
        // translate into english
        const [translation] = await GCP_TRANSLATE.translate(event.text, 'en');

        const url = `https://slack.com/api/chat.postMessage`;
        await axios.request({
            headers: {
                authorization: `Bearer ${SLACK_BOT_TOKEN}`
            },
            url,
            method: 'POST',
            data: {
                channel: event.channel,
                text: translation,
                as_user: true
            }
        });
    }
}

exports.handlerSqs = async (event: any, context: any) => {

    try {
        const sqs_msg = JSON.parse(event.Records[0].body);
        return await translateMessage(sqs_msg);
    } catch (err) {
        console.error(err);
    }
}

AWS Lambda + Serverless Framework でサクッと Slack Bot (ボット)を作る方法

以上、もろもろを踏まえた実装は下記のようになります。

認証に必要な情報はLambdaの環境変数にセットして渡します。

index.ts

const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
const SLACK_APP_ID = process.env.SLACK_APP_ID;
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
const SLACK_USER_TOKEN = process.env.SLACK_USER_TOKEN;
const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID;
const AWS_SQS_URL = process.env.AWS_SQS_URL;

import axios from 'axios';
import * as AWS from 'aws-sdk';
import { Translate } from '@google-cloud/translate';

const AWS_SQS = new AWS.SQS();
const GCP_TRANSLATE = new Translate({ projectId: GCP_PROJECT_ID });

async function translateMessage(msg: any) {

    const event = msg.event;

    if (!event.bot_id && event.text && event.channel_type === 'im') {
        // translate into english
        const [translation] = await GCP_TRANSLATE.translate(event.text, 'en');

        const url = `https://slack.com/api/chat.postMessage`;
        await axios.request({
            headers: {
                authorization: `Bearer ${SLACK_BOT_TOKEN}`
            },
            url,
            method: 'POST',
            data: {
                channel: event.channel,
                text: translation,
                as_user: true
            }
        });
    }
}

exports.handlerHttp = async (event: any, context: any) => {

    try {
        if (event.body.token !== SLACK_VERIFICATION_TOKEN) {
            throw new Error('ERROR : invalid verification token');
        }

        // url verify
        if (event.body.type === 'url_verification') {
            return { challenge: event.body.challenge };
        }

        if (event.body.api_app_id !== SLACK_APP_ID) {
            throw new Error('ERROR : invalidate app id');
        }

        // slack event
        // send sqs and response
        if (event.body.type === 'event_callback') {
            const params = {
                QueueUrl: AWS_SQS_URL,
                MessageBody: JSON.stringify(event.body)
            };
            return await AWS_SQS.sendMessage(params).promise();
        }

        console.log('END : do nothing');
    } catch (err) {
        console.error(err);
    }
}

exports.handlerSqs = async (event: any, context: any) => {
    try {
        const sqs_msg = JSON.parse(event.Records[0].body);
        return await translateMessage(sqs_msg);
    } catch (err) {
        console.error(err);
    }
}

Serverless Frameworkを使ってLambdaをデプロイ

プログラムはできたので、後はLambdaにアップして、API Gatewayを設定して、SQSを設定して、Lambdaにマッピングするだけです……。と言いたいのですが、これがかなり面倒です……。

なので、Serverless Frameworkを使うことにしました。

Serverlessの詳しい使い方は省略しますが、今回のLambdaをデプロイする方法は、ざっくりとは下記のとおりです。

  • npm install -g serverlessでServerless Frameworkをインストール
  • 「index.js」が置かれているフォルダにServerlessの設定ファイル「serverless.yml」を置く
  • Lambdaに設定する環境変数を、ローカルの環境変数に設定する
  • serverless deployでデプロイ

設定ファイルは下記になります。

serverless.yml

service: slack-bot-test

provider:
  name: aws
  iamManagedPolicies:
    - 'arn:aws:iam::aws:policy/AmazonSQSFullAccess'
  endpointType: REGIONAL
  runtime: nodejs8.10
  stage: dev
  region: ap-northeast-1
  deploymentBucket:
    name: <アップ先S3バケット名>
  environment:
    SLACK_VERIFICATION_TOKEN: '${env:SLACK_VERIFICATION_TOKEN}'
    SLACK_APP_ID: '${env:SLACK_APP_ID}'
    SLACK_BOT_TOKEN: '${env:SLACK_BOT_TOKEN}'
    SLACK_USER_TOKEN: '${env:SLACK_USER_TOKEN}'
    GOOGLE_APPLICATION_CREDENTIALS: '${env:GOOGLE_APPLICATION_CREDENTIALS}'
    GCP_PROJECT_ID: '${env:GCP_PROJECT_ID}'
    AWS_SQS_URL:
      Ref: slackBotTestSqs

resources:
  Resources:
    slackBotTestSqs:
      Type: 'AWS::SQS::Queue'
      Properties:
        QueueName: 'slackBotTestSqs'

package:
  exclude:
    - package.json
    - package-lock.json

functions:
  slack-bot-test-http-function:
    name: slack-bot-test-http-function
    handler: index.handlerHttp
    events:
      - http:
          path: slack/bot
          method: post
          integration: lambda

  slack-bot-test-sqs-function:
    name: slack-bot-test-sqs-function
    handler: index.handlerSqs
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - slackBotTestSqs
              - Arn
          batchSize: 1

Severlessの設定ファイルはシンプルなので、見れば何となくやっていることが分かると思うのですが、簡単に補足説明します。

  • Lambdaの環境変数は「env:~」を使って、ローカルの環境変数の値がセットされるようにしています。
  • SQSを使うので、Lambdaの実行ポリシーにSQSアクセス権を追加しています。
  • SQSはServerlessは自動では作ってくれないので「resources」セクションで明示的に作成しています。
    • ちなみに「resources」セクションの記述はAWS CloudFormationのテンプレート書式がそのまま使えます。そしてSQSはCloudFormationを介して作成されます。
  • SQSのURLはSQSが作成された時に決まるので、プログラムには環境変数「AWS_SQS_URL」を通じて渡しています。
  • SQSからメッセージを読み取る際、複数メッセージをまとめて読めるのですが、「batchSize」で読み取り数を1だけにしています。
  • Google・GCPの箇所は翻訳API利用のために使用しているだけなので、削除して構いません。

感想など

以上で、Slack Botの雛形ができました。やっぱり面倒くさいですね。

Lambda・API Gateway・SQSが絡み合うので、AWSのコンソールで同じことやろうとすると大変です。

Serverless Frameworkを使うと、AWSのコンソールは一切触らずに、設定ファイルの記述だけで済んでしまうので本当に便利です。Serverless Frameworkが無かったら確実に挫折してました…。

はじめ、エラー処理と応答時間は気にせず、適当に作っててハマりました…。

問題が発生したらエラーコード返すようにしていると(例外をキャッチしないでほっとくとか)、「エラー発生->Slackリトライ->エラー発生…」のループとなり、最後に止まるということになります。

また、イベントを受けた時に、横着してそこで全ての処理を書いてしまうと、時間内に処理が終わらず、「処理中にタイムオーバー->Slackリトライ->処理中にタイムオーバー…」のループとなり、何度も同じ処理(応答)を行って、最後に止まるということになります。

あと、SQSに送れるメッセージサイズは最大256kbなので、本格運用するのであれば、投稿本文はSQSには送らず、SQSメッセージを処理するLambda内で取得し直すような仕組みにした方がいいですね。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com




以上の内容はhttps://www.kwbtblog.com/entry/2019/03/21/000001より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14