AWS Lambdaでwebサイトの死活監視をして、Slackに報告する

# はじめに

## やりたかったこと
 - 定期的にwebサイトへリクエストを投げて、サーバの生死を確認したい
 - 複数の宛先をまとめてチェックしてほしい
 - 結果はSlackで確認したい
 - 報告は異常があった時だけでいいや

## スキルレベル
 - EC2とかroute53は使うが、Lambdaは未経験
 - JavaScriptは日常的に使うが、Node.js未経験
 - Slack Web APIは経験あり

## 要件
 - 任意のホスト、任意のパスにリクエストを送り応答を受け取る
 - ステータスコード200なら正常とする
 - 幾つかのサイトを1つのLambda関数で監視する　追加や変更も簡単にしたい


# 作業

## Lambdaを使用する準備

![Lambda.png](https://qiita-image-store.s3.amazonaws.com/0/151247/f74cedcf-9252-245b-6ab1-7efc554ff90d.png)


### 関数の作成

AWSダッシュボードからLambdaを開き、「関数の作成」を選択。
なにやら「https-request」というテンプレートを発見。これを使うことにしましょう。

<img width="943" alt="alive1.png" src="https://qiita-image-store.s3.amazonaws.com/0/151247/0ba3c709-a83b-780d-5ff1-961586223ca0.png">

「https-request」を選択し、次へ。


<img width="1166" alt="alive4.png" src="https://qiita-image-store.s3.amazonaws.com/0/151247/ab590fdd-8373-8ebb-e84b-34cd8be665da.png">


このLambda関数を呼び出す条件を選択するようです。

とりあえず、5分ごとに起動するように設定してみました。
慣れているcron式で記入→「cron(0/5 * * * ? *)」
意味は左から順に、

| 0/5 | * | * | * | ? | * |
|---|---|---|---|---|---|
|5分毎|毎時|毎日|毎月|曜日指定なし|毎年|
となります。
「日」と「曜日」のみOR条件になっていて、毎日にするならどちらかを指定なしにしないとバリデーションエラーで進めないので気をつけましょう。これは普通のcronでも同じ。
最後の「年」はcronにはありませんね。


ここは後からでも設定・追加できるので、よくわからなければスルーでおk。
で、次

<img width="928" alt="alive3.png" src="https://qiita-image-store.s3.amazonaws.com/0/151247/61656447-acab-3df7-0a44-7359ef6a982d.png">

ロールは適当に作っちゃいました。[ロールについてはこちら。](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_terms-and-concepts.html)
要するに、この関数を扱える権限の設定といったところ。
カンケーない人にはカンケーない項目なのでサクッと進みましょう。


# Slack Web API
Slackに投稿するためのトークンを取得します。
[@ykhirao](http://qiita.com/ykhirao)さんのこちらの記事[Slack APIのTokenの取得・場所](http://qiita.com/ykhirao/items/0d6b9f4a0cc626884dbb)
を参考にさせていただきました。

もう一つ、投稿したいチャンネルのコードを取得します。
任意のチャンネルのURLに含まれる、この赤枠の部分です。
<img width="544" alt="スクリーンショット 2017-07-01 12.55.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/151247/10a9b43a-8192-26ab-254c-f440543a79d3.png">




## コード 


```js

'use strict';

const https = require('https');
const querystring = require('querystring');

//Slack web API
const SLACK_HOST = 'slack.com';
const SLACK_PATH = '/api/chat.postMessage';
const TOKEN = 'トークン'; // 取得したSlackのトークン
const CHANNEL = 'チャンネル'; // 投稿したいチャンネルのコード
const NAME = 'INOKI BOT';
const ICON = ':rage1:';

function sendSlack(message, context){

    var formData = {
        token: TOKEN,
        channel: CHANNEL,
        text: message,
        as_user: false,
        icon_emoji: ICON,
        username: NAME,
    };
    var options = {
        hostname: SLACK_HOST,
        path: SLACK_PATH,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
    };
    var req = https.request(options, function(res){
        res.on('data', function(d) {
            console.log(d + "\n");
        });
    });
    req.write(querystring.stringify(formData));
    req.on('error', function(e){
        console.log("Slackにメッセージを送信できませんでした\n" + e.message);
    });
    req.end();
}



exports.handler = (event, context, callback) => {
    
    /**
     * paramList
     * host: 検証したいホスト
     * path: 検証したいパス(200が返ってくるところ)
     * auth: Basic認証の"username:password"
     */
    var paramList = new Array(
        { host: 'example.com', path:'', auth: '' },
        { host: 'example.com', path: '/about', auth: '' },
        { host: 'api.fugafuga.jp', path: '/login', auth: 'hogehoge:hogehoge!' }
    );
    var index = 0;

    sendSlack('＜ 元気ですかー！');
    function checkAlive(index) {
        var option = paramList[index];
        if(index == paramList.length){
            context.succeed();
            return;
        }
        index++;

        // httpsでリクエストを送ってみる
        var req = https.request(option, function (response) {
            var code = response.statusCode;
            if (code != 200) {	// 負けること考えるバカいるかよ!
                var message = "\n*【異常】* \n" + option.host + option.path + " は元気がないようです\n- STATUS CODE " + code;
                console.log(message);
                sendSlack(message, context);
            }
            else{	// 元気があればなんでもできる
                var message = option.host + " : " + code;
                console.log(message);
            }
            checkAlive(index);
        });
        req.on('error', function (error) {	// まるで屍のようだ
            var message = "\n*【エラー】* \n" + option.host + " にリクエストを送信できません";
            message += "\n - ERROR MESSAGE : " + error.message + "\n";
            console.log(message);
            sendSlack(message, context);
            checkAlive(index);
        });
        req.end();
    }

    checkAlive(index);
}

```

使い方としては、生死を確認したい宛先の情報を`paramList`に追加するだけ。
APIサーバだとアクセスを許可するドメインを制限したりする場合があると思うので、200が返ってくるパスを指定できるようにしています。
ついでにbasic認証も通せるように。

Node.jsとはいえただのJavaScriptで書けばいいのです。楽勝楽勝。


https.requestは非同期で実行されるので、そのままだとレスポンスを受け取る前に関数が終了してしまいます。
checkAlive関数を再帰的に呼び出し、一つ一つ実行していくような形で実装。
全てのホストを確認した後、`context.success()`を呼び関数を終了します。

この方法にはホストの状態によっては関数がタイムアウトするというデメリットがあります。
デフォルトでは1分になっている設定を増やせばタイムアウトしにくくはなりますが、まあ気持ち悪いですね。
Promiseとかでなんとかできたらいいな...（←たぶんやらない）
　


# 実行結果


### コンソール出力

```
example.com : 200

*【異常】* 
example.com/about は元気がないようです
- STATUS CODE 404

*【エラー】* 
api.fugafuga.jp にリクエストを送信できません
 - ERROR MESSAGE : getaddrinfo ENOTFOUND api.fugafuga.jp api.fugafuga.jp:443
```


### Slack
<img width="652" alt="スクリーンショット 2017-07-02 0.54.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/151247/84514daf-57a4-4eb8-b145-6ef7cd2ba5df.png">



example.comは例示用ドメインとして実際にアクセスできますが、/aboutなんてパスはないので404。
api.fugafuga.jpは存在しないドメインとしてエラーを吐きます。

なお、関数内で`console.log`を使って出力した部分はCloudWatchのロググループ（=Lambda関数名）で確認できます。


おしまい。