はじめに
この度、前任ベンダーが独自監視基盤で運用していた環境を引き継ぎ、CloudWatch + CloudWatch Agent による監視基盤を新規構築しました。
「CloudFormationも用意したし、事前検証済だし、余裕」とフラグを立てたら、案の定はまりまくったので、本ブログで紹介したいと思います。
本記事で使用した CloudFormation テンプレートやスクリプトは GitHub リポジトリ で公開しています
本シリーズの構成
本ブログは 2部構成 です。今回は監視基本編 で、続編に「自動一次対応編」が控えています。
| # | 記事 | スコープ |
|---|---|---|
| 1 | 本記事: 監視基本編 | メトリクス収集 + CloudWatch Alarm の作成 |
| 2 | 続編: 自動一次対応編 | アラーム発火後の自動診断 Lambda・Backlog 自動起票・PagerDuty 通知 |
最終的に組み上がる構成(全体像)
┌────────────────────────────────┐
│ EC2 Instances(混在OS) │
│ - CW Agent 対応 OS │
│ - CW Agent 非対応 OS(旧EOL) │
└──────────────┬─────────────────┘
│
┌──────────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
CW Agent cron + AWS CLI スクリプト Lambda + SSM Run Command
(標準的なOS) (CentOS 7 等) (CentOS 6 等)
│ │ │
└──────────────────────────┴───────────────────────┘
│
▼
CloudWatch メトリクス
(Namespace: CWAgent)
│
▼
CloudWatch Alarm
(CPU/Mem/Disk/Proc/ALB)
═══════════════════════════════ ▲ ════════════════════════════════
【本記事はここまで】
═══════════════════════════════ ▼ ════════════════════════════════
│ AlarmActions
▼
診断 Lambda 6本
(diag-cpu / diag-status /
diag-alb / diag-memory /
diag-disk / diag-process)
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
SNS(E-mail) Backlog 起票 PagerDuty 通知
【続編: 自動一次対応編】
本記事のゴール
「異種 OS が混在する EC2 群すべてに対して、統一された Namespace/Dimension の CloudWatch Alarm を成立させる」 こと。具体的には:
- CW Agent 対応 OS は標準的に Agent でメトリクス収集
- CW Agent 非対応の中堅 OS(CentOS 7 等)は cron + AWS CLI スクリプトで
put-metric-data - CW Agent も AWS CLI も動かない EOL OS(CentOS 6 等)は インスタンス無変更 で外部 Lambda + SSM Run Command でメトリクス収集
- Alarm 側は どの収集方式でも同じ定義で動く(
if文や条件分岐を入れない)
このゴールに到達するまでに踏んだ罠の数々を、問題ごとに解説します。
続編との関係: 本記事の YAML 例で
!ImportValue example-diag-*-arnという参照を見かけたら、その実体は続編で解説する診断 Lambda 群です。本記事だけでデプロイ確認したい場合はcfn/auto-response-stub.yaml(最小スタブ)を先にデプロイすれば AlarmActions の!ImportValue解決が成立します。
環境概要
前提
監視対象全てに対して、SSMエージェントインストール済で、SSM接続が可能。
監視対象
監視対象はEC2インスタンスです。
EC2 の OS が混在しており、CW Agent の対応状況に応じて3方式を使い分けました。
| 区分 | カスタムメトリクス取得方針 |
|---|---|
| CW Agent 対応 OS | CW Agent で取得 |
| CW Agent 非対応 OS(AWS CLI v2 動作可) | cron + aws cloudwatch put-metric-data スクリプトで取得 |
| CW Agent 非対応 OS(AWS CLI v2 動作不可) | Lambda + SSM Run Command による外部収集(※インスタンスに変更を加えない方式) |
監視項目
| カテゴリ | メトリクス | 閾値 | 取得方法 |
|---|---|---|---|
| CPU | CPUUtilization | 90% | AWS標準メトリクス |
| 死活 | StatusCheckFailed | 1以上 | AWS標準メトリクス |
| メモリ | mem_used_percent | 90% | CW Agent / カスタムスクリプト |
| ディスク | disk_used_percent | 95% | CW Agent / カスタムスクリプト |
| プロセス | procstat_lookup_pid_count | 1未満 | CW Agent / カスタムスクリプト |
| ALB | TargetResponseTime | 60秒/10秒 | AWS標準メトリクス |
| ALB | UnHealthyHostCount | 1以上 | AWS標準メトリクス |
CloudFormation構成
監視基盤本体は 4つのコアスタック で構成し、対象環境に応じて 追加スタック2つ(追加EBS監視 / CW Agent非対応OS向け外部収集)を任意で重ねる設計としました。
[コア4スタック]
1. sns-topics ← SNSトピック(Critical/Warning)+ メール購読
↓ Export (topic-arn)
2. auto-response-stub ← 診断Lambdaのスタブ(実体は自動一次対応編で解説)
↓ Export (diag-*-arn)
3. alarms-standard ← Agent不要(CPU, StatusCheck, ALB系)
4. alarms-custom ← Agent必要(メモリ, ディスク, プロセス)
[追加スタック(必要に応じて重ねる)]
5. alarms-ebs-additional ← 追加EBSボリューム(/mnt/ebs/* 等)の disk Alarm を別スタック化
6. metrics-collector ← 問題9 の Lambda+SSM 外部収集(CentOS 6 等の CW Agent 非対応OS向け)
スタック3と4は互いに依存しないため順序は問いませんが、両方ともスタック1・2の Export に依存します。スタック5は3・4と同様に1・2の Export に依存し、スタック6は単独で完結します(Alarm 側のスタックが Namespace CWAgent のメトリクスを参照するだけ)
スタック間参照の仕組み(Export / ImportValue)
少しYAMLファイルの文法説明を挟みます。
SNS トピックの ARN を Fn::ImportValue で参照することで、Alarm スタック側では SNS の ARN をパラメータで渡す必要がなくなります。
エクスポート側(sns-topics.yaml):
Resources:
CriticalTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: example-critical
Outputs:
CriticalTopicArn:
Value: !Ref CriticalTopic # AWS::SNS::Topic の !Ref は ARN を返す
Export:
Name: example-critical-topic-arn # リージョン内でユニークな Export 名
!Refの戻り値はリソースタイプごとに異なります。AWS::SNS::Topicの場合は Topic の ARN(例:arn:aws:sns:ap-northeast-1:123456789012:example-critical)が返ります
インポート側(alarms-standard.yaml / alarms-custom.yaml):
AlarmActions:
- !ImportValue example-diag-cpu-arn # 診断 Lambda の ARN を取得
OKActions:
- !ImportValue example-critical-topic-arn # OK遷移は SNS で直接通知
!ImportValueは Export Store から名前を指定して値を取得する関数です
削除時の制約:
Export/ImportValue によるスタック間参照には重要な制約があります。Import 側のスタックが存在する限り、Export 元のスタックは削除も Export 値の変更もできません。そのため削除はデプロイと逆順で行います。
オプショナルパラメータの条件分岐
sns-topics.yaml では、メール通知先を最大3名まで柔軟に設定できるよう Conditions を使っています。
Parameters:
CriticalEmail1:
Type: String # 必須(Default なし)
CriticalEmail2:
Type: String
Default: '' # 省略可(空文字がデフォルト)
Conditions:
HasCriticalEmail2: !Not [!Equals [!Ref CriticalEmail2, '']]
Resources:
CriticalSub2:
Type: AWS::SNS::Subscription
Condition: HasCriticalEmail2 # true のときだけリソースを作成
Properties:
TopicArn: !Ref CriticalTopic
Protocol: email
Endpoint: !Ref CriticalEmail2
評価の流れ:
!Equals [!Ref CriticalEmail2, '']— パラメータ値が空文字と等しいか判定!Not [...]— 論理否定(空文字ならfalse、値があればtrue)Condition: HasCriticalEmail2—trueならリソース作成、falseなら作成しない
これにより、通知先が1名でも3名でも同じテンプレートで対応できます。
完全なテンプレートは
cfn/sns-topics.yaml,cfn/alarms-standard.yaml,cfn/alarms-custom.yamlを参照してください。
デプロイコマンド
#例:SNS トピック
aws cloudformation create-stack \
--stack-name example-sns \
--template-body file://cfn/sns-topics.yaml \
--parameters file://cfn/params-example.json # sns-topics セクション
問題1: CentOS 6/7 は CloudWatch Agent の公式サポート対象外
事象
- 9 台中 4 台(CentOS 6 が 2 台、CentOS 7 が 2 台)が CW Agent の公式サポート OS 一覧に含まれていない
- AWS 公式の CloudWatch Agent サポート対象オペレーティングシステム(2026年4月時点)には Amazon Linux / RHEL / Ubuntu / Debian / SUSE 等は記載があるが、CentOS 6/7 は対象外
2 系統の代替策
非対応 4 台はさらに 2 系統に分かれる:
| 台数 | OS | AWS CLI v2 | 方式 |
|---|---|---|---|
| 2 台 | CentOS 7 | 動作する | 本問題で扱う: cron + aws cloudwatch put-metric-data スクリプトで送信 |
| 2 台 | CentOS 6 | 動作しない(glibc 2.12) | 問題9 で扱う: Lambda + SSM Run Command で外部収集 |
本問題では前者(CentOS 7 の 2 台)だけを扱い、後者(CentOS 6)は問題9 以降で詳述する。
ポイント
- CW Agent と同じ Namespace(
CWAgent)・Dimension 構成でメトリクスを送信すれば、Alarm 定義を Agent 対応台と統一できる(Alarm 側の if 文が不要) - スクリプトは
/proc/meminfo、df、pgrepでメトリクスを取得し、aws cloudwatch put-metric-dataで送信する - CentOS 7 については rpm の手動インストール自体は動作することが広く知られているが、公式サポート外である以上、本番運用で採用するとトラブル時の切り分けコストが読めないため、スクリプト方式を選択した
対応方法の概要
代替策はインスタンス側に AWS CLI を入れられるかどうかで分岐する。実装は GitHub に公開済みなので、本記事では概要と参照先のみ示す。
A. CentOS 7(インスタンス内 AWS CLI スクリプト方式)
cron で 5 分間隔にシェルスクリプトを起動し、/proc/meminfo ・ df ・ pgrep でメトリクスを取得して aws cloudwatch put-metric-data で Namespace=CWAgent に送信する。インスタンス内に AWS CLI v2 と IAM ロール(cloudwatch:PutMetricData)が必要。
| スクリプト | 役割 |
|---|---|
scripts/put_mem_metrics.sh |
mem_used_percent を送信(/proc/meminfo 直読み、MemAvailable フォールバック対応) |
scripts/put_disk_metrics.sh |
disk_used_percent をマウントポイント別に送信 |
scripts/process_monitor.sh |
procstat_lookup_pid_count を pgrep 結果から送信 |
cron 経由で動かす際の落とし穴は 問題7(PATH と AWS CLI v1/v2 の競合)で詳述する。
B. CentOS 6(外部 Lambda + SSM Run Command 方式)
CentOS 6 はAWS CLI v2 が動かず、また新規でv1をインストールすることも不可だったため、インスタンスを一切変更せず外部から押し込む 設計に振った。EventBridge(5分間隔)→ Lambda(VPC 外)→ SSM Run Command で df / /proc/meminfo / pgrep を実行 → Lambda が結果を解析して cloudwatch:PutMetricData(同じく Namespace=CWAgent)に送る。
| テンプレート | 役割 |
|---|---|
cfn/metrics-collector.yaml |
Lambda(Python 3.12)+ IAM Role + EventBridge Rule + Log Group を一括作成。Lambda コードはテンプレート内インラインで定義 |
詳細な設計判断(VPC 外配置の理由、IAM の Resource 指定の勘所、pgrep -f の自己検出回避など)は 問題9・問題10 で扱う。
どちらの方式も 送信先 Namespace と Dimension を CW Agent と完全に揃える ことで、アラート作成に必要なcfnテンプレートが少なくて済む。
問題2: AWS-ConfigureAWSPackage が AlmaLinux に非対応
事象
- SSM の
AWS-ConfigureAWSPackageで CW Agent をインストールしようとしたところ、AlmaLinux 2台が失敗- AlmaLinux 9:
no manifest found for platform: almalinux - AlmaLinux 8:
nil pointer dereference(SSM Agent が古すぎてクラッシュ)
- AlmaLinux 9:
解決策
- CW Agent の公式ダウンロードページで配布されている RHEL 向け rpm パッケージを直接インストール。AlmaLinux は RHEL のバイナリ互換ディストリビューションのため、RHEL 向け rpm がそのまま動作する
- SSM Agent が古い台は先に SSM Agent を手動更新
# SSM Agent が古い場合は先に更新
sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl restart amazon-ssm-agent
# CW Agent インストール(RHEL向けrpmをAlmaLinuxに適用)
sudo yum install -y amazon-cloudwatch-agent || \
(curl -O https://s3.amazonaws.com/amazoncloudwatch-agent/redhat/amd64/latest/amazon-cloudwatch-agent.rpm && \
sudo rpm -U ./amazon-cloudwatch-agent.rpm)
教訓
AWS-ConfigureAWSPackageは Amazon Linux / Ubuntu / RHEL 等の主要ディストリビューションのみ対応。RHEL互換OSでもマニフェスト未登録なら失敗する。回避策として公式ページから RHEL 向け rpm を取得し、rpm -Uで直接インストールできる。
問題3: Dimension の不一致で Alarm がメトリクスにマッチしない
事象
- 前任ベンダーの CW Agent 設定を引き継いだ3台で、Alarm が
INSUFFICIENT_DATAのまま遷移しない
原因
- 前任設定の
append_dimensionsがImageId,InstanceType,AutoScalingGroupName,InstanceIdの4つを含んでおり、CloudWatch 上のメトリクスが 7次元(append 4 + disk 固有の path/device/fstype の 3 つ)になっていた - 新規で作成したAlarm 定義は
InstanceId+ disk 固有次元の 4次元 で定義していた(cfn/alarms-custom.yamlのDiskWebProd等を参照) - CloudWatch は Dimension が1つでも異なれば完全に別のメトリクス として扱うため、Alarm がメトリクスを見つけられなかった
// NG: 前任設定
"append_dimensions": {
"ImageId": "${aws:ImageId}",
"InstanceId": "${aws:InstanceId}",
"InstanceType": "${aws:InstanceType}",
"AutoScalingGroupName": "${aws:AutoScalingGroupName}"
}
// OK: 修正後(Alarm定義と一致する4次元になる)
"append_dimensions": {
"InstanceId": "${aws:InstanceId}"
}
解決策
append_dimensionsをInstanceIdのみに変更
修正後の Agent 設定 JSON の全体像を示す。以下は実際に使用した設定の一例(Debian 13 / マップサーバ):
{
"agent": {
"metrics_collection_interval": 300,
"region": "ap-northeast-1"
},
"metrics": {
"namespace": "CWAgent",
"append_dimensions": {
"InstanceId": "${aws:InstanceId}"
},
"metrics_collected": {
"mem": {
"measurement": ["mem_used_percent"],
"metrics_collection_interval": 300
},
"disk": {
"measurement": ["disk_used_percent"],
"resources": ["/", "/mnt/ebs/0", "/mnt/ebs/0/webcontents"],
"metrics_collection_interval": 300
},
"procstat": [
{ "exe": "sshd", "measurement": ["pid_count"], "metrics_collection_interval": 300 },
{ "exe": "postgres", "measurement": ["pid_count"], "metrics_collection_interval": 300 },
{ "pattern": "apache2-80", "measurement": ["pid_count"], "metrics_collection_interval": 300 },
{ "pattern": "apache2-81", "measurement": ["pid_count"], "metrics_collection_interval": 300 },
{ "pattern": "apache2-82", "measurement": ["pid_count"], "metrics_collection_interval": 300 }
]
}
}
}
各キーの解説
agent セクション — Agent 自体の動作設定
| キー | 値 | 説明 |
|---|---|---|
metrics_collection_interval |
300 |
メトリクス収集のデフォルト間隔(秒)。個別の metrics_collected 内でも指定できるが、省略時はここの値が使われる |
region |
ap-northeast-1 |
メトリクス送信先のリージョン。EC2 のメタデータから自動取得もされるが、明示すると確実 |
metrics セクション — 何を・どの名前で送信するか
| キー | 値 | 説明 |
|---|---|---|
namespace |
CWAgent |
CloudWatch 上の Namespace。デフォルトも CWAgent だが、明示することで Alarm 定義との対応を分かりやすくする |
append_dimensions |
{"InstanceId": "${aws:InstanceId}"} |
全メトリクスに自動付与する Dimension。 ${aws:InstanceId} は Agent 起動時に EC2 メタデータから実値に置換される。本問題の核心: ここに余分なキーがあると Alarm の Dimension と一致しなくなる |
metrics_collected.mem — メモリ監視
| キー | 値 | 説明 |
|---|---|---|
measurement |
["mem_used_percent"] |
収集するメトリクス名。CloudWatch 上では mem_used_percent として送信される |
生成される Dimension: InstanceId のみ(append_dimensions 由来)
metrics_collected.disk — ディスク監視
| キー | 値 | 説明 |
|---|---|---|
measurement |
["disk_used_percent"] |
収集するメトリクス名 |
resources |
["/", "/mnt/ebs/0", ...] |
監視対象のマウントポイント。ここに列挙したパスのみが CloudWatch に送信される。"*" で全マウントも可能だが、不要なパスが増えるため明示指定が推奨 |
生成される Dimension: InstanceId(append 由来)+ path, device, fstype
※deviceやfstypeはCloudWatch Agentが自動付与しています。この仕様のため一回事故が起こっており、その詳細は問題12にまとめています。
metrics_collected.procstat — プロセス監視
| キー | 値 | 説明 |
|---|---|---|
exe |
"sshd" 等 |
プロセスの実行バイナリ名でマッチ。Dimension 名は exe になる。ネイティブバイナリ向け |
pattern |
"apache2-80" 等 |
プロセスのコマンドライン全体で正規表現マッチ。Dimension 名は pattern になる。スクリプト言語やポート別インスタンス向け(詳細は問題5・6で後述) |
measurement |
["pid_count"] |
procstat_lookup_pid_count として送信される。プロセスが存在すれば 1 以上、なければ 0 |
生成される Dimension: InstanceId(append 由来)+ exe or pattern + pid_finder(Agent が自動付与、値は native)の計3次元
Dimension まとめ — メトリクス種別ごとの次元構成
| メトリクス | Dimension | 由来 |
|---|---|---|
mem_used_percent |
InstanceId |
append_dimensions |
disk_used_percent |
InstanceId + path + device + fstype |
append_dimensions + Agent 自動付与 |
procstat_lookup_pid_count |
InstanceId + exe or pattern + pid_finder |
append_dimensions + Agent 自動付与 |
Alarm 定義(cfn/alarms-custom.yaml)では、上記と完全に同じ Dimension の組み合わせを指定する必要がある。1つでも過不足があれば INSUFFICIENT_DATA になる。
Dimension は誰が付与するのか — 公式ドキュメントと実機の突合
上の表を見ると、メトリクス種別によって Dimension の数が 1〜4次元とバラバラになっている。これは JSON で「この Dimension を送れ」と設定しているわけではなく、Agent が内部的に自動付与している ものがあるためである。各メトリクスの Dimension がどこで決まるのかを整理する。
mem(メモリ)— 自動付与 Dimension なし
公式ドキュメントによると、mem メトリクスは append_dimensions で指定したものだけが Dimension になる。disk のような自動付与は存在しない。
そのため append_dimensions に InstanceId のみを設定している場合、mem の Dimension は InstanceId の 1次元だけになる。これが最もシンプルなケースであり、Alarm 定義でも InstanceId のみ指定すればよい。
disk(ディスク)— path, device, fstype が(おそらく)自動付与
以下公式ドキュメント
The
diskmetrics have a dimension forPartition, which means that the number of custom metrics generated is dependent on the number of partitions associated with your instance.
パーティションの数だけメトリクスが増えると書かれている+実機を確認したところより、
path,device,fstypeが自動付与されるものかと推測できる。
唯一制御できるのは drop_device パラメータで、true にすると device Dimension を除外できる:
"disk": {
"measurement": ["disk_used_percent"],
"resources": ["/"],
"drop_device": true // device Dimension を除外
}
Setting this to
truecausesDeviceto not be included as a dimension for disk metrics.
ただし、drop_device を使うと Alarm 側の Dimension 定義も変わるため、安易に変更すると問題3の二の舞になる。
procstat(プロセス)— exe or pattern, pid_finder が(おそらく)自動付与
こちらに関しても明示的な記載を見つけられなかったため、実機で確認したところ以下のように推測できる。
実際の動作:
| JSON の設定 | 自動付与される Dimension |
|---|---|
"exe": "sshd" |
exe=sshd, pid_finder=native |
"pattern": "apache2-80" |
pattern=apache2-80, pid_finder=native |
exeとpatternのどちらを使ったかで Dimension 名自体が変わる。これが問題5で Alarm が反応しなかった原因でもあるpid_finderは Agent のデフォルト検出方式(native)が Dimension として自動付与される。JSON で明示的に指定していなくても付く
教訓
CW Agent のデフォルト設定ウィザードは余分な Dimension を付与する。監視用途なら
InstanceIdのみで十分。Alarm 定義と Agent 設定の Dimension は完全一致が必要。
問題4: run_as_user: cwagent で procstat が動作しない
事象
- 前任設定を引き継いだ3台で、procstat の pid_count が全て0になった(sshd すら ALARM)
- 新規インストールした2台では正常
原因
- 前任設定に
"run_as_user": "cwagent"があり、Agent が一般ユーザで動作していた - procstat の
pid_finder: nativeモードは/proc/[pid]/exeを直接読み取るが、cwagent ユーザには root 所有プロセスの/procエントリを参照する権限がない - そのため全プロセスが「見つからない」扱いになった
// NG: procstat と組み合わせると pid_count = 0 になる
{
"agent": {
"run_as_user": "cwagent"
}
}
// OK: デフォルト(root実行)— run_as_user を削除するだけ
{
"agent": {
"metrics_collection_interval": 300
}
}
解決策
run_as_userを削除し、デフォルトの root 実行に戻した
教訓
CW Agent はデフォルトで root 実行。
run_as_userでセキュリティを高められるが、procstat は/procの可視性に依存するため root が必要。なお、この制約は AWS 公式ドキュメントに記載がない(2026年4月時点)。
問題5: exe で Perl/Java プロセスを検出できない
事象
- Perl 製 HTTP サーバ(starman)と Java アプリ(Tomcat)が 正常に動いているはずなのに、ALARM になった
原因
exeはプロセスの実行バイナリ名でマッチする- starman の実体は
perl、Tomcat の実体はjavaなので、exe: "starman"やexe: "tomcat"ではマッチしない
前提知識
Linuxで動いているプロセスには以下2種類がある。
①バイナリ名
②コマンドライン全体
なお、以下のコマンドで特定のプロセスのバイナリ名およびコマンドライン全体が確認可能
#Tomcatの場合
#バイナリ名(comm)とコマンドライン(args)を分けて見る
ps -eo pid,comm,args | grep -i tomcat | grep -v grep
#出力例
12345 java /usr/lib/jvm/java-11/bin/java -Dcatalina.base=/opt/tomcat ...
出力をプロセスID、バイナリ名(comm)、コマンドライン(args)に絞ると
Tomcatの実体がjavaバイナリであることがわかる。
解決策
- バイナリとコマンドラインの関係性をコマンドで確認
patternでコマンドライン全体で正規表現マッチpatternを使うと Dimension 名がexe→patternに変わるため、Alarm 定義も修正が必要
// CW Agent 設定
{ "exe": "sshd", ... } // → Dimension: {exe: sshd} ← ネイティブバイナリ
{ "pattern": "starman", ... } // → Dimension: {pattern: starman} ← Perlスクリプト
{ "pattern": "tomcat", ... } // → Dimension: {pattern: tomcat} ← JVMアプリ
cfn/alarms-custom.yaml の ProcTomcatWebProd / ProcStarmanWebProd で、exe ではなく pattern を Dimension に使用している例を確認できます。
教訓
ネイティブバイナリ(sshd, httpd, nginx 等)は
exe、スクリプト言語や JVM アプリはpatternを使うこと。
問題6: apache2 のポート別インスタンスを個別監視できない(問題5の続き)
事象
- 2台で apache2 が systemd インスタンス化ユニット(
apache2@80,apache2@81,apache2@82)として3ポートで稼働 exe: apache2では全体を1つとしてカウントするため、特定ポートの停止を検知できない
原因
exeはバイナリ名のマッチなので、3ポートすべて同じ/usr/sbin/apache2にマッチし、合算 pid_count になる
解決策
- 各ポートの起動コマンドに
-d /etc/apache2-80のようにポート固有の文字列が含まれていることを実機確認 pattern: "apache2-80"/"apache2-81"/"apache2-82"でポート別にカウント- Alarm を旧2件 → 新6件(各3ポート × 2台)に変更。CFn
update-stackで旧削除・新作成を一括実施
# cfn/alarms-custom.yaml(抜粋)— apache2 ポート別 Alarm
ProcApache280MapServer:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: example-proc-apache280-map
Namespace: CWAgent
MetricName: procstat_lookup_pid_count
Dimensions:
- Name: InstanceId
Value: !Ref IdMapServer
- Name: pattern # ← exe ではなく pattern
Value: apache2-80 # ← コマンドライン引数の "-d /etc/apache2-80" にマッチ
- Name: pid_finder
Value: native
Statistic: Minimum
Period: 300
Threshold: 1
ComparisonOperator: LessThanThreshold
...
教訓
同一バイナリが複数インスタンスで動く場合は、コマンドライン引数の差異を
patternで捕捉する。実機でps auxを確認してからパターンを設計すること。
問題7: cron で AWS CLI スクリプトが動かない
事象
- CentOS 7 にカスタムスクリプトを導入し、SSM Run Command での手動テストは成功
- しかし cron 経由では一切メトリクスが送信されない
調査過程
- cron 自体は正常にスクリプトを起動していた(
/var/log/cronで確認) - スクリプトのログに
Python 2.7のImportErrorが出ていた - cron の PATH(
/sbin:/bin:/usr/sbin:/usr/bin)には/usr/local/binが含まれない /usr/bin/aws(壊れた v1)が/usr/local/bin/aws(正常な v2)より優先されていた
cron (PATH=/sbin:/bin:/usr/sbin:/usr/bin)
→ スクリプト内の `aws` コマンド
→ /usr/bin/aws (v1 / Python 2.7) が解決される
→ ImportError
なぜ2台中1台だけ発生したのか
- 1台目: v2 新規インストール時に旧 v1 をリネーム済み → 問題なし
- 2台目: v2 が既にインストール済みだったため導入手順をスキップ → 壊れた v1 が残存
対策
v1 リネーム + /usr/bin/aws に v2 へのシンボリックリンク作成
sudo mv /usr/bin/aws /usr/bin/aws.v1.bak
sudo ln -sf /usr/local/bin/aws /usr/bin/aws
# cron の PATH を再現して動作確認
env -i PATH=/sbin:/bin:/usr/sbin:/usr/bin aws --version
教訓
cron の PATH は SSH や SSM より限定的。手動テストで動いても cron で動くとは限らない。AWS CLI v2 導入後は「v1 リネーム + シンボリックリンク」をセットで行うこと。
問題8: SSM Run Command で heredoc が動作しない
事象
- Agent 設定ファイルを SSM Run Command の heredoc で書き込んだところ、JSON 内の
${aws:InstanceId}が空文字に置換されてファイルに書かれ、Agent 起動後に Dimension が空になった
原因: プレースホルダがシェルに先回りで食われる
ここには 2つのレイヤ が関わっている。
${aws:InstanceId}は CW Agent の正式なプレースホルダで、Agent が起動時に EC2 メタデータから置換する 設計(${aws:ImageId}/${aws:InstanceType}等も同様)- しかし heredoc の本体は、POSIX シェル仕様上 区切り文字をクォートしない限りパラメータ展開される
つまりクォートなしだと、Agent が後で処理するはずの ${aws:InstanceId} を シェルが先に展開しようとする → 該当変数が無いため空文字 になり、ファイルに書かれた時点で記法が消失する。
解決策
区切り文字をシングルクォートで囲み、ファイル書き込みは sudo tee を使う:
# OK: クォート付き heredoc + sudo tee
sudo tee /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json > /dev/null <<'EOF'
{
"metrics": {
"append_dimensions": {
"InstanceId": "${aws:InstanceId}" ← リテラルのまま Agent に渡る
}
}
}
EOF
# NG: クォートなしだと ${aws:InstanceId} が空文字に化ける
sudo tee ... <<EOF
{ "InstanceId": "${aws:InstanceId}" }
EOF
教訓
CW Agent のプレースホルダ記法(
${aws:...})は、見た目がシェル変数と同形のためクォートなし heredoc に通すと先回りで展開されてしまう。
問題10: CentOS 6 特有のコマンド非互換
問題9 の Lambda 内で実行する df / free / pgrep を CentOS 6 で動かす際、現代的なオプションがことごとく使えなかった。
引っかかった非互換一覧
| 問題 | 現代環境 | CentOS 6 | 対処 |
|---|---|---|---|
df --output=pcent,source,target,fstype で必要カラムだけ抽出 |
OK | --output 非対応 |
df -P で POSIX形式 + 固定カラム解析 |
pgrep -c でマッチ数を取得 |
OK | -c 非対応 |
pgrep -x(ネイティブバイナリ)/pgrep -f(JVM/スクリプト)の終了コードで判定。-f の自己検出には問題9 の注意事項を参照 |
free の used 列でメモリ使用量を取得 |
バッファ/キャッシュ除外済み | バッファ/キャッシュ込み | /proc/meminfo から MemTotal - MemFree - Buffers - Cached で計算 |
sudo を SSM 経由で実行 |
OK | requiretty により失敗 |
SSM Run Command は root 実行のため sudo 自体不要 |
メモリ計算の互換対応
free は kernel バージョンによって挙動が違うので、/proc/meminfo から直接計算するのが最も安全:
# kernel 3.14+ (CentOS 7): MemAvailable が使える
awk '/MemTotal:/{t=$2} /MemAvailable:/{a=$2} END{printf "%.1f", (t-a)*100/t}' /proc/meminfo
# kernel < 3.14 (CentOS 6): MemAvailable がないので近似計算
awk '/MemTotal:/{t=$2} /MemFree:/{f=$2} /Buffers:/{b=$2} /^Cached:/{c=$2} \
END{printf "%.1f", (t-f-b-c)*100/t}' /proc/meminfo
Cached:の正規表現は/^Cached:/とアンカーを付ける。SwapCached:にもマッチしてしまうため。
教訓
EOL OS を監視する場合、コマンドオプションは「何年前から存在するか」を意識する。
df --outputは util-linux の比較的新しい機能。POSIX 標準に寄せたdf -Pなら Solaris から Debian まで同じ解析ロジックで済む。
問題11: Agent 設定の更新で前回手順の変更を巻き戻す
構築終盤で踏んだ、地味だが最も再発しやすい事故。
事象
- 問題6(apache2 ポート別監視)で、ows/owsdev の CW Agent 設定を
pattern: apache2-80/81/82に変更済み - その後、追加 EBS ボリューム(
/mnt/ebs/*)の監視を追加するため、同じ2台のdisk.resourcesに追加パスを入れる必要が出た - 新しい手順書の「設定ファイルを全文
sudo teeで上書き」という雛形をそのまま使うと、procstat がexe: apache2に戻る → ポート別監視が消える
根本原因: fetch-config は全文上書き
この問題を理解するには、CW Agent の設定管理の仕組みを知る必要がある。
amazon-cloudwatch-agent-ctl の3つの設定アクション
Agent 設定の投入には amazon-cloudwatch-agent-ctl -a を使うが、アクションによって動作が大きく異なる。
amazon-cloudwatch-agent-ctl -a fetch-config -c file:<path> -s # 設定を置換して再起動
amazon-cloudwatch-agent-ctl -a append-config -c file:<path> -s # 設定を追加して再起動
| アクション | 動作 | ユースケース |
|---|---|---|
fetch-config |
既存の設定を破棄し、指定した設定で置き換える | 設定を一から作り直すとき |
append-config |
既存の設定にマージ(追加)する。同じキーがあれば上書き | 既存設定を残したまま項目を追加するとき |
今回の手順書では全て fetch-config を使っていたため、投入するたびに JSON 全文で上書きされ、前回の変更が含まれていなければ消失する。
.d ディレクトリの仕組み
設定ファイルの実体は、手順書で sudo tee した /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json ではなく、Agent 起動時に自動生成される .d ディレクトリ配下のコピーである。
/opt/aws/amazon-cloudwatch-agent/etc/
├── amazon-cloudwatch-agent.json ← 手動で配置した設定(投入元)
├── amazon-cloudwatch-agent.d/ ← Agent が起動時に自動生成するコピー
│ └── file_amazon-cloudwatch-agent.json ← 実際に Agent が読む設定
└── amazon-cloudwatch-agent.toml ← JSON → TOML に変換された最終設定
公式ドキュメントにも以下の記載がある:
When the agent is started, it creates a copy of each configuration file in the
amazon-cloudwatch-agent.ddirectory, with the filename prefixed with eitherfile_(for local file sources) orssm_(for Systems Manager parameter store sources) to indicate the configuration origin.
解決策
- 「現行設定の原本(Single Source of Truth)」を1つ決める
- インスタンスごとに「最新の Agent 設定 JSON」を1箇所に置く
- 新しい手順書を作るときは、必ずその原本から派生させる
- 手順書の中で「原本はここ」と明示する
- 「この設定を投入すると、apache2 ポート別監視が巻き戻ります」のような相互注意を書く
教訓
CW Agent の設定は「宣言的な全文上書き」。複数の変更要件が時間をまたいで来ると、手順書を順に実施するだけで前の変更が消える。変更を重ねるなら、原本を1つにまとめる運用にするしかない。
問題12: メンテナンス後にディスク Alarm が INSUFFICIENT_DATA に — NVMe デバイス名スワップ
サービス断ではないが、運用で必ず1度は踏む地味な事故。問題3 の発展形。
事象
- インスタンスのメンテナンス(
stop→startあるいはボリューム再アタッチ)後、特定のディスク Alarm がINSUFFICIENT_DATAのまま戻らない - 対象は NVMe-backed の EC2 インスタンスで、複数の EBS ボリュームを持つ構成
dfでマウント状態は正常、CW Agent も稼働中、メトリクスもlist-metricsには現れる。しかし 既存 Alarm だけが 値を受け取らない
原因: NVMe デバイス名はカーネル検出順で割り当てられる(非決定的)
AWS 公式ドキュメント(Amazon EBS および NVMe)に明記されているとおり、NVMe デバイス名(nvme1n1, nvme2n1, …)は ブロックデバイスマッピングで指定した順序とは独立に、カーネルが NVMe コントローラを検出した順 で連番が振られる。
The block device driver can assign NVMe device names in a different order than you specified for the volumes in the block device mapping.
停止/起動を経るとこの検出順は変動しうるため、同じボリュームでも device 名が入れ替わる現象が発生する。
# メンテナンス前
/mnt/ebs/0 → nvme1n1 (vol-aaa)
/mnt/ebs/0/webcontents → nvme2n1 (vol-bbb)
# メンテナンス後(カーネル検出順が変動)
/mnt/ebs/0 → nvme2n1 (vol-aaa) ← 同じボリュームなのに device 名が変わった
/mnt/ebs/0/webcontents → nvme1n1 (vol-bbb)
CloudWatch メトリクスは device を Dimension に含むため、device 名が変わると CloudWatch から見ると別系列の新メトリクスとして登録される。既存 Alarm は旧 device 名(nvme1n1)の系列を見続けたまま、新規データは新系列(nvme2n1)にしか流れないので、永久に値が来ない=INSUFFICIENT_DATA。
問題3 で扱った「Dimension 完全一致」のルールが裏目に出るパターン。
解決策: drop_device パターン
CW Agent 設定で drop_device: true を指定すると、disk メトリクスの Dimension から device を出力しなくなる。これは AWS 公式が Nitro 系インスタンス向けに明示的に推奨しているパターン(CloudWatch Agent Configuration File):
Preventing
Devicefrom being used as a dimension can be useful on instances that use the Nitro system because on those instances the device names change for each disk mount when the instance is rebooted. This can cause inconsistent data in your metrics and cause alarms based on these metrics to go toINSUFFICIENT DATAstate.
つまり今回の現象は AWS 公式が想定している既知の挙動であり、drop_device がその対策として用意されている。
"disk": {
"measurement": ["disk_used_percent"],
"resources": ["/", "/mnt/ebs/0", "/mnt/ebs/0/webcontents"],
"drop_device": true, // ← device Dimension を除外
"metrics_collection_interval": 300
}
これに合わせて Alarm 側からも device Dimension を削除する。cfn/alarms-custom.yaml の DiskMapServer がこのパターンの例:
DiskMapServer:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: example-disk-map-server
AlarmDescription: "drop_device パターン - device Dimension を意図的に除外"
Namespace: CWAgent
MetricName: disk_used_percent
Dimensions:
- Name: InstanceId
Value: !Ref IdMapServer
- Name: path
Value: /
- Name: fstype # ← device Dimension は無し
Value: ext4
# ...
cfn/alarms-ebs-additional.yaml の DiskEbs0WebcontentsWebProd でも同じパターンを採用している。
トレードオフ
drop_device を使うと、同じ InstanceId × 同じ path で異なる device のボリュームを区別できなくなる。ただし以下の理由で実用上はほぼ問題にならない:
- ルートFS(
/)は常に1ボリューム /mnt/ebs/Nのような追加ボリュームは path 自体が違うので、device で区別する必要がない- 同一 path に複数 device がマウントされるのはマルチパスや bind mount などの特殊構成だけ
適用方針
- 1度でもデバイス名スワップを経験したインスタンス: 即時
drop_device: true化 - メンテナンスの頻度が高い系統(再起動・ボリューム再アタッチが日常的): プロアクティブに drop_device を採用
- それ以外: 通常は
deviceあり構成のままでも問題ないが、運用標準として drop_device 寄せにしておくと将来の事故を予防できる
教訓
NVMe デバイス名は安定した識別子ではない。
deviceを Alarm の Dimension に入れると、メンテナンス1回でINSUFFICIENT_DATAになる。「値が動くもの」を Alarm の Dimension に含めるのは原則アンチパターン。Alarm はInstanceId + path (+ fstype)で識別するのが運用上もっとも頑健。
付録: Route53 ヘルスチェック用 SG(マネージドプレフィックスリストパターン)
公開 URL の稼働監視を Route53 ヘルスチェックで行う場合、ALB 側の Security Group で ヘルスチェッカーの送信元 IP を許可 する必要があります。AWS は健全性チェック用の送信元 IP 範囲を時々ローテーションするため、IP を直接 SG に書き込むと運用負荷になります。
解決策: マネージドプレフィックスリストの参照
AWS が公式に提供するマネージドプレフィックスリスト com.amazonaws..route53-healthchecks(IPv4)/ com.amazonaws..ipv6.route53-healthchecks(IPv6)を SourcePrefixListId で参照すれば、AWS 側で IP が更新されても SG は自動追従します。
注意: Route53 ヘルスチェック用プレフィックスリストは リージョン別 に提供されており、名前自体に という名前は存在しないので、CloudFront 等の本当にグローバルなマネージドリストと混同しないこと。 が含まれます(例: ap-northeast-1 では com.amazonaws.ap-northeast-1.route53-healthchecks)。com.amazonaws.global.route53-healthchecks
# cfn/route53-healthcheck-sg.yaml(抜粋)
AlbHealthCheckSgProd:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow Route53 health checker traffic to the production ALB
VpcId: !Ref ProdVpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourcePrefixListId: !Ref Route53HealthCheckPrefixListId
Description: HTTPS from Route53 health-checker source IPs
Tags:
- Key: Name
Value: example-prod-alb-route53-healthcheck-sg
プレフィックスリスト ID の取得
リージョンごとに ID が異なるため、デプロイ前に確認してパラメータに渡します:
# 例: ap-northeast-1 の場合
aws ec2 describe-managed-prefix-lists \
--region ap-northeast-1 \
--filters Name=prefix-list-name,Values=com.amazonaws.ap-northeast-1.route53-healthchecks \
--query 'PrefixLists[0].PrefixListId' --output text
# リージョン名を変数化したい場合
REGION=$(aws configure get region)
aws ec2 describe-managed-prefix-lists \
--filters "Name=prefix-list-name,Values=com.amazonaws.${REGION}.route53-healthchecks" \
--query 'PrefixLists[0].PrefixListId' --output text
設計のポイント
- VPC 単位で SG を作る: SG は VPC スコープなので、本番 VPC・開発 VPC それぞれに同じパターンで SG を1つずつ用意する
- ALB に直接アタッチ: 既存の ALB SG に Ingress Rule を足してもよいが、Route53 用と分離しておくと「どの SG が Route53 用か」が一目で分かり、後から削除/移管もしやすい
- ポート: 通常は 443(HTTPS)。HTTP 監視も併用するなら 80 を追加
教訓
AWS が「アドレス範囲を所有しつつ更新し続けるサービス」(Route53 ヘルスチェック / CloudFront / S3 等)に対して SG ルールを書くときは、マネージドプレフィックスリスト一択。IP 直書きは運用事故の温床。


