見出し画像

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

改善前: Simple is best

CloudWatchアラームがしきい値を超えたらSNSへpublish、AWS ChatbotがSNSトピックをsubscribeしているので、後はお任せでSlackへアラートが飛んできます。

胃がキュッとなるやつ

アラーム状態が検知されるのは一度きり

上記の仕組みを問題なく活用できているものの、チーム内で以下のような意見が上がりました。

「問題が継続しているかどうか判断し難いので、問題継続中は定期的にアラートが来るようにできないだろうか?」

確かに、仕組みを構築したメンバーやアラート対応しているエンジニアは重要度・緊急度をイメージしやすいですが、他メンバーと解像度を合わせるには工夫が必要です。

簡単なドキュメントは作成したものの、それだけでは不十分だと考え、継続通知の仕組み化を考えることにしたのですが、ここで立ちはだかったのがCloudWatchアラームの仕様です。

https://aws.amazon.com/jp/blogs/news/how-to-enable-amazon-cloudwatch-alarms-to-send-repeated-notifications/

CloudWatch アラームは、状態が変化したときにのみアラームアクションを呼び出すように設計されています。

Amazon Web Services ブログ

「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で嬉しいアップデートが来ていました。

どちらも夢が拡がるアップデートなので、機会を見て活用していきたいと思います。

この記事が気に入ったらサポートをしてみませんか?