Route 53ヘルスチェック結果を元に稼働率を計算してslackにpostする with Serverless Framework

# はじめに
Webサービスの稼働率を計算して定期的にモニタリングしたい事案があり、Route 53ヘルスチェックのメトリクスをLambdaで集計してslackに投げる仕組みをServerless Frameworkで作ってみたのでご紹介します。

# 仕様
* CloudWatch Eventsのcron式により定期的に発火（下記サンプルの通り進めてもらうと毎週月曜9:05JSTに動きます）
* Route 53ヘルスチェック結果をCloudwatchメトリクスから取得
* 稼働率を計算してslackにpostする

# 稼働率の定義
こちらの内容から~~パク~~ インスパイアされております。リスペクト。
[Route 53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/downtime-calc-by-route53/)

2/3以上のヘルスチェッカーが異常として記録した時間帯をダウンタイムとし、
`アップしていた計測ポイント / 全計測ポイント`
を稼働率としています。

# 設定手順
## Route 53ヘルスチェック作成
特に難しいところはありませんので、下記ページ等を参考に設定してください。

* [Route 53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/downtime-calc-by-route53/)	
* [ヘルスチェックを作成または更新するときに指定する値 - Amazon Route 53](https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/health-checks-creating-values.html#health-checks-creating-values-health-checker-regions)

※ご自身の管理下にあるドメインに対して設定してくださいね！

## SlackのIncoming Webhook URL生成
こちら等を参考に。

* [Slack でのIncoming Webhook の利用 | Slack](https://slack.com/intl/ja-jp/help/articles/115005265063)

## Incoming Webhook URLをSSMパラメータストアに保存
Webhook URLを平文でコードに書くのは憚られるので、SSMパラメータストアに暗号化して保存します。
※面倒だったら飛ばしても構いません。逆に他のパラメータも平文で書きたくなければ同様の手順で対応してください。

### 1. KMSでキーを作成
* 適当な名前をつける
<img width="999" alt="スクリーンショット 2019-10-13 16.31.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/e41782fc-b2ff-9a76-7963-b7a733d52ac9.png">

* 必要に応じてタグを設定
<img width="987" alt="スクリーンショット 2019-10-13 16.31.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/a28028bc-61a6-de7a-f52e-0d8624fd61b2.png">

* 後ほどserverless deployを実行するIAMユーザまたはロールにキーへのアクセス許可を設定
    * キーの管理者権限とdeployする権限が異なる場合は必要に応じて調整してください
<img width="1093" alt="スクリーンショット 2019-10-14 3.51.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/b2038d81-1e13-0926-162d-fee5f6950614.png">
<img width="1022" alt="スクリーンショット 2019-10-17 1.36.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/4112222b-2fd7-34ab-8d44-006431f5c5b6.png">

* 確認して完了


以上で暗号化キーの作成完了です。

### 2. SSMパラメータストアにWebhook URLを保存

* `KMSキーID` に先ほど作成したキーを指定するのがポイント
<img width="878" alt="スクリーンショット 2019-10-13 16.40.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/ddc32ea0-7c87-2c81-9e41-1aac02accf84.png">


## Serverless Framework
### 1. Serverless Frameworkをインストール

```
$ npm install -g serverless
```

*※参考*
[Serverless Getting Started Guide](https://serverless.com/framework/docs/getting-started/)

なおこの記事を書くにあたって動作確認したのは以下の状態です。

```
$ serverless -v                                                                                                                                      19-10-17 1:47:40
Framework Core: 1.54.0
Plugin: 3.1.2
SDK: 2.1.2
Components Core: 1.1.1
Components CLI: 1.2.3
```

### 2. 必要に応じてcredential設定
*※参考*
[Serverless Framework Commands - AWS Lambda - Config Credentials](https://serverless.com/framework/docs/providers/aws/cli-reference/config-credentials/)

### 3. 以下の内容でserverless.ymlとhandler.pyを適当なディレクトリに配置する
* こちらのリポジトリをcloneしてきていただいても大丈夫です。
  * [GitHub - j-un/uptime-percentage-calculator](https://github.com/j-un/uptime-percentage-calculator)

* serverless.ymlにて、以下のパラメータを各自の環境に合わせて更新が必要になります
    * **HookUrl**
        * 先ほど作成したslackのWebhook URL
        * 下の例のように書けば、パラメータストアに暗号化して保存した文字列を復号してLambdaFunctionの環境変数にセットされます。
    * **slackChannel**
        * postするチャンネル名
    * **HealthcheckId**
        * 先ほど作成したRoute 53ヘルスチェックのリソースID

```serverless.yml
service: uptime-percentage-calculator

frameworkVersion: ">=1.2.0 <2.0.0"

provider:
  name: aws
  runtime: python3.7
  stage: production
  region: us-east-1
  timeout: 300
  iamRoleStatements:
    - Effect: "Allow"
      Action: "cloudwatch:GetMetricStatistics"
      Resource: "*"

functions:
  cron:
    handler: handler.run
    environment:
      HookUrl: ${ssm:SlackWebhook~true}
      slackChannel: "#notice"
      HealthcheckId: "your_healthcheck_id"
    events:
      - schedule: cron(5 0 ? * MON *)
```

```handler.py
import boto3
import datetime
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

SLACK_CHANNEL = os.environ['slackChannel']
HOOK_URL = os.environ['HookUrl']
HEALTHCHECK_ID = os.environ['HealthcheckId']

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def get_metrics_1day(date):
    starttime = date.replace(hour=0, minute=0, second=0, microsecond=000000)
    endtime = date.replace(hour=23, minute=59, second=59, microsecond=999999)

    return boto3.client('cloudwatch', region_name='us-east-1').get_metric_statistics(
        Namespace='AWS/Route53',
        MetricName='HealthCheckPercentageHealthy',
        Dimensions=[
            {
                'Name': 'HealthCheckId',
                'Value': HEALTHCHECK_ID
            }
        ],
        StartTime=starttime,
        EndTime=endtime,
        Period=60,
        Statistics=[
            'Average'
        ]
    )


def extract_healthcheck_results_per_min(metrics):
    datapoints = map(lambda x: x['Datapoints'], metrics)
    healthcheck_results_per_day = map(
        lambda x: [d.get('Average') for d in x], datapoints)
    return sum(healthcheck_results_per_day, [])


def calculate_uptime_percentage(healthcheck_results):
    if not healthcheck_results:
        logger.error("Metrics not available yet.")
        return False

    uptimes = list(filter(lambda x: x > 66.6, healthcheck_results))
    return round(len(uptimes)/len(healthcheck_results), 5)*100


def run(event, context):
    startdate = (datetime.datetime.now() - datetime.timedelta(days=7))
    datelist = [startdate + datetime.timedelta(days=day) for day in range(7)]

    metrics = map(get_metrics_1day, datelist)
    healthcheck_results = extract_healthcheck_results_per_min(metrics)
    uptime_percentage = calculate_uptime_percentage(healthcheck_results)

    if uptime_percentage is False:
        return "Abnormal end."

    message = '''
    * * Uptime percentage of last week ({startdate} ~ {enddate})*
    {uptime_percentage} %
    '''.format(
        uptime_percentage=uptime_percentage,
        startdate=datelist[0].strftime("%Y/%m/%d"),
        enddate=datelist[-1].strftime("%Y/%m/%d")
    ).strip()

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': message
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)
```

### ちょっとハマったところ
Route 53のこのメトリクスはus-east-1（バージニア北部）からしか取得できませんでした。
一生懸命東京リージョンからgetしようとして悩んでしまった。

## デプロイ
上記ファイルを配置したパスで以下のコマンドを実行します。

```
$ serverless deploy
```

うまくいくと、us-east-1にLambdaFunctionがデプロイされます。

# 結果
きました。
<img width="537" alt="スクリーンショット 2019-10-17 1.21.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92629/303b0979-14b8-0844-6d9e-3917b1bd0721.png">

# まとめ
だいぶ前にServerless Frameworkを少しだけさわった時の印象で、破壊的な更新がどんどん入ってくるちょっと怖い奴というイメージだったのですが、久々に使ってみたらだいぶ安定感のあるツールになっていました（個人の記憶と感想）。
これなら軽くスクリプト書くくらいの感覚でLambdaFunctionを書くのもいいなと思いました。ありがとう、いいツールです。
以上です。


# 参考文献
[Route 53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/downtime-calc-by-route53/)
[CloudWatch — Boto 3 Docs 1.9.244 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch.html#CloudWatch.Client.get_metric_statistics)
[Serverless Getting Started Guide](https://serverless.com/framework/docs/getting-started/)
[Serverless Framework Commands - AWS Lambda - Config Credentials](https://serverless.com/framework/docs/providers/aws/cli-reference/config-credentials/)