AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す

こんにちは、AWS担当のwakです。前回に続きAWS Lambdaの話をします。

f:id:nurenezumi:20160104165124j:plain

良い角度を知っている猫

【インデックス(予定)】

  1. CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
  2. AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す(今回)
  3. DynamoDBにパラメーターを入れてハードコードをなくす
  4. CloudWatchの監視結果をTwilioに流して電話をかける
  5. SlackからEC2インスタンスを起動・停止するコマンドを作る

今回の目的について

前回に続いて、Lambda (Node.js)を使ってWebサイトの監視を行います。

  • 任意のWebサイトにアクセスし、HTTP 200が返るかどうかを確かめる
  • その結果をSlackで通知する
  • この処理をN分おきに実行する(cron形式でスケジュールを設定可能)

これに似たサービスはいくらでもあるのですが、

  • cron形式でスケジュールを設定可能
  • 通知先はSlackに限らない
  • AWS Lambdaで実行されるため、サーバー障害などにも非常に強い
  • 安価
  • AWS Lambdaと仲良くなれる

といった利点があります。それではさっそく始めましょう。

Slackの準備をする

Slack側で通知を行うためにIncoming WebHooksの準備をします。こちらは前回と同じ手順で新規作成しても構いませんし、新規に作っても構いません。

Lambdaの準備をする

Management ConsoleからLambdaを選ぶと、最初にブループリント(テンプレートみたいなものですね)の選択画面が表示されます。試しに「https-request」を選ぶと、Node.jsでHTTPSアクセスを行うためのサンプルコードが入力された状態でコード編集画面へと遷移します。

f:id:nurenezumi:20151218165730p:plain

コードを入力する

元から記入されているコードを全て削除し、置き換えます。完全な形のコードはこの記事の末尾に掲載しているのでコピペしてください。sendToSlack()は前回と同じもので、Slackへ通知を行ってから処理を終了する関数です。

var https = require('https');
var http = require('http');

function sendToSlack(path, message, channel, context, otherParameters) {
    ...
}

exports.handler = function(event, context) {
   ...
}

テスト実行する

eventの値は見ていないので、適宜コードを修正して監視対象のURLとページ名(Slack通知時に使われます)、Slackのエンドポイント・通知チャンネル名を変更してテスト実行してみましょう。設定がうまく済んでいれば通知が行われるはずです。

実行設定する

Event sourcesタブを開き、"Add event source"をクリックします。"Event source type"に"Scheduled Event"をセットすると、「5分ごと」「15分ごと」のようなプリセットの他に、cron形式で入力ができるようになります。

公式ドキュメント を読めば特に難しいことはないのですが、たとえば「月曜日から金曜日の毎時5分、15分、……55分」というスケジュールで定期実行するように設定するのであればこうなります。

f:id:nurenezumi:20151225180849p:plain

cron(5/10 * ? * MON-FRI *) が指定している内容で、パラメーターは順に

  1. 5/10は毎時5分、15分、……、55分を示す書式です。0/5なら0分、5分、10分、……になります。0なら毎時0分になります。
  2. 次の *全ての時(hour) を指定しています。
  3. 次の ? は日を 指定しない ことを意味しています。ここで*(全ての)を指定してしまうと、後の曜日指定と矛盾してしまうのでエラーになります(ちょっとハマりました)。
  4. 次の * は全ての月を指定しています。ここは良いでしょう。
  5. 次のMON-FRIは曜日です。MON,WED,FRIのように列挙することもできます。
  6. 最後の * は全ての年です。

ですから、たとえば「毎日9時から18時の間、毎時0分と30分」であればcron(0,30 9-18 * * ? *)になります(今度は曜日の方を「指定しない」としています)。

ここで登録したスケジュールは、AWS Lambdaとは独立したリソースとして保存・管理されます*1。別のLambda functionで使い回すこともできるようになっています。

まとめ

作ろうと思ったら簡単に実装ができてしまいました。次回はソースコードの中の値をDynamoDBに格納し、ハードコードを排除してみます。

コード

var https = require('https');
var http = require('http');

/*
 * Slackに通知を行う。
 * path: Slackのエンドポイント。 "/services/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" の形式の文字列
 * message : 送信するメッセージ
 * context : Lambdaのコンテキスト
 * otherParameters : icon_emoji, usernameなどをキーに持った配列(省略可能)
 */
function sendToSlack(path, message, channel, context, otherParameters) {
    context = context ? context : {succeed: function(){}, fail: function(){}, done: function(){}};
    
    var options = {
        hostname: "hooks.slack.com",
        port: 443,
        path: path,
        method: 'POST'
    };

    var req = https.request(options, function(res) {
        if (res.statusCode == 200) {
            context.done();
        } else {
            var message = "通知に失敗しました. SlackからHTTP " + res.statusCode + " が返りました";
            console.error(message);
            context.fail(message);
        }
    });
    req.on('error', function(e) {
        var message = "通知に失敗しました. Slackから次のエラーが返りました: " + e.message;
        console.error(message);
        context.fail(message);
    });
    
    var parameters = otherParameters ? otherParameters : {};
    parameters.text = message;
    parameters.channel = channel;
    req.write(JSON.stringify(parameters));
    req.write("\n");
    req.end();
}

exports.handler = function(event, context) {
    console.log(event);
    
    // 証明書エラーを無視する
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

    var url = "https://www.google.co.jp";
    var title = "Googleトップページ";
    var path = "/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx";
    var channelName_error = "#channelNameError"; // エラー時の通知先チャンネル名。必須
    var channelName_ok = "#channelNameOk"; // OK時の通知先。空文字にするとOK時は通知が出なくなる
    var option = { icon_emoji : ":ghost:", username : "MYBOT" };
    
    var agent = (url.indexOf("https://") === 0) ? https : http;
    agent.get(url, function(res) {
        console.log(res);
        var message = title + ": " + url + " にアクセスして HTTP " + res.statusCode + " が返ってきました";
        var channel = (res.statusCode == 200) ? channelName_ok : channelName_error;
        if (!channel) {
            console.log(message + ". 通知はスキップします");
            context.done();
            return;
        } else {
            console.log(message);
            sendToSlack(path, message, channel, context, option);
        }
    }).on('error', function(e) {
        var message = url + "にアクセスできませんでした:\n" + e.message;
        console.error(message);
        sendToSlack(path, message, channelName_error, context, option);
    });
};
  • sendToSlack()は前回からの使い回しです。
  • 前回に続き、処理はSlack通知をもって完了します。
    • そのため、context.succeed()context.failsendToSlack()の中だけで呼んでいます。
    • それより前に呼んでしまうと、(非同期で)コンテンツを取得しに行く前に処理が終わってしまいます。

*1:そのため、スケジュール保存時に単独でARNが振られます