はじめに
Webサービスの稼働率を計算して定期的にモニタリングしたい事案があり、Route 53ヘルスチェックのメトリクスをLambdaで集計してslackに投げる仕組みをServerless Frameworkで作ってみたのでご紹介します。
仕様
- CloudWatch Eventsのcron式により定期的に発火(下記サンプルの通り進めてもらうと毎週月曜9:05JSTに動きます)
- Route 53ヘルスチェック結果をCloudwatchメトリクスから取得
- 稼働率を計算してslackにpostする
稼働率の定義
こちらの内容からパク インスパイアされております。リスペクト。
Route 53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた | DevelopersIO
2/3以上のヘルスチェッカーが異常として記録した時間帯をダウンタイムとし、
アップしていた計測ポイント / 全計測ポイント
を稼働率としています。
設定手順
Route 53ヘルスチェック作成
特に難しいところはありませんので、下記ページ等を参考に設定してください。
※ご自身の管理下にあるドメインに対して設定してくださいね!
SlackのIncoming Webhook URL生成
こちら等を参考に。
Incoming Webhook URLをSSMパラメータストアに保存
Webhook URLを平文でコードに書くのは憚られるので、SSMパラメータストアに暗号化して保存します。
※面倒だったら飛ばしても構いません。逆に他のパラメータも平文で書きたくなければ同様の手順で対応してください。
1. KMSでキーを作成
- 適当な名前をつける
- 必要に応じてタグを設定
- 後ほどserverless deployを実行するIAMユーザまたはロールにキーへのアクセス許可を設定
- キーの管理者権限とdeployする権限が異なる場合は必要に応じて調整してください
- 確認して完了
以上で暗号化キーの作成完了です。
2. SSMパラメータストアにWebhook URLを保存
-
KMSキーID
に先ほど作成したキーを指定するのがポイント
Serverless Framework
1. Serverless Frameworkをインストール
$ npm install -g serverless
※参考
Serverless Getting Started Guide
なおこの記事を書くにあたって動作確認したのは以下の状態です。
$ 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
3. 以下の内容でserverless.ymlとhandler.pyを適当なディレクトリに配置する
-
こちらのリポジトリをcloneしてきていただいても大丈夫です。
-
serverless.ymlにて、以下のパラメータを各自の環境に合わせて更新が必要になります
-
HookUrl
- 先ほど作成したslackのWebhook URL
- 下の例のように書けば、パラメータストアに暗号化して保存した文字列を復号してLambdaFunctionの環境変数にセットされます。
-
slackChannel
- postするチャンネル名
-
HealthcheckId
- 先ほど作成したRoute 53ヘルスチェックのリソースID
-
HookUrl
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 *)
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がデプロイされます。
結果
まとめ
だいぶ前にServerless Frameworkを少しだけさわった時の印象で、破壊的な更新がどんどん入ってくるちょっと怖い奴というイメージだったのですが、久々に使ってみたらだいぶ安定感のあるツールになっていました(個人の記憶と感想)。
これなら軽くスクリプト書くくらいの感覚でLambdaFunctionを書くのもいいなと思いました。ありがとう、いいツールです。
以上です。
参考文献
Route 53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた | DevelopersIO
CloudWatch — Boto 3 Docs 1.9.244 documentation
Serverless Getting Started Guide
Serverless Framework Commands - AWS Lambda - Config Credentials