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

良い角度を知っている猫
【インデックス(予定)】
- CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
- AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す(今回)
- DynamoDBにパラメーターを入れてハードコードをなくす
- CloudWatchの監視結果をTwilioに流して電話をかける
- SlackからEC2インスタンスを起動・停止するコマンドを作る
今回の目的について
前回に続いて、Lambda (Node.js)を使ってWebサイトの監視を行います。
- 任意のWebサイトにアクセスし、HTTP 200が返るかどうかを確かめる
- その結果をSlackで通知する
- この処理をN分おきに実行する(cron形式でスケジュールを設定可能)
これに似たサービスはいくらでもあるのですが、
といった利点があります。それではさっそく始めましょう。
Slackの準備をする
Slack側で通知を行うためにIncoming WebHooksの準備をします。こちらは前回と同じ手順で新規作成しても構いませんし、新規に作っても構いません。
Lambdaの準備をする
Management ConsoleからLambdaを選ぶと、最初にブループリント(テンプレートみたいなものですね)の選択画面が表示されます。試しに「https-request」を選ぶと、Node.jsでHTTPSアクセスを行うためのサンプルコードが入力された状態でコード編集画面へと遷移します。

コードを入力する
元から記入されているコードを全て削除し、置き換えます。完全な形のコードはこの記事の末尾に掲載しているのでコピペしてください。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分」というスケジュールで定期実行するように設定するのであればこうなります。

cron(5/10 * ? * MON-FRI *) が指定している内容で、パラメーターは順に
5/10は毎時5分、15分、……、55分を示す書式です。0/5なら0分、5分、10分、……になります。0なら毎時0分になります。- 次の
*は 全ての時(hour) を指定しています。 - 次の
?は日を 指定しない ことを意味しています。ここで*(全ての)を指定してしまうと、後の曜日指定と矛盾してしまうのでエラーになります(ちょっとハマりました)。 - 次の
*は全ての月を指定しています。ここは良いでしょう。 - 次の
MON-FRIは曜日です。MON,WED,FRIのように列挙することもできます。 - 最後の
*は全ての年です。
ですから、たとえば「毎日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.failはsendToSlack()の中だけで呼んでいます。 - それより前に呼んでしまうと、(非同期で)コンテンツを取得しに行く前に処理が終わってしまいます。
- そのため、
*1:そのため、スケジュール保存時に単独でARNが振られます