ちょっと話題の記事

CloudWatch Agentのバージョン取得を自動化したいのでいろいろ試してみた

2022.07.09

しばたです。

EC2を運用しているとSSM AgentやCloudWatch Agentなどのエージェントプログラムを定期的に更新する必要があります。

更新作業自体はSSM Run CommandやSSM State Managerを使うと比較的容易に行えますが、その前段として「今どのバージョンを使っているか?」の管理も必要です。
SSM AgentはSSMの根幹のエージェントなのでマネジメントコンソールの画面やCLIから容易にバージョン情報を参照できます。 *1

(フリートマネージャーからマネージドインスタンスの一覧といっしょにSSM Agentのバージョンが取得できる)

CLIならこんな感じのコマンドで一発取得できます。

# AWS CLIでSSM Agentのバージョン一覧を取得
aws ssm describe-instance-information \
    --query "sort_by(InstanceInformationList, &ComputerName)[].{ComputerName: ComputerName, InstanceId: InstanceId, Platform: PlatformType, SSMAgentVersion: AgentVersion}" \
    --output table

対してCloudWatch AgentはAWS謹製のエージェントではあるもののバージョン情報を取得するのは一筋縄ではいきませんでした。

本記事では私が試した方法と最終的に採った方法について説明していきます。

方法1. SSM Inventoryを使う

最初に試したのはSSM Inventoryを使うことでした。
社内の有識者から「SSM InventoryならCloudWatch Agentの情報も登録されるよ」とのアドバイスを受け、「それなら楽だ」と思い直ぐ試してみました。

SSM Inventoryはマネージドインスタンス内部にインストールされているアプリケーション等の情報を定期的に取得・更新して管理できるサービスです。
取得可能な情報はオプションで選択可能ですがCloudWatch Agentは「Applications」で取得可能です。

(インベントリのパラメーターで「Application」を選べばよい)

インベントリを作成してしばらくまてば各インスタンスの「インベントリ」欄からアプリケーション情報を取得できます。

(Linuxインスタンスの場合)

(Windowsインスタンスの場合)

CLIだと各インスタンスに対してaws ssm list-inventory-entriesコマンドを使うことで取得できます。
Linux用エージェントとWindows用エージェントで微妙に設定内容が異なるので少しだけ注意が必要です。

項目 Linux Agent Windows Agent
名前 amazon-cloudwatch-agent Amazon CloudWatch Agent
アプリケーションタイプ Applications/CloudWatch-Agent - (未設定)
# Linux、Windows両方に対応するには名前でOR検索するしかない感じ
aws ssm describe-instance-information \
    --query "sort_by(InstanceInformationList, &ComputerName)[].[InstanceId, PlatformType]" \
    --output text | awk 'BEGIN {print "InstanceId, Platform, Version"} {
        "aws ssm list-inventory-entries --instance-id "$1" --type-name \"AWS:Application\" --filters \"Key=Name,Values=Amazon CloudWatch Agent, amazon-cloudwatch-agent\" --query \"Entries[0].Version\" --output text" | getline version
        print $1", "$2", "version
    }' | column -t -s ','

# 補足 : AWK内部で実行してるコマンドは複数行に分けるとこんな感じ
aws ssm list-inventory-entries --instance-id $1 \
    --type-name "AWS:Application" \
    --filters "Key=Name,Values=Amazon CloudWatch Agent, amazon-cloudwatch-agent" \
    --query "Entries[0].Version" \
    --output text

Windows用エージェントのバージョンがおかしい

「これで無事完了!」と思ってたのですが、よく見るとWindows Agentのバージョンの値が1.3.50744と違和感のある値になっています。
詳細は後述しますが、残念ながらこの値は間違っており、SSM InventoryだとWindows Agentのバージョンは取れないことが分かりました...

なおLinux Agentであれば正しいバージョン番号を取得できます。

方法2. シェルスクリプト/PowerShellスクリプトを使う

それでは「CloudWatch Agentの正しいバージョン番号の取得方法は何だろう?」と気になり、調べたところ以下のドキュメントに記載されていました。

こちらにある通りCloudWatch AgentのバージョンはOS内部にあるamazon-cloudwatch-agent-ctlコマンドから取るのが正解とのことでした。

# Linux環境の場合
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status

# Windows環境の場合
& $Env:ProgramFiles\Amazon\AmazonCloudWatchAgent\amazon-cloudwatch-agent-ctl.ps1 -m ec2 -a status

加えて前節のWindows Agentのバージョンについても

このコマンドを使用することは、CloudWatch エージェントのバージョンを検索する正しい方法です。
コントロールパネルの [Programs and Features (プログラムと機能)] を使用すると、誤ったバージョン番号が表示されます。

と思いっきり書いています。
SSM Inventoryから取得されるバージョン番号はこっち(インストーラーのプログラムバージョン)とのことでした。

というわけでCloudWatch Agentのバージョンを取得するにはOS内部でコマンドを実行するしかない感じです。

最低限のチェック処理を入れてやり、以下の様にすれば正しいCloudWatch Agentのバージョンを取得できます。

Linux環境 (Bash)の場合

#!/bin/bash

# バージョン取得関数
get_cwagent_version () {
    if [ -f /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl ]
    then
        sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status | grep '"version":' | awk -F ":" '{gsub(/[ \"]/, "", $2);print $2}'
    else
        echo "Not installed"
    fi
}
get_cwagent_version

SSM Sessionから簡単に動作確認するとこんな感じです。

sh-4.2$ get_cwagent_version
1.247352.0b251908

Windows環境 (PowerShell)の場合

# バージョン取得関数
function Get-CloudWatchAgentVersion () {
    $agentCtlPath = "$env:ProgramFiles\Amazon\AmazonCloudWatchAgent\amazon-cloudwatch-agent-ctl.ps1"
    if (-not (Test-Path -LiteralPath $agentCtlPath)) {
        return "Not installed"
    }
    return (& $agentCtlPath -m ec2 -a status ) | ConvertFrom-Json | Select-Object -ExpandProperty version
}
Get-CloudWatchAgentVersion

こちらもSSM Sessionから簡単に動作確認するとこんな感じです。

PS C:\> Get-CloudWatchAgentVersion
1.247352.0b251908

今度はちゃんと正しいバージョン(1.247352.0b251908)が取得できています。

補足 : よりシンプルな方法

ちなみにamazon-cloudwatch-agent-ctlコマンドは実装がシェルスクリプト/PowerShellスクリプトであり、ソースを読んでみるとバージョン番号は所定の設定ファイルの内容を読み込んでいるだけでした。

  • Liunx : /opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION
  • Windows : "$env:ProgramFiles\Amazon\AmazonCloudWatchAgent\CWAGENT_VERSION"

このため将来の互換性などを気にしないで良いのであれば以下の様によりシンプルに実装することも可能です。

# Linux : Bash
get_cwagent_version () {
    if [ -f /opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION ]
    then
        cat /opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION
    else
        echo "Not installed"
    fi
}
get_cwagent_version

# Windows : PowerShell
function Get-CloudWatchAgentVersion () {
    $versionFilePath = "$env:ProgramFiles\Amazon\AmazonCloudWatchAgent\CWAGENT_VERSION"
    if (-not (Test-Path -LiteralPath $versionFilePath)) {
        return "Not installed"
    }
    return @(Get-Content -LiteralPath $versionFilePath)[0]
}
Get-CloudWatchAgentVersion

CloudWatch Agent本体の仕組みやamazon-cloudwatch-agent-ctlコマンドの実装がいつ変わるかわかりませんのでドキュメントに記載されている方法を使うほうが安全ではあります。
環境に応じて良い感じの方法を選ぶと良いでしょう。

自動化を検討する

記事公開後に以下のTypoを修正しています。
例示しているコードとスクリーンショットが一致しない場合があります。
・My-GathreCustomInventory → My-GatherCustomInventory
・cloudwath-agent.json → cloudwatch-agent.json

ここまでの内容を踏まえてどう自動化していくかを考えます。

最初に試したSSM Inventoryは非常に便利なのでこの仕組みは使っていきたいです。
バージョンがおかしいのはWindows Agentでしたので、最低限Windows Agentだけカスタムインベントリを使うという方法で問題を改善できそうですが、せっかくなのでLinux Agentもカスタムインベントリを使い、

  • Linux Agent / Windows Agent共に共通化された情報を保持する様にする
  • CloudWatch Agentがインストールされていない場合に「未インストール」であることを明示する

様にした方がより便利そうです。

カスタムインベントリの使い方はこちらのAWSブログの内容が非常に参考になります。

カスタムインベントリでは各インスタンスの所定のパスにJSONファイルを配置することでインベントリデータとすることができます。

  • Linux : /var/lib/amazon/ssm/インスタンスID/inventory/custom/アプリケーション名.json
  • Windows : "C:\ProgramData\Amazon\SSM\InstanceData\インスタンスID\inventory\custom\アプリケーション名.json"

SSM Documentの登録

上記ブログで参照されているこちらのSSM Documentをそのまま使い自分のAWS環境に登録しておきます。

# YAMLダウンロード
curl https://raw.githubusercontent.com/aws-samples/aws-systems-manager-custom-inventory-log4j-example/main/customInventoryLog4jDocument.yml --output ./document.yaml

# SSM Document登録 : 今回は My-GatherCustomInventory という名前にします
aws ssm create-document --content file://./document.yaml --name "My-GatherCustomInventory" --document-type Command --document-format YAML
# 結果確認
aws ssm list-documents --document-filter-list key=Name,value=My-GatherCustomInventory

登録後はこんな感じです。

続けてインベントリデータを書き込むスクリプトを以下の様に用意します。
Linux用、Windows用それぞれ必要です。

Linux用スクリプト

#!/bin/bash

# Get CloudWatch Agent version
get_cwagent_version () {
    if [ -f /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl ]
    then
        sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status | grep '"version":' | awk -F ":" '{gsub(/[ \"]/, "", $2);print $2}'
    else
        echo "Not installed"
    fi
}

# Update inventory JSON data
# $1 : Filename
# $2 : Application Name
# $3 : Type
# $4 : Version
update_custom_inventory () {
    # find inventory path
    if [ "$(curl -sL -w '%{http_code}' 169.254.169.254/latest/meta-data/instance-id -o /dev/null)" = "200" ]; then
        local instanceId=$(curl 169.254.169.254/latest/meta-data/instance-id)
        local inventoryPath=(/var/lib/amazon/ssm/$instanceId/inventory/custom)
    else
        local hybridDirectory=$(find /var/lib/amazon/ssm -name "mi-*")
        local inventoryPath=($hybridDirectory/inventory/custom)
    fi

    # update inventory JSON (overwrite)
    local json=$(cat <<EOM
{
    "SchemaVersion": "1.0",
    "TypeName": "Custom:MyApplications",
    "Content": {
        "Name": "$2",
        "Type": "$3",
        "Version": "$4"
    }
}
EOM
    )
    echo "$json" > "$inventoryPath/$1"
}

# Main
version=$(get_cwagent_version)
update_custom_inventory "cloudwatch-agent.json" "Amazon CloudWatch Agent" "Applications/CloudWatch-Agent" "$version"

Windows用スクリプト

# Get CloudWatch Agent version
function Get-CloudWatchAgentVersion () {
    $agentCtlPath = "$env:ProgramFiles\Amazon\AmazonCloudWatchAgent\amazon-cloudwatch-agent-ctl.ps1"
    if (-not (Test-Path -LiteralPath $agentCtlPath)) {
        return "Not installed"
    }
    return (& $agentCtlPath -m ec2 -a status ) | ConvertFrom-Json | Select-Object -ExpandProperty version
}

# Update inventory JSON data
function Set-CustomInventory ([string]$FileName, [string]$ApplicationName, [string]$ApplicationType, [string]$Version) {
    # find inventory path
    $instanceId = Invoke-RestMethod -Uri http://169.254.169.254/latest/meta-data/instance-id
    if ($null -ne $instanceId) {
        $inventoryPath = "C:\ProgramData\Amazon\SSM\InstanceData\" + $instanceId + "\inventory\custom\"
    } else {
        $hybridInstanceId = (Get-Content -Path "C:\ProgramData\Amazon\SSM\InstanceData\registration" | ConvertFrom-Json).ManagedInstanceId
        $inventoryPath = "C:\ProgramData\Amazon\SSM\InstanceData\" + $hybridInstanceId + "\inventory\custom\"
    }

    # update inventory JSON (overwrite)
    $content = @"
{
    "SchemaVersion": "1.0",
    "TypeName": "Custom:MyApplications",
    "Content": {
        "Name": "$ApplicationName",
        "Type": "$ApplicationType",
        "Version": "$Version"
    }
}
"@
    Set-Content -Path (Join-Path $inventoryPath $FileName) -Value $content
}

# Main
$params = @{
    FileName        = "cloudwatch-agent.json"
    ApplicationName = "Amazon CloudWatch Agent"
    ApplicationType = "Applications/CloudWatch-Agent"
    Version         = Get-CloudWatchAgentVersion
}
Set-CustomInventory @params

SSM State Managerに登録

スクリプトを用意したらAWSブログの記事にある通りにMy-GatherCustomInventoryをSSM State Managerに登録して定期実行(+インベントリ更新)する様設定します。

My-GatherCustomInventoryのパラメーターはこんな感じ。

「PowerShell Commands」「Shell Commands」にそれぞれのスクリプトを転記します。
下の欄はスクリプト実行後に同時に更新するインベントリ情報になるのですが、今回は「Custom Inventory」だけ同時に更新する様にしています。

その他のパラメーターは環境に応じて設定します。
今回は「手動で対象インスタンスを設定」「デフォルト間隔 (30分ごと)」で試してます。

State Managerの実行が上手くいった場合、各インスタンスのインベントリに「Custom:MyApplications」が増えCloudWatch Agentの情報が参照できる様になります。

Windows Agentのバージョンもバッチリ正しい値を取れています。

CLIからの取得もより汎用的になりました。

# Custom InventoryならLinux, Windows共通設定にできるので検索もしやすい。
aws ssm describe-instance-information \
    --query "sort_by(InstanceInformationList, &ComputerName)[].[InstanceId, PlatformType]" \
    --output text | awk 'BEGIN {print "InstanceId, Platform, Version"} {
        "aws ssm list-inventory-entries --instance-id "$1" --type-name \"Custom:MyApplications\" --filters \"Key=Type,Values=Applications/CloudWatch-Agent\" --query \"Entries[0].Version\" --output text" | getline version
        print $1", "$2", "version
    }' | column -t -s ','

# 補足 : AWK内部で実行してるコマンドは複数行に分けるとこんな感じ
aws ssm list-inventory-entries --instance-id $1 \
    --type-name "Custom:MyApplications" \
    --filters "Key=Type,Values=Applications/CloudWatch-Agent" \
    --query "Entries[0].Version" \
    --output text

ちなみにCloudWatch AgentをインストールしていないEC2を追加した場合はこんな感じになります。

最後に

以上となります。

ちょっと手間取りましたが良い感じに自動化できました。
SSM Inventoryのカスタムインベントリを使う方法はCloudWatch Agent以外のアプリケーションでも使えますので、例えばですが社内アプリケーションといった独自アプリケーションの管理に使うと良いのではないでしょうか。

本記事の内容が皆さんの役に立てば幸いです。

脚注

  1. 「対象EC2が起動していること」という前提条件はありますが、今回の本筋ではないのでその辺はスルーします