こちらのページで基本的な使い方を把握した AWS Lambda は API Gateway のバックエンドとして設定することができます。Slack API から実行するためのエンドポイントを API Gateway で用意することによって、Hubot 等を用いない Serverless な Slack 連携が可能になります。AWS コンソールを利用した具体的な設定およびサンプルコードを示します。
関連するドキュメント
Slack API には Slack App という箱を作成してその枠組みの中で設定を管理する利用方法と、Slack API 単体で利用する方法の二つがあります。前者は比較的新しい仕様であり、ドキュメントも別々になっています。例えば Slack に外部から通知する Incoming Webhooks の最新のドキュメントはこちらにあり、昔のドキュメントはこちらにあります。Outgoing Webhooks のように、新しい Slack App の枠組みの中では利用できない API も存在しますが、これは Events API で代替可能です。本ページでは Slack App 内で利用する API を考えることにします。
AWS Lambda はイベントドリブンな処理を行うサービスであるため、常時接続が必要な WebSocket を利用した Slack API は利用できません。そこで、Slack 内から特定の条件下で AWS Lambda に HTTP POST が発生する Slack API を利用します。2017/11/14 現在のところ Slash Commands, Events API, Outgoing Webhooks が該当しますが、Outgoing Webhooks は前述のとおり Slack App 内で利用できないため今回は考えないことにします。
token
として Slack から Lambda に送信されます。Lambda はこの値をもとに Slack からのリクエストであることを確認します。/{コマンド名}
が利用できることを確認します。Slash Commands の場合と同様に Slack App を作成してから、Event Subscriptions 項目で HTTP POST 先となる Request URL や送信したいイベント種別を設定します。Slash Commands の場合と異なり適当な URL を設定することはできず、Slack からの challenge リクエストに適切なフォーマットで応答できるエンドポイントを設定しなければなりません。そのため、後述の AWS 設定を先に行う必要があります。本ページでは Slash Commands を扱うことにします。より複雑な処理を行いたい場合は Events API のドキュメントを読んで対応します。その際には以下のようなことに注意します。
subtype
bot_message を利用します。token
や team_id
を認証に利用します。event_id
の HTTP POST が最大 3 回 (直後、1分後、5分後) 送られてくるため、重複して処理しないように工夫する必要があります。
X-Slack-Retry-Reason
ヘッダーの値を確認してリトライがなされた原因を確認します。解消できない場合 X-Slack-No-Retry: 1
として HTTP POST にレスポンスを返すとリトライを抑制できます。X-Slack-Retry-Num
ヘッダーの値が 1,2,3 となっていることを利用して重複処理を回避できます。event_id
や event_time
で重複処理を回避することもできます。Slack からのリクエストに含まれる各種情報を文字列化して返すだけの Lambda 関数を作ってみます。
Lambda
を選択して、ポリシーは CloudWatch Logs 出力が必要なため AWSLambdaBasicExecutionRole
を付与します。slack-echo-command
という名称の、nodejs に関する Lambda blueprint から引用して簡略化したものです。/コマンド名
を入力してレスポンスが返ることを確認します。index.js
'use strict';
const qs = require('querystring');
const token = 'Slack App で確認した値'; // KMS で暗号化した方が安全です。
exports.handler = (event, context, callback) => {
const params = qs.parse(event.body);
const requestToken = params.token;
if (requestToken !== token) {
console.error(`Request token (${requestToken}) does not match expected`);
return callback('Invalid request token');
}
const user = params.user_name;
const command = params.command;
const channel = params.channel_name;
const commandText = params.text;
callback(null, {
statusCode: '200',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(`${user} invoked ${command} in ${channel} with the following text: ${commandText}`)
});
};