👀

EC2(Windows)のサービスをCloudWatchで監視したい

2022/10/23に公開

Windowsサービスを監視したい

EC2(Windows)のWindowsサービスをCloudWatchで監視する仕組みを作りました。
現在のCloudWatch Agentはプロセス監視はできますが、サービス監視はできないようです。
サービスに紐づくプロセスを監視する方法もありますが、プロセスの起動数などの仕様がわからないと適切な監視設定ができません。
そこで、サービスの状態はPowerShellコマンドで取得し、その結果をもとにCloudWatchのカスタムメトリクスとしてCloudWatchに出力することにしました。

構成

構成はこのようになります。

Windows Server 2022 にインストールしたアプリのバージョン

PS C:\> python --version
Python 3.10.8
PS C:\> aws --version
aws-cli/2.8.5 Python/3.9.11 Windows/10 exec-env/EC2 exe/AMD64 prompt/off

前提条件

紹介するスクリプトservicech.py実行に必要な前提条件です。

  • 監視対象Windows環境のPythonが使用可能
    • 前述の構成で記載したバージョンかそれ以降で動作すると思います。
    • スクリプト内で使用しているsubprocessはバージョンにより一部仕様が異なるようなので注意ください。
  • 監視対象Windows環境のaws cliが使用可能
    • WindowsからCloudWatchへのカスタムメトリクス送信はスクリプト内でAWS CLIコマンドを使用しています。
  • 監視対象Windows環境のEC2からCloudWatchへのアクセスできる
    • EC2にはCloudWatchアクセス権限があるIAMロールを付与しています。

スケジュール設定

Windowsのタスクスケジューラでスクリプトを定期的に実行できるようにします。

タスクの作成をクリックし、操作タグの新規ボタンで実行するプログラムを設定します。

  • ① プログラム/スクリプト
    • C:\Users\Administrator\AppData\Local\Programs\Python\Python310\python.exe
  • ② 引数の追加
    • "C:\Users\Administrator\Desktop\test\servicech.py"
  • ※参考※
    • python.exeのパスは、コマンドプロンプトでwhere pythonコマンドと実行すると確認できます。
    • servicech.pyは今回作成したスクリプトです。

スクリプトの説明

概要

指定したサービスの状態がRunningの場合に1を、それ以外は0になります。1つのカスタムメトリクスには、複数のサービスを監視対象にできます。

カスタムメトリクスの挙動

0 異常、1 正常】という設計です。

メトリクスの値 条件
0 指定したサービスの状態が1つでもRunning以外の場合。また、指定したサービスのうち1つでもサービスが存在しない場合
1 指定したサービスの状態が全てRunningの場合

ディメンジョン

ディメンジョンはEC2インスタンスのインスタンスIDです。

カスタムメトリクスの設定

カスタムメトリクスの設定はソースコードの最初にある以下のコードで行います。

metrics_target_dict = {
  "WINDOWS_win-service-01": ["AmazonSSMAgent", "CoreMessagingRegistrar"],
  "WINDOWS_win-service-02": ["Power", "Spooler"]
}

サンプルで作成されるカスタムメトリクス

ネームスペース名 カスタムメトリクス名 Windowsサービス(群)
WINDOWS win-service-01 AmazonSSMAgent, CoreMessagingRegistrar
WINDOWS win-service-02 Power, Spooler
  • ※注意事項※
    • スクリプトのカスタムメトリクスの設定を編集する場合は、ネームスペース名とカスタムメトリクス名の間に_ (アンダースコア)が必要です。
    • このスクリプトでは、ネームスペース名とカスタムメトリクス名に、_ (アンダースコア)は使用しないでください。_ (アンダースコア)を使用すると、CloudWatchカスタムメトリクスの情報が意図した設定にならない可能性があります。

Windowsのサービス名について

スクリプトservicech.pyに登録するWindowsのサービス名は、Windowsのサービスか、PowerShellのGet-Serviceコマンドで確認できます。

スクリプト

スクリプトservicech.pyのソースです。

servicech.py
import subprocess
import re

# カスタムマトリクス名と関連するWindowsサービス名
# ネームスペース名_メトリクス名
metrics_target_dict = {
  "WINDOWS_win-service-01": ["AmazonSSMAgent", "CoreMessagingRegistrar"],
  "WINDOWS_win-service-02": ["Power", "Spooler"]
}

# カスタムメトリクスの値を格納
metrics_status_dict = {}  # 1:OK, 0:NG
# 情報取得が必要なWindowsサービス名
service_target_dict = {}
# コマンド実行時のWindowsサービスごとの状態を格納
service_current_dict = {}
# Windowsサービス確認時の除外対象
SKIPLIST = ["Name", "----"]
# windowsサービス名
tmp_service_name = ""
# windowsサービスの状態
tmp_service_status = ""
# カスタムメトリクスの状態
tmp_metrics_value = 0
# EC2インスタンスのリージョン
tmp_region_name = ""
# EC2インスタンスのID
tmp_incetanceid = ""
# メトリクスのネームスペース名
tmp_namespace_name = ""
# メトリクスの名前
tmp_metrics_name = ""
# メトリクスの値(1:OK, 0:NG)
tmp_metrics_value = ""
# OSコマンド
tmp_cmd = ""
# --------------------------------------- #
# FUNCTIONS


def exec_os_cmd(cmd):
    """OSコマンドを実行し標準出力を返す"""
    cmd_result = subprocess.run(
        cmd, stdout=subprocess.PIPE,
        stderr=subprocess.PIPE, shell=True, check=True
    )
    return cmd_result


def set_service_target_dict():
    """service_target_dictに監視対象のWindowsサービス名を格納"""
    for key_lo, metrics_lo in metrics_target_dict.items():
        for service_name_lo in metrics_lo:
            service_target_dict[service_name_lo] = ""


# --------------------------------------- #
# ROOT FLOW

# service_target_dictに監視対象のWindowsサービス名を格納
set_service_target_dict()

tmp_cmd = "powershell -Command Get-Service"
# windowsサービス名と状態を表示するコマンド実行
res = exec_os_cmd(tmp_cmd)
# コマンド実行結果を改行区切りでリストsvlistに格納
svlist = res.stdout.decode("cp932", "ignore").split("\n")

for sv in svlist:
    tmp_service_name = ""
    tmp_service_status = ""

    # 連続する空白を`,`(コロン)にする
    sv = re.sub('[  ]{2,}', ',', sv)
    # 前後の空白を削除
    sv = sv.strip()
    # `,`(コロン)で区切りカラムごとの値をリスト culumns に格納
    culumns = sv.split(",")

    for index, culumn in enumerate(culumns):
        if index == 0:
            tmp_service_status = culumn
        if index == 1:
            tmp_service_name = culumn

    # サービス名ではない情報を除外
    for sk in SKIPLIST:
        if sk == tmp_service_name:
            tmp_service_name = ""
    # サービス名と状態を辞書service_current_dictに格納
    if tmp_service_name != "":
        service_current_dict[tmp_service_name] = tmp_service_status

# 辞書service_current_dictとservice_target_dict両方にあるサービス検出
intersection_keys = service_target_dict.keys() & service_current_dict.keys()

for serv in intersection_keys:
    print(f"{serv} {service_current_dict[serv]}")

for key, metrics in metrics_target_dict.items():
    tmp_metrics_value = 1
    print(f"{key=} {metrics=}")
    for tmp_service_name in metrics:
        if tmp_service_name in service_current_dict:
            """監視対象サービスが存在する場合"""
            if service_current_dict[tmp_service_name] != "Running":
                tmp_metrics_value = 0
                break
        else:
            """監視対象サービスが存在しない場合"""
            tmp_metrics_value = 0
            break
    metrics_status_dict[key] = tmp_metrics_value

tmp_cmd = """powershell -Command Invoke-RestMethod
 -Headers @{'X-aws-ec2-metadata-token-ttl-seconds' = '21600'}
 -Method PUT -Uri http://169.254.169.254/latest/api/token
"""
# 改行コード削除
tmp_cmd = tmp_cmd.replace("\n", "")

res = exec_os_cmd(tmp_cmd)
token = res.stdout.decode("cp932", "ignore").rstrip()
print(f"{token=}")

tmp_cmd = f"""powershell -Command Invoke-RestMethod
 -Headers @{{'X-aws-ec2-metadata-token' = '{token}'}}
 -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id
"""
# 改行コード削除
tmp_cmd = tmp_cmd.replace("\n", "")

res = exec_os_cmd(tmp_cmd)
instanceid = res.stdout.decode("cp932", "ignore").rstrip()

for key, tmp_metrics_value in metrics_status_dict.items():
    print(f"{key=} {tmp_metrics_value=}")
    tmp_region_name = "ap-northeast-1"
    tmp_incetanceid = instanceid
    tmp_namespace_name = key.split("_")[0]
    tmp_metrics_name = key.split("_")[1]
    tmp_metrics_value = tmp_metrics_value

    tmp_cmd = f"""powershell -Command aws cloudwatch put-metric-data
    --region {tmp_region_name}
    --namespace '{tmp_namespace_name}'
    --metric-name '{tmp_metrics_name}'
    --value {tmp_metrics_value}
    --unit 'Count'
    --dimensions 'InstanceId={tmp_incetanceid}'
    """
    # 改行コード削除
    tmp_cmd = tmp_cmd.replace("\n", "")
    print(f"=== {tmp_cmd}")
    res = exec_os_cmd(tmp_cmd)
    res = res.stdout.decode("cp932", "ignore").rstrip()
    print(f"{res=}")

実行結果

このようにCloudWatchにカスタムメトリクスの情報が表示されたら成功です。

ふぅ

カスタムメトリクスを作ったことがなかったので苦労しましたが、なんとかスクリプトが作れました。EC2のインスタンスメタデータ(*1)にリージョン情報(今回はap-northeast-1)がないので静的な値としてスクリプトに記載していますが、これも環境から取得する方がよかったかも。

作成したスクリプトはPEP8(*2)に準拠するよう頑張ってみました。PEP8に反してると、チェックツールからやたら指摘されていい勉強になりました。

Discussion