SSM Agent の Ping ステータスを監視する仕組みを EventBridge + Lambda 関数 + SNS を使って実装してみた

2024.05.02

はじめに

テクニカルサポートの 片方 です。

前回のブログで執筆数が 200 本を越えました。199 本目200 本目 のブログに引き続き Lambda 関数を利用した内容を紹介します。
こちらは、私の所属するテクニカルサポートチームへ「〇〇〇 を実現するサービスや機能はないか」と実際にお客様より頂いたお問い合わせを参考 (ヒント) にしてカスタムソリューションを作成してみました。
本ブログでは、マネージドノードに表示されている SSM Agent の Ping ステータスを監視する仕組みを EventBridge + Lambda 関数 + SNS を使って実装してみました。

構成と説明

SSM エージェントのステータスを毎分ポーリングする Lambda 関数を作成します。それをメトリクスとして取得して CloudWatch に送信します。
上記で作成した SSM エージェントのメトリクスを毎分反復処理する Lambda 関数を作成し、そこから CloudWatch アラームを作成します。CloudWatch アラームは、SSM エージェントのステータスメトリクスが「オンライン」以外の場合にトリガーされるように実装します。
アラームがトリガーされると、SNS トピックにメッセージが公開され通知される流れです。

以下はイメージです。

やってみた

SNS

通知機能として SNS を利用します。SNS トピックおよびサブスクリプションを作成します。
今回は、[Protocol (プロトコル)] として [Email (E メール)] を選択して作成しました。後に arn が必要なので控えます。

ロール

Lambda 関数にアタッチするロールを 2 つ作成します。共に信頼関係は以下です。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

ロール名: LambdaFunctionCheckSSMAgentStatusRole

アタッチするポリシー例

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ssm:DescribeInstanceInformation",
                "cloudwatch:PutMetricData"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

ロール名: LambdaCreateCWAlarmRole

アタッチするポリシー例

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudwatch:ListMetrics",
                "cloudwatch:PutMetricAlarm",
                "cloudwatch:DescribeAlarms"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": "ssm:DescribeInstanceInformation",
            "Resource": "*"
        }
    ]
}

Lambda 関数

2 つ Lambda 関数を作成します。ランタイムは共に Python 3.12 を使用しました。

関数名: LambdaFunctionCheckSSMAgentStatus

SSM エージェントのステータス状態を CloudWatch メトリクスとして送信する処理を実装します。
アタッチする実行ロールは、先ほど作成した "LambdaFunctionCheckSSMAgentStatusRole" を選択します。

サンプルコード
import boto3
from botocore.exceptions import ClientError

def send_cw_metric(metric_dimension_value, metric_value):
    client = boto3.client('cloudwatch')
    try:
        client.put_metric_data(
            Namespace='SSM Agent',
            MetricData=[
                {
                    'MetricName': 'Status',
                    'Dimensions': [
                        {
                            'Name': 'Per-Instance Metrics',
                            'Value': metric_dimension_value
                        }
                    ],
                    'Value': metric_value,
                    'Unit': 'Count'
                },
            ]
        )
    except ClientError as e:
        print(e)

def lambda_handler(event, context):
    status_map = {
        'Online': 0,
        'ConnectionLost': 1,
        'Inactive': 2
    }

    client = boto3.client('ssm')
    paginator = client.get_paginator('describe_instance_information')
    pages = paginator.paginate()

    for page in pages:
        for instance in page['InstanceInformationList']:
            send_cw_metric(instance['InstanceId'], status_map[instance['PingStatus']])

関数名: LambdaCreateCWAlarm

SSM(Systems Manager)のマネージドノードの状態を CloudWatch メトリクスとして送信する処理を実装します。各マネージドノードに関連付けられた CloudWatch アラームが作成され、インスタンスの状態が閾値以上になった場合に通知が送信されます。
アタッチする実行ロールは、先ほど作成した "LambdaCreateCWAlarmRole" を選択します。

サンプルコード
import boto3, os
from botocore.exceptions import ClientError

def create_cw_alarm(instance_id):
    client = boto3.client('cloudwatch')
    alarm_name = 'ssm-agent-' + instance_id
    client.put_metric_alarm(
        AlarmName=alarm_name,
        AlarmActions=[
            os.environ['CWAlarmSNSTopic']
        ],
        MetricName='Status',
        Namespace='SSM Agent',
        Statistic='Maximum',
        Dimensions=[
            {
                'Name': 'Per-Instance Metrics',
                'Value': instance_id
            },
        ],
        Period=60,
        EvaluationPeriods=1,
        DatapointsToAlarm=1,
        Threshold=1.0,
        ComparisonOperator='GreaterThanOrEqualToThreshold',
        TreatMissingData='missing'
    )

def lambda_handler(event, context):
    ssm_client = boto3.client('ssm')
    managed_nodes_response = ssm_client.describe_instance_information()
    managed_node_ids = [instance['InstanceId'] for instance in managed_nodes_response['InstanceInformationList']]

    client = boto3.client('cloudwatch')
    paginator = client.get_paginator('list_metrics')
    pages = paginator.paginate(
        Namespace='SSM Agent',
        MetricName='Status',
        Dimensions=[
            {
                'Name': 'Per-Instance Metrics'
            },
        ],
    )

    for page in pages:
        for metric in page['Metrics']:
            for dimension in metric['Dimensions']:
                instance_id = dimension['Value']
                if instance_id in managed_node_ids:
                    create_cw_alarm(instance_id)

環境変数を利用しているので、設定 ⇒ 環境変数 画面より以下を記述します。

  • キー: CWAlarmSNSTopic
  • 値: 作成した SNS arn

EventBridge

ルール名: LambdaCheckSSMAgentStatusRule

イベントバスは default で、ルールタイプはスケジュールを選択します。

通常のレートで実行されるスケジュール (10 分ごとなど)。を選択します。
rate 式は 1 分 (minutes) に設定します。

ターゲットのセクションで、ターゲットタイプを AWS のサービスを選択し、ターゲットを選択では Lambda 関数 を選択します。
先ほど作成した関数名 "LambdaFunctionCheckSSMAgentStatus" を選択してルール作成を行えば終了です。

ルール名: LambdaCreateCWAlarmRule

先に案内した手順および同様の設定です。 関数名 "LambdaCreateCWAlarm" を選択してルール作成を行えば終了です。

以上で実装は終了です。お疲れさまでした。

検証してみた

フリートマネージャーのマネージドノードに表示されるように、今回は 4 台 の EC2 インスタンスを起動します。

CloudWatch アラームのコンソール画面でも対象マネージドノードが表示されていることを確認しました。

各マネージドノードにはタグを付け、以下 AWS re:Post を参考にマネージドノードに必要な条件を削除(変更)して Ping のステータスが "接続が失われました (Connection Lost)" となり検知されるか確認しました。

  • タグ: Metadata
    インスタンスメタデータサービスの無効化

  • タグ: Security-group
    条件を満たさないプロトコルとポートを設定したセキュリティグループに変更

  • タグ: Agent-Stop
    SSM Agent を Stop

  • タグ: Roll
    IAM ロールのデタッチ

上記の様に対応後、暫く様子を見ました。
先に SSM Agent を Stop したマネージドノードと、セキュリティグループを変更したマネージドノードが "接続が失われました (Connection Lost)" と状態変化しました。 また、それと同時刻付近でアラーム状態になることも確認できましたので、成功です。

アラーム状態となったので、SNS で設定した通知先で通知内容を確認しました。 以下は通知例です。

※一部マスク処理します。
You are receiving this email because your Amazon CloudWatch Alarm "ssm-agent-i-0572676272d863124" in the Asia Pacific (Tokyo) region has entered the ALARM state, because "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (12/04/24 08:27:00)] was greater than or equal to the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition)." at "Friday 12 April, 2024 08:28:26 UTC".
View this alarm in the AWS Management Console:
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/deeplink.js?region=ap-northeast-1#alarmsV2:alarm/ssm-agent-i-0572676272d863124
Alarm Details:
- Name:                       ssm-agent-i-0572676272d863124
- Description:              
- State Change:               OK -> ALARM
- Reason for State Change:    Threshold Crossed: 1 out of the last 1 datapoints [1.0 (12/04/24 08:27:00)] was greater than or equal to the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition).
- Timestamp:                  Friday 12 April, 2024 08:28:26 UTC
- AWS Account:                123456789012
- Alarm Arn:                  arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:ssm-agent-i-0572676272d863124
Threshold:
- The alarm is in the ALARM state when the metric is GreaterThanOrEqualToThreshold 1.0 for at least 1 of the last 1 period(s) of 60 seconds.
Monitored Metric:
- MetricNamespace:                     SSM Agent
- MetricName:                          Status
- Dimensions:                          [Per-Instance Metrics = i-0572676272d863124]
- Period:                              60 seconds
- Statistic:                           Maximum
- Unit:                                not specified
- TreatMissingData:                    missing
State Change Actions:
- OK:
- ALARM: [arn:aws:sns:ap-northeast-1:123456789012:SSM-Ping]
- INSUFFICIENT_DATA:
--
If you wish to stop receiving notifications from this topic, please click or visit the link below to unsubscribe:
https://sns.ap-northeast-1.amazonaws.com/unsubscribe.html?SubscriptionArn=arn:aws:sns:ap-northeast-1:123456789012:SSM-Ping:87654321-xxxx-aaaa-0987-1234567890ab&Endpoint=xxxxxxxxxxxx@gmail.com
Please do not reply directly to this email. If you have any questions or comments regarding this email, please contact us at https://aws.amazon.com/support


上記と同じタイミングで、インスタンスメタデータサービスの無効化と、IAM ロールのデタッチをしたものの、内部的な仕様なのか対象マネージドノードが "接続が失われました (Connection Lost)" になりませんでした。
料金の関係上、タグ: Agent-Stop と Security-group のマネージドノード(EC2 インスタンス)は削除して経過観察しました。
暫くすると、残りのマネージドノードがほぼ同じタイミングで "接続が失われました (Connection Lost)" となりましたので、成功です。

補足

先の検知時間は 2024-04-12 08:28:26 UTC でした。
インスタンスメタデータサービスの無効化と、IAM ロールのデタッチをしたノードの検知時間は 2024-04-12 09:15:38 UTC 頃でした。
そのため、45 分程度の遅れも許容可能な監視であればご活用ください。

注意点

本機能は正常なマネージドノードから、Ping のステータスが "接続が失われました (Connection Lost)" へ状態変化があったことを検知します。
そのため、正常なマネージドノードの状態を監視していることが前提です。
既に Ping のステータスが "接続が失われました (Connection Lost)" の状態であるマネージドノードは検知されません。 例えば、インスタンスメタデータサービスの無効化後に、停止(stop)・起動(start) した場合、マネージドノードとしてコンソール上では表示はされているものの、非マネージドノードとして判断がされるためマネージドノードとして監視されませんでした。(検証済み)
上記認識の元、ご利用いただければ幸いです。

また、遅延して上手く処理がされないといった状況であれば、Lambda 関数の一般設定より "タイムアウト値" を伸ばす、"メモリ" を増やすなどのご対応をしてください。 恥ずかしながらデフォルト値で検証していたので、上手く処理がされず苦戦しました。しかしながら、上記を増やすことで問題なく挙動しました。

まとめ

本ブログが誰かの参考となれば幸いです。

参考資料

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。