API Gateway + LambdaでSlackのCustom Commandを作る

こんにちは、AWS担当のwakです。先日このような記事を書いたので、表題通りAPI Gateway + Lambdaで実装するサンプルを作ります。

tech.sanwasystem.com

f:id:nurenezumi:20160613190357j:plain

鋭い眼光でエラーを見逃さない猫(多分)

はじめに

暇なWebサーバーは無駄

SlackのCustom Commandsは、言ってみればユーザーの操作に応じてWebサーバーにリクエストを送り、ごく短時間で終わる処理をしてもらってその結果を受け取るものです。少なければ1日数回、どんなに多くても1日に数百回のこのコマンドのためだけにWebサーバーを用意して24時間待ち受けを行うなんて無駄なことはしたくありません。API Gateway + Lambdaならこのような目的にぴったりで、EC2のt1.microインスタンスなどよりもずっと安価です。

何か作ろう

弊社ではCloudWatchで何かアラートが出たとき・復帰したときにそれぞれSlackへ通知する仕組みが既に稼働中です。ただ、境界値付近で値が行ったり来たりすると通知が何度も飛んできて、「今出ている警告は何か」を見失うことがありました。そこで、今回は【現在ALARM状態になっているメトリクスを取得して一覧表示する】コマンドをAPI Gateway + Lambdaで作ることにします。

Custom Command作成

上述の記事のように、まずはCustom Commandを新規に作成し、トークンを取得します。URLは未定なので未設定のまま置いておきます。

IAMロール作成

まずLambdaを実行するためのロールを作ります(既にあるなら不要です)。今回はCloudWatchに対する権限(読み取り専用で十分)が必要です。Slackから渡されるトークンはコードに埋め込むことにしましたが、もしDynamoDBで管理するのであればDynamoDBの読み取り権限も必要になります。

f:id:nurenezumi:20160606102609p:plain
f:id:nurenezumi:20160606102645p:plain

Lambda作成

単にCloudWatchのメトリクスを取得して返すだけの簡単なコードになりました。response_typein_channelを入れておくと、コマンドの実行結果を他のユーザーも見ることができるようになります。8行目にあるxxxxxxxxxxxxxxxxxxxxxxxxはSlackから与えられたトークンで置き換えます。

"use strict";

var AWS = require("aws-sdk");
var cloudwatch = new AWS.CloudWatch();

exports.handler = function(event, context) {
    
    if (event.token !== "xxxxxxxxxxxxxxxxxxxxxxxx") {
        context.fail("トークンが変です");
        return;
    }

    cloudwatch.describeAlarms({StateValue: "ALARM"}, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            context.succeed(err);
        } else {
            if (data.MetricAlarms.length === 0) {
                context.succeed({response_type: "in_channel", text: `状態がALARMの監視項目はありません。`});
                return;
            }
            
            context.succeed({
                response_type: "in_channel",
                text: `状態がALARMの監視項目が${data.MetricAlarms.length}件あります。`,
                attachments : data.MetricAlarms.map(x => {
                    return {
                        color : "#FF8888",
                        text : [
                            `名前: ${x.AlarmName}`,
                            `内容: ${x.AlarmDescription}`,
                            `状態: ${x.StateReason}`,
                            `ALARMになった時刻: ${x.StateUpdatedTimestamp}`
                        ].join("\n")
                    };
                })
            });
            return;
        }
    });
}

念のためパラメーターのtokenだけセットしてテスト実行してみましょう。どちらかの結果が戻ってくれば成功です。

f:id:nurenezumi:20160613184539p:plain

API Gatewayの準備

API作成

まずAPI Gatewayのトップ画面で「Create API」ボタンを押し、New APIを選んで新規APIを作ります。名前は変えられないので慎重に行きましょう。

f:id:nurenezumi:20160531155032p:plain
Resources作成

要するにURL設計です。Create Resourceで好きなだけ階層を掘り、掘ったところでCreate Methodを選んでGETやPOSTを作ります。それぞれのメソッドごとに別々の処理が作れます。

今回はGETでSlackに呼び出してもらうことにします。これだけ作ったら、

f:id:nurenezumi:20160613193305p:plain

Integration Typeとして"Lambda Function"を選び、先ほど作成したLambda functionを選択します。

Method Execution設定

ここからはAPI GatewayとLambdaをどのように繋げるかを設定していきます。つまり、全体として

  1. API Gatewayは外部からのリクエストを受け取る
  2. Lambdaはパラメーターとして1件のJSONオブジェクトを受け取り、1件のJSONオブジェクトを返す関数
  3. API Gatewayは何らかのデータをレスポンスとして返す

という処理の流れがあるわけですが、1→2, 2→3の間には何らかの変換ルールが必要です(後者ではもともとJSONをそのまま返すというルールが設定されています)。これを設定します。

Method Request設定

API呼び出し元から与えられる想定のパラメーターを設定します。数が多いのですがぽちぽち入力します(今回はtokenだけでも構いません)。

f:id:nurenezumi:20160606133558p:plain

Method Request設定

Body Mapping Templatesの設定で、API呼び出し元から与えられたパラメーターをどのような形でLambda functionに渡すかを決めます。

{
  "channel_name" : "$input.params('channel_name')",
  "user_id"      : "$input.params('user_id')",
  "command"      : "$input.params('command')",
  "text"         : "$input.params('text')",
  "team_id"      : "$input.params('team_id')",
  "token"        : "$input.params('token')",
  "channel_id"   : "$input.params('channel_id')",
  "team_domain"  : "$input.params('team_domain')",
  "response_url" : "$input.params('response_url')",
  "user_name"    : "$input.params('user_name')",
  "apiId"        : "$context.apiId"
}

テスト実行する

テスト実行しましょう。tokenにトークンを入力してテストボタンを押します。先ほどLambdaを実行したときと同様の結果が返ってくれば成功です。

デプロイする

これで動作に問題ないことが確認できたのでデプロイします。これでURLが決まります。

Slackの設定

Slackに戻り、必要に応じて項目の設定を行います。

  • URLの設定(API Gatewayで決まったもの。これは必須)
  • Customize Name(BOTが発言するときの名前。分かりやすい名前にしましょう)
  • Customize Icon(BOTが発言するときのアイコン。Custom Emojiは使えないので、使いたければ画像ファイルを再度アップロードする必要があります)
  • Autocomplete help textの設定(Show this command in the autocomplete listにチェックを入れるとオートコンプリートが使えるようになります

これでもう終わりです。ブラウザからでもスマホからでも、コマンドを実行するだけでAWSAPIが実行されて結果が得られるようになりました。

まとめ

実に簡単にCustom Commandが、それもサーバーレスで実現できました。ややこしいことはAWSが引き受けてくれるので、サーバーが落ちたらどうしようといったことを考える必要がありません。お手軽で効果がある、コストパフォーマンスの高いコードはどんどん書いていきたいものですね。それでは。