サイトアイコン 協栄情報ブログ

AWS監視基盤移管で踏んだ落とし穴

はじめに

この度、前任ベンダーが独自監視基盤で運用していた環境を引き継ぎ、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 を成立させる」 こと。具体的には:

このゴールに到達するまでに踏んだ罠の数々を、問題ごとに解説します。

続編との関係: 本記事の 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 名

インポート側(alarms-standard.yaml / alarms-custom.yaml):

AlarmActions:
  - !ImportValue example-diag-cpu-arn          # 診断 Lambda の ARN を取得
OKActions:
  - !ImportValue example-critical-topic-arn   # OK遷移は SNS で直接通知

削除時の制約:

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

評価の流れ:

  1. !Equals [!Ref CriticalEmail2, ''] — パラメータ値が空文字と等しいか判定
  2. !Not [...] — 論理否定(空文字なら false、値があれば true
  3. Condition: HasCriticalEmail2true ならリソース作成、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 の公式サポート対象外

事象

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 以降で詳述する。

ポイント

対応方法の概要

代替策はインスタンス側に AWS CLI を入れられるかどうかで分岐する。実装は GitHub に公開済みなので、本記事では概要と参照先のみ示す。

A. CentOS 7(インスタンス内 AWS CLI スクリプト方式)

cron で 5 分間隔にシェルスクリプトを起動し、/proc/meminfodfpgrep でメトリクスを取得して aws cloudwatch put-metric-dataNamespace=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_countpgrep 結果から送信

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 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 がメトリクスにマッチしない

事象

原因

// NG: 前任設定
"append_dimensions": {
  "ImageId": "${aws:ImageId}",
  "InstanceId": "${aws:InstanceId}",
  "InstanceType": "${aws:InstanceType}",
  "AutoScalingGroupName": "${aws:AutoScalingGroupName}"
}

// OK: 修正後(Alarm定義と一致する4次元になる)
"append_dimensions": {
  "InstanceId": "${aws: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_dimensionsInstanceId のみを設定している場合、mem の Dimension は InstanceId1次元だけになる。これが最もシンプルなケースであり、Alarm 定義でも InstanceId のみ指定すればよい。

disk(ディスク)— path, device, fstype が(おそらく)自動付与

以下公式ドキュメント

The disk metrics have a dimension for Partition, 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 true causes Device to 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

教訓

CW Agent のデフォルト設定ウィザードは余分な Dimension を付与する。監視用途なら InstanceId のみで十分。Alarm 定義と Agent 設定の Dimension は完全一致が必要。


問題4: run_as_user: cwagent で procstat が動作しない

事象

原因

// NG: procstat と組み合わせると pid_count = 0 になる
{
  "agent": {
    "run_as_user": "cwagent"
  }
}

// OK: デフォルト(root実行)— run_as_user を削除するだけ
{
  "agent": {
    "metrics_collection_interval": 300
  }
}

解決策

教訓

CW Agent はデフォルトで root 実行。run_as_user でセキュリティを高められるが、procstat は /proc の可視性に依存するため root が必要。なお、この制約は AWS 公式ドキュメントに記載がない(2026年4月時点)。


問題5: exe で Perl/Java プロセスを検出できない

事象

原因

前提知識

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バイナリであることがわかる。

解決策

// CW Agent 設定
{ "exe": "sshd",        ... }  // → Dimension: {exe: sshd}        ← ネイティブバイナリ
{ "pattern": "starman",  ... }  // → Dimension: {pattern: starman}  ← Perlスクリプト
{ "pattern": "tomcat",   ... }  // → Dimension: {pattern: tomcat}   ← JVMアプリ

cfn/alarms-custom.yamlProcTomcatWebProd / ProcStarmanWebProd で、exe ではなく pattern を Dimension に使用している例を確認できます。

教訓

ネイティブバイナリ(sshd, httpd, nginx 等)は exe、スクリプト言語や JVM アプリは pattern を使うこと。


問題6: apache2 のポート別インスタンスを個別監視できない(問題5の続き)

事象

原因

解決策

# 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 スクリプトが動かない

事象

調査過程

  1. cron 自体は正常にスクリプトを起動していた(/var/log/cron で確認)
  2. スクリプトのログに Python 2.7ImportError が出ていた
  3. cron の PATH(/sbin:/bin:/usr/sbin:/usr/bin)には /usr/local/bin が含まれない
  4. /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台だけ発生したのか

対策

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 が動作しない

事象

原因: プレースホルダがシェルに先回りで食われる

ここには 2つのレイヤ が関わっている。

  1. ${aws:InstanceId} は CW Agent の正式なプレースホルダで、Agent が起動時に EC2 メタデータから置換する 設計(${aws:ImageId} / ${aws:InstanceType} 等も同様)
  2. しかし 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 設定の更新で前回手順の変更を巻き戻す

構築終盤で踏んだ、地味だが最も再発しやすい事故。

事象

根本原因: 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.d directory, with the filename prefixed with either file_ (for local file sources) or ssm_ (for Systems Manager parameter store sources) to indicate the configuration origin.

解決策

  1. 「現行設定の原本(Single Source of Truth)」を1つ決める
    • インスタンスごとに「最新の Agent 設定 JSON」を1箇所に置く
    • 新しい手順書を作るときは、必ずその原本から派生させる
  2. 手順書の中で「原本はここ」と明示する
    • 「この設定を投入すると、apache2 ポート別監視が巻き戻ります」のような相互注意を書く

教訓

CW Agent の設定は「宣言的な全文上書き」。複数の変更要件が時間をまたいで来ると、手順書を順に実施するだけで前の変更が消える。変更を重ねるなら、原本を1つにまとめる運用にするしかない。


問題12: メンテナンス後にディスク Alarm が INSUFFICIENT_DATA に — NVMe デバイス名スワップ

サービス断ではないが、運用で必ず1度は踏む地味な事故。問題3 の発展形。

事象

原因: 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 Device from 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 to INSUFFICIENT DATA state.

つまり今回の現象は 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.yamlDiskMapServer がこのパターンの例:

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.yamlDiskEbs0WebcontentsWebProd でも同じパターンを採用している。

トレードオフ

drop_device を使うと、同じ InstanceId × 同じ path で異なる 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 ヘルスチェック用プレフィックスリストは リージョン別 に提供されており、名前自体に が含まれます(例: ap-northeast-1 では com.amazonaws.ap-northeast-1.route53-healthchecks)。com.amazonaws.global.route53-healthchecks という名前は存在しないので、CloudFront 等の本当にグローバルなマネージドリストと混同しないこと。

# 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

設計のポイント

教訓

AWS が「アドレス範囲を所有しつつ更新し続けるサービス」(Route53 ヘルスチェック / CloudFront / S3 等)に対して SG ルールを書くときは、マネージドプレフィックスリスト一択。IP 直書きは運用事故の温床。


モバイルバージョンを終了