AWS lambdaを使ってWebサイトの死活監視を行う

以前mimamoというウェブサイトの死活監視をHTTPのステータスコードレベルでのチェックをしてくれるサービスがありました。
ただやはりマネタイズが出来ないからでしょうか、残念ながらクローズしてしまいました。

railsとかでさくっと作っても良かったんですけどサーバを持つのが嫌だったのでタイトルにある通りサーバレスでHTTPステータスの監視を行うようにしました。

## アーキテクチャ
![cloudcraft.png](https://qiita-image-store.s3.amazonaws.com/0/35451/f54788f2-f5bf-5a01-89a6-e8f3a4198f23.png)

左側の縦のラインがHTTPのステータスチェックを行う箇所で、右側のラインがSNSからlambdaへ通知しslackやメールで通知します。
DBなど持ちたくなかったので、S3にcsvファイルが置いてありチェック対象のURLが記述してあります。

## CSVファイル
以下のドメインはあくまでも例です。

```list.csv
https://404.jp,https://200.jp
```

## HTTPステータスチェックlambda

lambdaで使用出来るモジュールは限定されておりnpm installで取得したライブラリごとzip化してアップロード出来るけどそうするとデプロイにコストがかかってしまうため極力使わないようにコードを書いています。

### 時間単位で起動するlambdaを作成

![スクリーンショット 2016-12-14 9.44.48.png](https://qiita-image-store.s3.amazonaws.com/0/35451/cebd90a7-6036-f2b2-5a13-8a9a8506bc80.png)

### Configure function

![スクリーンショット 2016-12-14 9.48.05.png](https://qiita-image-store.s3.amazonaws.com/0/35451/0072be06-215c-2d46-ddbf-76fc2a1b729d.png)

### Lambda function code

コード内にSNSのarnがenvironment variableとして登録しているのでSNSを作成したらtopicArnとして登録してください。

```javascript:haiken.js
'use strict';

exports.handler = (event, context, callback) => {
    let AWS = require('aws-sdk');
    let s3 = new AWS.S3();
    s3.getObject({Bucket: 'haiken', Key: 'list.csv'}, function(err, listCsv){
      let csv = listCsv.Body.toString();
        csv.split(',').forEach(function(url){
          let https = require('https');
            https.get(url, function(res){
             if(res.statusCode != 200){
               let sns = new AWS.SNS();
               sns.publish({
                 Message: url + ' is error',
                 Subject: 'error notification',
                 TopicArn: process.env.topicArn
               }, (err, data) => {
                   console.log(err);
               });

             }
            }).on('error', (err) => {console.log(err)});
    
        });
    })
}
```

### Lambda function handler and role
このlambdaからはS3とSNSにアクセスするためroleを追加します。
大抵のroleであれば Create new role from templateを選択すればS3やSQSへのアクセス権は得られますがSNSがなかったため、新しいroleを作成してChoose an existing roleを選択し、作成したroleを選択する。

![スクリーンショット 2016-12-14 10.09.06.png](https://qiita-image-store.s3.amazonaws.com/0/35451/362979f6-13d6-5063-3121-a4e2e97b7229.png)

## 通知用lambda

### ブループリントを使用して新しく作成する

slackに通知するlambdaのブループリント(cloudwatch-alarm-to-slack)があったのでそれを使用しました。

![スクリーンショット 2016-12-14 10.15.15.png](https://qiita-image-store.s3.amazonaws.com/0/35451/a345a65f-7550-b5b9-a147-55b3f2aee140.png)

### Lambda function code
基本的にはHTTPステータスチェック用lambdaと内容は同じで、通知用lambdaでは、slackのwebhook用URLをEnvironment variablesに登録する。

![スクリーンショット 2016-12-14 10.26.40.png](https://qiita-image-store.s3.amazonaws.com/0/35451/6f2a7a50-14b0-6f80-6d94-99357673bf70.png)

```javascript:haiken_notification.js
'use strict';

const AWS = require('aws-sdk');
const url = require('url');
const https = require('https');

const kmsEncryptedHookUrl = process.env.kmsEncryptedHookUrl;
const slackChannel = process.env.slackChannel;
let hookUrl = process.env.hookUrl;

function postMessage(message, callback) {
    const body = JSON.stringify(message);
    const options = url.parse(hookUrl);
    options.method = 'POST';
    options.headers = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
    };

    const postReq = https.request(options, (res) => {
        const chunks = [];
        res.setEncoding('utf8');
        res.on('data', (chunk) => chunks.push(chunk));
        res.on('end', () => {
            if (callback) {
                callback({
                    body: chunks.join(''),
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage,
                });
            }
        });
        return res;
    });

    postReq.write(body);
    postReq.end();
}

function processEvent(event, callback) {
    console.log(event.Records[0].Sns.Message);
    const message = event.Records[0].Sns.Message;

    const slackMessage = {
        channel: slackChannel,
        text: message,
    };

    postMessage(slackMessage, (response) => {
        if (response.statusCode < 400) {
            console.info('Message posted successfully');
            callback(null);
        } else if (response.statusCode < 500) {
            console.error(`Error posting message to Slack API: ${response.statusCode} - ${response.statusMessage}`);
            callback(null);  // Don't retry because the error is due to a problem with the request
        } else {
            // Let Lambda retry
            callback(`Server error when processing message: ${response.statusCode} - ${response.statusMessage}`);
        }
    });
}

exports.handler = (event, context, callback) => {
    if (hookUrl) {
        // Container reuse, simply process the event with the key in memory
        processEvent(event, callback);
    } else if (kmsEncryptedHookUrl && kmsEncryptedHookUrl !== '<kmsEncryptedHookUrl>') {
        const encryptedBuf = new Buffer(kmsEncryptedHookUrl, 'base64');
        const cipherText = { CiphertextBlob: encryptedBuf };

        const kms = new AWS.KMS();
        kms.decrypt(cipherText, (err, data) => {
            if (err) {
                console.log('Decrypt error:', err);
                return callback(err);
            }
            hookUrl = `https://${data.Plaintext.toString('ascii')}`;
            processEvent(event, callback);
        });
    } else {
        callback('Hook URL has not been set.');
    }
};
```

## SNS
上記のlambdaから直接通知用のslackAPIを叩いたりしてもいいですが、そうするとメールで通知したり他の通知方法を追加する時にコードを修正する必要が出て来るため極力notification patternを利用して実装します。
さらにオブジェクト指向よろしくlambdaも処理時間に上限が決まってたりするため1コード1ロールくらいに収めた方が良さそうです。

### Create Topic
TopicはSubscribeの入れ物で、subscribeがどこに通知するかを登録するものです。

![スクリーンショット 2016-12-14 10.40.22.png](https://qiita-image-store.s3.amazonaws.com/0/35451/32bccb91-4793-28d2-9b5e-bbc7eaa60336.png)

### Subscribe to Topic
TopicArnはSubscribe to Topicから作成すれば自動で入力されｔげいます。
EndpointはProtocolによって入力する値が変わります(HTTPだったらURLとか)が、lambdaの場合は選択ボックスで作成したlambdaの一覧が出て来るので、実行したいlambdaを選択する。

![スクリーンショット_2016-12-14_10_37_12.png](https://qiita-image-store.s3.amazonaws.com/0/35451/5346ec51-a404-1f80-9574-5fd980c7e92f.png)

## slack

haiken.jsを実行するとこんな感じで通知される。
slack以外に通知したい場合はSNSで通知先を登録してやればメールが届くようにも出来たりします。

![スクリーンショット_2016-12-14_9_05_53.png](https://qiita-image-store.s3.amazonaws.com/0/35451/030c3d95-7453-69cf-1b49-736f3ead3c4b.png)

現場から以上です。