akippaインフラ改善物語 Vol.3
akippaのインフラを改善していく物語の3回目です。前回はAWSのアカウント分割までたどり着いた、というお話でした。
今回のテーマ
アカウント分割の完了後、取り組むべき課題が多々ある中で、現在はEOLが確定しているAuroraのバージョンアップに取り組んでいます。
来年で10周年を迎えるakippaの既存コードベースを踏まえてバージョンアップを行うには、なかなか険しい道のりというのが現実で、バージョンアップを完遂した暁にはご紹介したいと思います。
システム監視を改善したい
そんな中で、今回のテーマは「システム監視を改善したい」です。
AWSを活用しているサービスやproductでは、CloudWatchやEventBridgeを駆使して問題を検知、Slackなどに通知して初動をとる、というスタイルが多いと思います。
akippaも例に漏れず、CloudWatchのメトリクス・アラームでヘルスチェック→しきい値を超えればSlackへ通知、というスタイルで運用しており、ここを改善強化したお話です。
現状運用の課題
CloudWatchからSlackへの連携はいたってシンプルで、以下のサービスを組み合わせて実現しています。
CloudWatch
AWS SNS
AWS Chatbot
CloudWatchアラームがしきい値を超えたらSNSへpublish、AWS ChatbotがSNSトピックをsubscribeしているので、後はお任せでSlackへアラートが飛んできます。
アラーム状態が検知されるのは一度きり
上記の仕組みを問題なく活用できているものの、チーム内で以下のような意見が上がりました。
「問題が継続しているかどうか判断し難いので、問題継続中は定期的にアラートが来るようにできないだろうか?」
確かに、仕組みを構築したメンバーやアラート対応しているエンジニアは重要度・緊急度をイメージしやすいですが、他メンバーと解像度を合わせるには工夫が必要です。
簡単なドキュメントは作成したものの、それだけでは不十分だと考え、継続通知の仕組み化を考えることにしたのですが、ここで立ちはだかったのがCloudWatchアラームの仕様です。
「ALRAM状態に変化したよ」は通知されますが、「まだALARM状態だよ」は通知されないということですね。既存の仕組みだけでは、やりたい事が実現できないようです。
無ければ作る
幸い、上記ブログの内容がやりたい事そのものだったため、これをベースで仕組み化することにしました。CDKでそのまま利用できるリポジトリまで公開してくれていますが、既存の構成とうまく同居させるため、CDKは敢えて使わず、一部の仕組みも改変しています。
継続通知対象のアラーム
サンプルでは、特定のタグが付与されているアラームのみ継続通知の対象となっていますが、以下の観点からすべてのアラームを対象としました。
アラーム数が160ほどあり、プライオリティをつけるのに時間がかかる
最初にフィルタリングすると、意図せず重要なアラームが埋もれてしまいかねない
既存SNSトピックとの連携
着手当初は、既にあるAWS Chatbotを利用したSNSトピックを流用して、継続通知もSlack通知しようと考えていました。
しかしながら、Chatbotはカスタムメッセージを受け付けない仕様のため、何とか流用させようとすると継続通知用のLambdaがどんどん複雑な処理になってしまいます。ここは潔く断念し、サンプル同様に継続通知用のSNSトピックを新たに作成しました。
改善後
以上を踏まえて、出来あがった仕組みがこちらです。Step FunctionsのステートマシンはAWS公式ブログと同じなので割愛しますが、既存のSlack通知に加え、EventBridge経由の継続通知フローが生えました。
Lambdaのコードも、サンプルコードをスリムアップさせた極めてシンプルなものですので、それぞれ掲載しておきます。
アラーム状態を確認し、沈静化していなければSNSへpublish
import datetime
import json
import logging
import os
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
session = boto3.session.Session()
CW_CLIENT = session.client("cloudwatch")
SNS_CLIENT = session.client("sns")
def lambda_handler(event, context):
logger.info(event)
event.update({"currState": "null"})
try:
# 対象のCloudWatchアラームから現在の状態を取得
alarm_name = event["detail"].get("alarmName")
alarm_response = CW_CLIENT.describe_alarms(
AlarmNames=[alarm_name],
AlarmTypes=["MetricAlarm"]
)
logger.info(alarm_response)
# datetimeオブジェクトを正しく処理できるよう、
# いったんdump -> loadしておく
alarm_details = json.loads(
json.dumps(
alarm_response.get("MetricAlarms")[0],
default=datetime_converter
)
)
current_state = alarm_details.get("StateValue")
if current_state == "ALARM":
# ALARM状態が続いているので、継続通知用のSNSにpublish
topic_arn = os.getenv("SNS_TOPIC_ARN")
SNS_CLIENT.publish(
TopicArn=topic_arn,
Subject=f"要確認: {alarm_name} のアラーム状態が続いています",
Message=json.dumps(alarm_details)
)
logger.info(f"Publish to {topic_arn}")
event["currState"] = current_state
except Exception as ex:
logger.error(f"Error: {repr(ex)}")
raise ex
return event
def datetime_converter(field):
"""helper function to perform JSON dump on object containing datetime"""
if isinstance(field, datetime.datetime):
return field.__str__()
新設したSNSトピックのsubscriber(Slack通知)
import json
import logging
import os
import urllib.request
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info(event)
for ev in event.get("Records", []):
# SNSメッセージのSubjectが通知すべき内容なので、そのままSlackへ通知する
message = json.dumps({"text": ev["Sns"]["Subject"]}).encode("utf-8")
req = urllib.request.Request(SLACK_WEBHOOK_URL, data=message)
req.add_header("Content-Type", "application/json")
urllib.request.urlopen(req)
必要なリソースを作成して組み合わせ、継続通知が無事届くようになりました。
まとめ
今回は、クラウド運用でよくある「アラート通知の改善」を取り上げました。同じような悩みを抱えている、どなたかの参考になれば幸いです。
また、仕組み化している最中はリリースされていませんでしたが、今回利用したEventBridgeとAWS Chatbotで嬉しいアップデートが来ていました。
AWS Chatbot でカスタム通知が利用可能に
EventBridgeがイベントパターンのワイルドカードをサポート
どちらも夢が拡がるアップデートなので、機会を見て活用していきたいと思います。