ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

AWSをMackerelで監視(通知応用編)

クラウドインテグレーション事業部所属の自称魚介系エンジニア兼Mackerelアンバサダーの松尾です。

コロナが蔓延し始めてからほとんど記事が投稿できておりませんでしたので(約1年半!?)、本当に久しぶりの投稿となります。

投稿していなかったこの期間に色んな出来事がありました。 個人的に特に一番大きい出来事は、この短い期間で二児の父親になったことです。 家族が増えたことで今までよりさらに気を引き締めて仕事に取り組んでいこうと思っています。

〆る(締める)といえば、夏が近づいて参りましたので、もうそろそろゴマサバの季節ですね。

サバといえば??

そうです!Mackerelです!!

今回は以前投稿したMackerelの通知方法から、より具体的な運用ケースにフォーカスしたお話をさせていただきます。 ※基本的な用語の説明などは省かせていただきます。

なお、これまでのMackerelに関する投稿は以下の通りです。
気になる記事がございましたら、是非ご一読下さい!!

今回お話ししたいこと

皆さんアラートの通知を行う際に以下のようなことで悩まれたこと無いですか?

  • Warningのアラートは特に監視する必要は無いが、状態確認のために通知はさせたい
  • Warningのアラートが多くCriticalのアラートが埋もれてしまうことがある
  • WarningとCriticalアラートの通知先を明確に分けたい

この要件をMackerelで満たすためにはどうすべきか、というところではあるのですが、Mackerelでは以下のような仕様があります。

  • 1つのアラート設定内でWarningとCriticalの値が設定できる
  • 通知グループの設定でCriticalのみを通知することができる設定がある
  • 通知グループの設定でWarningのみ通知を行う設定はない

つまり、Mackerel上の設定でWarningのみを通知先に通知させるためにはWarningのみ値を入力した監視設定を行った上で、Warning用に準備した通知先へ個々の監視設定を適用していく必要があります。

また、当然Criticalも通知させる必要がありますので、1つのメトリクスに対し、WarningとCriticalの2つの監視設定を行った上で、それぞれの通知グループに対し一つ一つの監視項目を適用していく必要があるため、かなり面倒な作業となります。

なお、それに加え、外形監視などは仕様上完全にWarningとCriticalに通知先を分離することが難しい(5xxエラーなどは設定で制御できないため)というような問題もあります。

今回はそういったケースに対しMackerelのみの機能に頼らず、Amazon EventBridgeと連携することで要件が満たせないかを検証してみたいと思います。

検証

前提

先に結論をからお話しすると、EventBridgeに連携される情報のみでイベントを生成した場合、直接Mackerelから通知を行う場合と比べ情報量が少ないです。

EventBridgeに連携される情報を使ってMackerelからも情報を引っ張ってくることで、このギャップは埋めることは可能ではありますが、今回はEventBridgeに連携される情報のみでイベントを生成します。
※まぁ通知さえできればMackerelの管理コンソールで詳細は確認できますので。。。

構成としては以下のような形になります。

AWS構成図

全体の概要

EventBridgeに連携された情報はLambdaで整形を行った上、webhookでSlack通知を行います。

Lambda自体の通知が正常になされたかの監視も必要になるかと思いますので、そちらをCloudWatch+SNS+Chatbotで監視します。
※Mackerelで監視してもよかったのですが、個人的にChatbotの検証をしてみたかったためAWS上から監視を行います。

Slackの設定について

前段でもお伝えした通り、今回の目的はCriticalとWarningの通知を分けることですので、SlackのチャンネルはCriticalとWarningで別に設け、それぞれincoming-webhookの設定を行います。

DynamoDBの利用用途について

Mackerelはエラーが復旧した際にも各アラートに紐づいたOKステータスの通知を行われます。

このOKステータスも固有のSlackチャンネルに通知してもよいのですが、どのアラートに対するOKステータスかを分かり易くするため、OKステータス直前のステータスがCriticalの場合はCriticalチャンネルに、Warningの場合はWarningチャンネルにOKステータスを通知するように設定します。(Mackerelで直接通知させた場合と同様の動作を目指す)

そこで、アラートが発生するたびに、それぞれが所持している一意のアラートIDとステータス(CriticalまたはWarning)情報をDynamoDBに保管し、OKステータスが通知された際にはそこから情報を収集し通知すべきSlackチャンネルを指定する処理を行います。
※他の保管場所でもよいのですが、今回の用途とコストパフォーマンスを考えてパッと思いついたのがDyanmoDBだったため、DynamoDBを利用しています。(深い理由はありません・・・)

EventBridge連携設定(Mackerel側)

本設定においては以下のブログのEventBridge連携設定(Mackerel側)をご参考下さい。

なおSlackチャンネルは用途別に以下を準備しております。

  • Lambda実行エラー通知用チャンネル
  • Mackerel Warningアラート通知用チャンネル
  • Mackerel Criticalアラート通知用チャンネル

EventBridge連携設定(AWS側)

AWS側は以下のリソースの作成と設定を行います。

  • DynamoDBのテーブル作成
    前述の通り、OKステータス通知先のSlackチャンネルを判別するために必要

  • Lambda関数実行用のIAMロール作成
    Lambda関数で他サービスと連携を行うために必要

  • Lambda関数の作成
    EventBridgeの通知をトリガーとしてRunCommandでWEB再起動を行うために利用する

  • EventBridgeの設定
    Mackerelの外形監視からイベントを受け取り、Lambda関数へ処理を受け渡すために利用する

  • SNSトピック設定
    CloudWatchアラームの通知をChatbotへ連携するために利用する

  • Chatbot設定
    Lambdaの実行アラートをSlackに通知するために利用する

  • CloudWatchアラーム設定
    Lambdaの実行エラーを監視する

DynamoDBのテーブル作成

DynamoDBテーブルの作成を行います。

今回は検証が目的であるため、ほぼデフォルト設定とします。

  • テーブル名
    今回は「mackerel-alertid-table」で設定します

  • パーティションキー
    アラートIDをキーとしたいため、「alertid」で設定します

  • ソートキー
    今回はアラートIDとステータス情報のみの単純構成であり、OKステータスとなった都度レコードも削除するため、設定は行いません

  • 設定
    デフォルト設定のままとします(検証が目的であり、キャパシティの設計が面倒なため)

  • タグ
    特に今回は設定しません

DynamoDB設定画面

以上の情報でテーブル作成を行い、実際に作成されたらDynamoDBの準備は完了です。

Lambda関数実行用のIAMロール作成

IAMロールの作成方法につきましては説明を省略させていただき、以降はアタッチするIAMポリシーについて説明します。

今回以下のような権限を持つIAMポリシーの作成を行います。

  • CloudWatchLogsへのログ出力権限
    Lambdaの実行結果をCloudWatchLogsに出力するために必要

  • DyanamoDBテーブルの操作権限
    アラートID、ステータスをDyanamoDBテーブルに一時登録を行うために必要

以上を踏まえて作成したIAMポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:[アカウントID]:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:[アカウントID]:log-group:/aws/lambda/[Lambda関数名]:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:DeleteItem",
                "dynamodb:GetItem"
            ],
            "Resource": "arn:aws:dynamodb:ap-northeast-1:[アカウントID]:table/[DynamoDBテーブル名]"
        }
    ]
}

※[]で括ったものは実際の値と置き換えが必要となります。
※上記は東京リージョン(ap-northeast-1)を対象とした例です。

Lambda関数の作成

次にLambda関数の作成を行います。 細かい手順は省略しますが、ランタイムは「Python3.9」を利用し、前項で作成したIAMロールを指定して関数を作成します。

関数の作成が完了したら、コードを入力します。
概要は前提に記載した通りではありますが、私自身のコーディング力と準備の手間を鑑みて具体的には以下のような形で実装を行います。

  • EventBridgeに通知される内容だけをもとにアラートメッセージを整形する
    アラートのカテゴリ(ホストメトリック監視、外形監視など)によって通知パラメータが違うが、カテゴリごとの情報を補完するためにMackerelから情報を取得したりしない

  • 3点のカテゴリ(ホストメトリック監視、ホスト死活監視、外形監視)のみが今回の判定範囲とする
    それ以外のカテゴリ(式監視、チェック監視、サービスメトリック監視など)の監視においては考慮しない(確認要)

  • 実動作に関わる部分のみの実装をする
    例外などのエラーハンドリングは行わない

以上のような内容で実装を行うため、もしも参考にされる場合は、ご注意ください。

では実際のコードの中身ですが、すべてを記載すると長文になってしまいますので、いくつかピックアップして記載します。
各項目の説明はコード内のコメントをご確認下さい。

import urllib3 
import json
import boto3
import re
from boto3.dynamodb.conditions import Key, Attr

# WebhookでSlack通知を行う際に利用します。
http = urllib3.PoolManager()

# 各エラーステータスSlackチャンネルのWebhookのURLを指定します。
crit_webhook_url = "https://hooks.slack.com/services/XXX"
warn_webhook_url = "https://hooks.slack.com/services/T02EQH9GB/XXX"

dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table('mackerel-alertid-table')

def lambda_handler(event, context):
    
    # 関数内で共通利用するアラート基本情報を変数として指定します。
    orgname = event['detail']['orgName']
    alertid = event['detail']['alert']['id']
    status = event['detail']['alert']['status']
    monitorname = event['detail']['alert']['monitorName']
    alert_URL = event['detail']['alert']['url']
    memo = event['detail']['memo']

    # 通知されたイベントが死活監視かどうかを判定します。
    # 死活監視の場合は常にCriticalチャンネル通知のためDynamoDBに保管するデータは判定に利用しません。
    if monitorname == "connectivity" and status == "critical":
        # 死活監視用の通知パラメータを抜き出します。
        host = event['detail']['host']['name']
        role = event['detail']['host']['roles'][0]['serviceName']
        service_url = event['detail']['host']['roles'][0]['serviceUrl']
        # Slackに通知するメッセージの設定を行います。
        msg = {
            "channel": "#mackerel-critical",
            "username": "Mackerel",
            "icon_emoji": ":mackerel-white:",
            "attachments":[
                {
                    "fallback":"Mackerel Alert",
                    "pretext":"[" + str(orgname) +"]" + "CRITICAL:" + str(host) + " 死活監視エラー " + "<" + alert_URL + "|View Alert>"  + " "  + "<!channel>",
                    "color":"#FF0000",
                    "fields":[
                        {
                            "title":"",
                            "value": memo + "\n" + "Role:[" + "<" + service_url + "|" + role + ">" + "]" + "Monitor:[" + monitorname + "]"
                        }
                    ]
                }
            ]
        }
        # Slackへのメッセージ通知を行います。
        encoded_msg = json.dumps(msg).encode('utf-8')
        resp = http.request('POST', crit_webhook_url, body=encoded_msg)
        # DynamoDBへのアラートID、ステータスのレコード登録を行います。
        res = table.put_item(
            Item={
                "alertid": alertid,
                "status": status
            }
        )
    # OKステータスかどうかの判定を行います。
    elif monitorname == "connectivity" and status == "ok":
        host = event['detail']['host']['name']
        role = event['detail']['host']['roles'][0]['serviceName']
        service_url = event['detail']['host']['roles'][0]['serviceUrl']
        msg = {
            "channel": "#mackerel-critical",
            "username": "Mackerel",
            "icon_emoji": ":mackerel-white:",
            "attachments":[
                {
                    "fallback":"Mackerel Alert OK!",
                    "pretext":"[" + str(orgname) +"]" + "OK:" + str(host) + " 死活監視エラー " + "<" + alert_URL + "|View Alert>"  + " "  + "<!channel>",
                    "color":"good",
                    "fields":[
                        {
                           "title":"",
                           "value": memo + "\n" + "Role:[" + "<" + service_url + "|" + role + ">" + "]" + "Monitor:[" + monitorname + "]"
                        }
                    ]  
                }
            ]
        }
        encoded_msg = json.dumps(msg).encode('utf-8')
        resp = http.request('POST', crit_webhook_url, body=encoded_msg)
        # DynamoDBに登録していたアラートIDレコードを削除します。
        res = table.delete_item(
            Key={
            "alertid": alertid
            }
        )
    # 死活監視と同じ流れで外形監視の判定を行います。
    elif re.compile("外形監視").search(monitorname) and status == "critical":
    ~~~ 死活監視と同じ流れのため省略 ~~~
    # 以降で死活、外形監視以外の監視を判定します。
    # ホストメトリック監視を基準とした内容としております。
    else:
        # 基本的な流れは前述のものと同じであるためWarningも含め省略します。
        if status == "critical":
         ~~~ 死活監視/外形監視と同じ流れのため省略 ~~~
        # ホストメトリック監視でOKステータスが出た場合は、アラート発生時にDynamoDBに保管していた自身のアラートIDに紐づいたステータスを取得します。
        elif status == "ok":
            items = table.get_item(
                Key={
                     "alertid": alertid
                }
            )
            items_data = items['Item']['status']
            # DynamoDBのレコードがCritical出会った場合、CriticalのSlackチャンネルに通知を行います。
            if items_data == "critical":
                host = event['detail']['host']['name']
                role = event['detail']['host']['roles'][0]['serviceName']
                service_url = event['detail']['host']['roles'][0]['serviceUrl']
                msg = {
                    "channel": "#mackerel-critical",
                    "username": "Mackerel",
                    "icon_emoji": ":mackerel-white:",
                    "attachments":[
                        {
                            "fallback":"Mackerel Alert OK!",
                            "pretext":"[" + str(orgname) +"]" + "OK:" + str(host) + " " + str(monitorname) + " " + "<" + alert_URL + "|View Alert>"  + " "  + "<!channel>",
                            "color":"good",
                            "fields":[
                                {
                                   "title":"",
                                   "value": memo + "\n" + "Role:[" + "<" + service_url + "|" + role + ">" + "]" + "Monitor:[" + monitorname + "]"
                                }
                            ]  
                        }
                    ]
                }
                encoded_msg = json.dumps(msg).encode('utf-8')
                resp = http.request('POST', crit_webhook_url, body=encoded_msg)
                # Slackチャンネルに通知が完了したら不要なアラートIDであるため削除します。
                res = table.delete_item(
                    Key={
                     "alertid": alertid
                    }
                )
            # Warningの通知先判定もCriticalと同様に行います。
            elif items_data == "warning":
            ~~~ Criticalと同様の流れのため省略 ~~~
        # Unknownステータスが発生することもあるため、こちらも判定します。
        elif status == "unknown":
            msg = {
                "channel": "#mackerel-critical",
                "username": "Mackerel",
                "icon_emoji": ":mackerel-white:",
                "attachments":[
                    {
                        "fallback":"Mackerel Alert",
                        "pretext":"[" + str(orgname) +"]" + "UNKNOWN:" + str(monitorname) + " " + "<" + alert_URL + "|View Alert>"  + " "  + "<!channel>",
                        "color":"#FFFFFF",
                        "fields":[
                            {
                                "title":"",
                                "value":"UNKNOWN:" + str(monitorname)
                            }
                        ]
                    }
                ]
            }
            encoded_msg = json.dumps(msg).encode('utf-8')
            resp = http.request('POST', crit_webhook_url, body=encoded_msg)

上記のSlack APIのメッセージ作成は以下Slackドキュメントを参考にしております。 api.slack.com

ちなみに補足として、アラートの種類によってはWarningからCriticalにアラートレベルが変化する場合はありますが、その場合でもアラートIDは変わりません。 CriticalのみOKステータスとなってクローズし、WarningのアラートIDだけDynamoDBに残り続けるというような不具合は発生しませんのでご安心下さい。

EventBridgeの設定

前項までの設定が全て完了したらEventBridgeの設定を行います。 本設定においては以下のブログのEventBridge連携設定(AWS側) > EventBridgeの設定をご参考下さい。

SNSトピック作成

SNSトピックの作成は、一般的な手順での作成・設定となりますので、以下のAWSドキュメントをご参照の上設定を行ってください。

Amazon SNS 通知の設定 - Amazon CloudWatch

Chatbot設定

Chatbotのコンソール画面より、まずは新しいクライアントを設定を押下し、自分のアカウントが所属するSlackワークスペースと連携します。

ChatbotとSlack連携画面①
ChatbotとSlack連携画面②

Slackワークスペースとの連携が完了したら、次は新しいチャンネルを設定します。 今回は以下のように設定します。

  • 設定名
    今回は「mackerel-info」で設定します

  • チャネルタイプ
    パブリックチャンネルを利用するため、「パブリック」で設定します

  • パブリックチャネル名
    今回は「mackerel」というSlackチャンネルを利用します

  • ロール設定
    デフォルトの「チャネルIAMロール」のままとします

  • チャネルIAMロール
    デフォルトの「テンプレートを使用してIAMロールを作成する」のままとします

  • ロール名
    例として記載してある「AWSChatbot-role」で設定します

  • ポリシーテンプレート
    デフォルトの「通知のアクセス許可」のままとします

  • チャネルガードレールポリシー
    ReadOnlyAccess」とします

  • SNSトピック
    事前に設定したSNSトピックを指定します
    今回は「Mackerel-Lambda-Topic」で作成しています

Chatbotチャンネル設定画面

これでChatbot側の準備も完了です。

CloudWatchアラーム設定

CloudWatchアラームの作成は、一般的な手順での作成・設定となりますので、以下のAWSドキュメントをご参照の上設定を行ってください。
ちなみに、今回監視するメトリクスはLambdaのErrorsメトリクスです。

docs.aws.amazon.com

通知テスト

環境の準備は整ったので、実際にどのような通知がされるかを確認します。(コード見ただけではイメージが湧かないと思いますので・・・)

Mackerelで(監視ホストで)意図的にWarning、Criticalそれぞれ発生させて、復旧させると、それぞれ以下のような通知がされます。

  • 死活監視のCritical通知

  • 死活監視のOK通知

  • 外形監視のCritical通知

  • 外形監視のOK通知

  • ホストメトリック監視のCritical通知

  • ホストメトリック監視のWarning通知

  • ホストメトリック監視のOK通知

  • ChatbotからのLambdaエラー通知

色とか内容とかほぼ思ったようにできたので満足です!!
Mackerelからの通知(以下)にかなり近づけれました!

EventBridgeの情報だけではここまでの表現ができないため、情報に不足を感じられる場合はMackerelからも情報を収集する必要があります。
なお、アイコンが違うのは、青背景のロゴは公式ページで現在は提供されていないため、黒背景のものを利用しております。

余談といいますか、少しオマケですが、検証始めた頃は以下の通知内容でしたので、かなり頑張った感出てます・・・(笑)

次の配信について

今回はMackerelの通知応用編として、エラーレベルによって通知先を振り分ける設定について記載させていただきました。
※Mackerelというよりも、かなりAWSに偏った話になったかもしれませんが・・・(笑)

Mackerelからの通知パターンが増えたり新たな監視カテゴリが増えたりすると、都度メンテナンスが大変そうなので、できれば「Warningのみ通知」機能を追加いただきたいところです・・・!(はてなさんお願いします!)

次回は何を書くかも、いつになるかも分かりませんが、これまでの投稿で書く書く詐欺していたような内容もありますので、できればそういったところを回収していければと思っています。。。

それでは次回もお楽しみに!!