EC2 インスタンスの再起動完了を CloudWatch カスタムメトリクス表示させるようにユーザーデータで実装してみた

EC2 インスタンスの再起動完了を CloudWatch カスタムメトリクス表示させるようにユーザーデータで実装してみた

Clock Icon2025.07.02

はじめに

テクニカルサポートの 片方 です。
AWS の仕様上、OS 側でシャットダウンや再起動といった操作をされた場合、Amazon EC2 API コールをイベントとしてキャプチャできません。
そのため、CloudTrail などにイベント履歴として証跡が残らず、再起動完了の検知が困難になります。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-reboot.html

インスタンスからオペレーティングシステムの再起動コマンドを実行する代わりに、Amazon EC2 コンソール、コマンドラインツール、または Amazon EC2 API を使用してインスタンスを再起動することをお勧めします。Amazon EC2 コンソール、コマンドラインツール、または Amazon EC2 API を使用してインスタンスを再起動する場合、インスタンスが数分以内に完全にシャットダウンしないと、ハードリブートが実行されます。AWS CloudTrail を使用しながら、Amazon EC2 によりインスタンスを再起動した場合は、インスタンスがいつ再起動されたかについての API レコードが作成されます。

しかしながら、パッチ適用やアプリケーション更新など、様々な理由で OS 内から再起動を実施する必要があるケースもあるかと思います。
本ブログでは、ユーザーデータを使って EC2 インスタンスの再起動完了を CloudWatch カスタムメトリクスとして表示させる方法をご紹介します。この方法により OS 内部からの再起動であっても、その完了を AWS 側で確認可能になります。

やってみた

今回のご紹介する方法では AWS CLI コマンドを使用して直接 CloudWatch にカスタムメトリクスを送信しています。インスタンスが再起動すると、OS 起動時に自動実行されるスクリプトが aws cloudwatch put-metric-dataコマンドを実行し、再起動完了を示すメトリクスを CloudWatch に送信します。これにより、インスタンスの再起動完了を CloudWatch で視覚的に確認できるようになります。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/PublishMetrics.html

前提条件

実現には以下の条件が必要です。権限については、CloudWatchFullAccessV2 ポリシーなどを EC2 にアタッチしているロールに付与してください。

  • AWS CLI インストール
  • cloudwatch:PutMetricData といった権限

ユーザーデータ

EC2 コンソールで Launch Wizard を使用してインスタンスを起動するときに、ユーザーデータを指定してください。
01

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html

Linux OS

検証には Amazon Linux 2023 を利用しました。
Amazon Linux 2023 は AWS CLI がインストール済みのため、異なるディストリビューションの場合は事前に AWS CLI をインストールするか、下記のユーザーデータスクリプトにインストールするよう組み込んでください。

#!/bin/bash

# ログ出力の設定
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1

echo "Starting user data script execution..."

# 必要なパッケージの更新(AWS CLIは通常プリインストールされています)
dnf update -y
dnf install -y curl jq

# IAMロールが適切に設定されているか確認
echo "Checking IAM role..."
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_PROFILE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/info)
if [[ $INSTANCE_PROFILE == *"cloudwatch:PutMetricData"* ]]; then
  echo "IAM role with CloudWatch permissions detected."
else
  echo "Warning: IAM role may not have proper CloudWatch permissions."
fi

# インスタンスの詳細情報を取得
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region)
if [ -z "$REGION" ]; then
  # 古いメタデータ形式の場合
  AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
  REGION=$(echo $AZ | sed 's/[a-z]$//')
fi

echo "Instance ID: $INSTANCE_ID"
echo "Region: $REGION"

# 起動時に毎回実行されるスクリプトを作成
cat > /usr/local/bin/report-reboot-complete.sh << 'EOF'
#!/bin/bash

# CloudWatch メトリクス用の変数
NAMESPACE="ServerManagement"
METRIC_NAME="RebootComplete"
INSTANCE_ID=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" | xargs -I {} curl -s -H "X-aws-ec2-metadata-token: {}" http://169.254.169.254/latest/meta-data/instance-id)
REGION=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" | xargs -I {} curl -s -H "X-aws-ec2-metadata-token: {}" http://169.254.169.254/latest/meta-data/placement/region)
if [ -z "$REGION" ]; then
  AZ=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" | xargs -I {} curl -s -H "X-aws-ec2-metadata-token: {}" http://169.254.169.254/latest/meta-data/placement/availability-zone)
  REGION=$(echo $AZ | sed 's/[a-z]$//')
fi

# 現在のタイムスタンプを取得
TIMESTAMP=$(date +%s)

# CloudWatch メトリクスをプッシュする
aws cloudwatch put-metric-data \
    --namespace "$NAMESPACE" \
    --metric-name "$METRIC_NAME" \
    --dimensions InstanceId="$INSTANCE_ID" \
    --value 1 \
    --timestamp $TIMESTAMP \
    --unit Count \
    --region "$REGION"

# ログに記録
logger "Reboot complete metric pushed for instance $INSTANCE_ID at $(date)"
EOF

# スクリプトに実行権限を付与
chmod +x /usr/local/bin/report-reboot-complete.sh

# systemdサービスとして登録
cat > /etc/systemd/system/reboot-complete.service << 'EOF'
[Unit]
Description=Report EC2 reboot completion to CloudWatch
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/report-reboot-complete.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

# サービスを有効化
systemctl daemon-reload
systemctl enable reboot-complete.service
systemctl start reboot-complete.service

# 初回実行時のログ
echo "Initial service execution completed."
systemctl status reboot-complete.service

echo "User data script execution completed."

※ 適宜修正の上ご利用ください

Windows OS

検証には Windows Server 2022 を利用しました。
Windows AMI には基本的に AWS CLI はインストールされていないため、ユーザーデータスクリプト実行時にインストールさせています。

<powershell>
# ログ設定
$logFolder = "C:\CloudWatchSetup"
New-Item -Path $logFolder -ItemType Directory -Force
Start-Transcript -Path "$logFolder\UserData_Log.txt" -Append -Force

Write-Output "Starting user data script execution... $(Get-Date)"

# TLS 1.2を有効化
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# AWS CLI のインストール(Windows には必須)
Write-Output "Downloading AWS CLI installer..."
$awsCliUrl = "https://awscli.amazonaws.com/AWSCLIV2.msi"
$awsCliMsi = "$logFolder\AWSCLIV2.msi"
Invoke-WebRequest -Uri $awsCliUrl -OutFile $awsCliMsi

Write-Output "Installing AWS CLI..."
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i", $awsCliMsi, "/qn", "/L*v", "$logFolder\aws-cli-install.log" -PassThru -Wait
Write-Output "AWS CLI installer exited with code: $($process.ExitCode)"

# インストール後にパスを更新
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine")

# AWS CLI が正しくインストールされたか確認
$awsCliPath = "C:\Program Files\Amazon\AWSCLIV2\aws.exe"
if (Test-Path $awsCliPath) {
    Write-Output "AWS CLI successfully installed at: $awsCliPath"
    Write-Output "AWS CLI version: $((& $awsCliPath --version) 2>&1)"
} else {
    Write-Output "ERROR: AWS CLI installation failed or installed to unexpected location"
    $possiblePaths = Get-ChildItem -Path "C:\" -Recurse -Filter "aws.exe" -ErrorAction SilentlyContinue | Select-Object -First 5
    if ($possiblePaths) {
        Write-Output "Possible AWS CLI locations found:"
        $possiblePaths | ForEach-Object { Write-Output $_.FullName }
        $awsCliPath = $possiblePaths[0].FullName
    } else {
        Write-Output "No aws.exe found on the system. Cannot proceed with CloudWatch metrics."
        exit 1
    }
}

# PowerShell 実行ポリシーを変更
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force

# IMDSv2トークンを使用してインスタンスメタデータを取得する関数
function Get-EC2Metadata {
    param (
        [string]$Path
    )

    try {
        $token = Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token-ttl-seconds" = "21600"} -Method PUT -Uri "http://169.254.169.254/latest/api/token"
        $metadata = Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token" = $token} -Method GET -Uri "http://169.254.169.254/latest/meta-data/$Path"
        return $metadata
    }
    catch {
        Write-Output "Error accessing metadata: $_"
        return $null
    }
}

# インスタンスIDとリージョンを取得
try {
    $instanceId = Get-EC2Metadata -Path "instance-id"
    Write-Output "Instance ID: $instanceId"

    $az = Get-EC2Metadata -Path "placement/availability-zone"
    $region = $az.Substring(0, $az.Length - 1)
    Write-Output "Region: $region"
}
catch {
    Write-Output "Error retrieving instance metadata: $_"
    # デフォルト値を設定(テスト用)
    $instanceId = "unknown-instance"
    $region = "us-east-1"
}

# 再起動完了を報告するスクリプトを作成
$reportScriptPath = "$logFolder\ReportRebootComplete.ps1"
$reportScriptContent = @"
# ログ設定
`$logFile = "$logFolder\RebootReport_Log.txt"
Start-Transcript -Path `$logFile -Append -Force

Write-Output "Starting reboot report script... `$(Get-Date)"

# AWS CLI パスを設定
`$awsCliPath = "$awsCliPath"
Write-Output "Using AWS CLI at: `$awsCliPath"

# IMDSv2トークンを使用してインスタンスメタデータを取得する関数
function Get-EC2Metadata {
    param (
        [string]`$Path
    )

    try {
        `$token = Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token-ttl-seconds" = "21600"} -Method PUT -Uri "http://169.254.169.254/latest/api/token"
        `$metadata = Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token" = `$token} -Method GET -Uri "http://169.254.169.254/latest/meta-data/`$Path"
        return `$metadata
    }
    catch {
        Write-Output "Error accessing metadata: `$_"
        return `$null
    }
}

# インスタンスIDとリージョンを取得
try {
    `$instanceId = Get-EC2Metadata -Path "instance-id"
    Write-Output "Instance ID: `$instanceId"

    `$az = Get-EC2Metadata -Path "placement/availability-zone"
    `$region = `$az.Substring(0, `$az.Length - 1)
    Write-Output "Region: `$region"
}
catch {
    Write-Output "Error retrieving instance metadata: `$_"
    exit 1
}

# 現在のタイムスタンプを取得(Unix時間)
`$currentDate = Get-Date
`$unixEpochStart = New-Object DateTime 1970, 1, 1, 0, 0, 0, ([DateTimeKind]::Utc)
`$timestamp = [Math]::Floor((`$currentDate.ToUniversalTime() - `$unixEpochStart).TotalSeconds)
Write-Output "Timestamp: `$timestamp"

# CloudWatch メトリクスをプッシュする
Write-Output "Sending CloudWatch metric..."
`$dimensions = "InstanceId=`$instanceId"
`$command = "& '`$awsCliPath' cloudwatch put-metric-data --namespace 'ServerManagement' --metric-name 'RebootComplete' --dimensions `$dimensions --value 1 --timestamp `$timestamp --unit Count --region `$region"
Write-Output "Executing: `$command"

try {
    Invoke-Expression `$command
    `$exitCode = `$LASTEXITCODE
    Write-Output "AWS CLI exit code: `$exitCode"
    if (`$exitCode -eq 0) {
        Write-Output "Successfully sent metric to CloudWatch."
    }
    else {
        Write-Output "Failed to send metric to CloudWatch. Exit code: `$exitCode"
    }
}
catch {
    Write-Output "Error sending metric: `$_"
}

# イベントログに記録
try {
    if (-not [System.Diagnostics.EventLog]::SourceExists("EC2RebootComplete")) {
        New-EventLog -LogName Application -Source "EC2RebootComplete"
    }
    Write-EventLog -LogName Application -Source "EC2RebootComplete" -EntryType Information -EventId 1000 -Message "Reboot complete metric pushed for instance `$instanceId at `$(Get-Date)"
    Write-Output "Event log entry created."
}
catch {
    Write-Output "Error writing to event log: `$_"
}

Stop-Transcript
"@

Set-Content -Path $reportScriptPath -Value $reportScriptContent

# イベントログソースを作成
try {
    if (-not [System.Diagnostics.EventLog]::SourceExists("EC2RebootComplete")) {
        New-EventLog -LogName Application -Source "EC2RebootComplete"
    }
}
catch {
    Write-Output "Error creating event log source: $_"
}

# スケジュールタスクを作成
$taskName = "ReportEC2RebootComplete"
$taskDescription = "Reports EC2 reboot completion to CloudWatch"
$taskAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$reportScriptPath`""
$taskTrigger = New-ScheduledTaskTrigger -AtStartup
$taskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -WakeToRun
$taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

# 既存のタスクを削除(もし存在する場合)
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

# 新しいタスクを登録
Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Settings $taskSettings -Principal $taskPrincipal -Description $taskDescription

# 初回実行
Write-Output "Running initial report script..."
try {
    & PowerShell.exe -ExecutionPolicy Bypass -NoProfile -File $reportScriptPath
    Write-Output "Initial script execution completed."
}
catch {
    Write-Output "Error during initial script execution: $_"
}

Write-Output "User data script execution completed. $(Get-Date)"
Stop-Transcript
</powershell>

※ 適宜修正の上ご利用ください

確認してみた

以下の通り、出力できています。
証跡として残していないものの、OS 内からの reboot でも出力されます。

02

まとめ

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

参考資料

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

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

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.