監視ルールのドリフト検知とは、Prometheus に実際に配備されているアラート定義が、管理側の「正」であるテンプレートと一致しているかを機械的に確かめる仕組みである。ルールを直しても監視サーバへ届いていなければ意味がない。この記事では、ハブスポーク型のコード管理を前提に、内容を md5 で直接比べる検知をどう設計したかを書く。実装の過程で踏んだ二つの罠も残す。
監視ルールのドリフト検知とは何か
ドリフトとは、設定が時間の経過とともに正からずれていく現象を指す。英語の drift は「漂流する」という意味で、構成管理の文脈では「あるべき状態と、実際の状態の差」を表す語として使われる。
アラートルールは、多くの現場で Ansible などの構成管理ツールを使ってテンプレートから配備する。テンプレートが正であり、サーバ上のファイルはその写しである。ところが運用を続けると、この関係は静かに崩れる。誰かがサーバ上で直接ファイルを編集する。配備が途中で失敗し、片方だけ古いまま残る。テンプレート側を更新したのに、配備を忘れる。
ずれは、アラートが鳴ったときに初めて見えることが多い。しかも気づき方が遅い。本来鳴るはずのアラートが鳴らない、あるいは古い定義のまま誤ったアラートが鳴り続ける。前者は障害の見逃しに直結する。監視が壊れていることに、監視そのものでは気づけないという構造的な穴である。この穴を塞ぐのが、配備ドリフトの検知だと考えている。
前提となる Prometheus の監視構成とハブスポーク型コード管理
この仕組みを理解するには、二つの前提が要る。監視エンジンが Prometheus であること、そしてコード管理がハブスポーク型であることである。
Prometheus は、メトリクスを収集して保存し、あらかじめ書いたアラートルールを継続的に評価する監視エンジンである。ルールは監視サーバ上の /etc/prometheus/alert_rules.yml に置かれ、Prometheus 本体がそれを読み込んで評価する。ルールを更新したときは、プロセスを再起動するのではなくリロードで読み直す。今回の環境では SIGHUP によるリロードを使っている。収集と評価を止めないまま定義だけを差し替えられる点が、この運用の前提になっている。
監視は専用ノードで動かしている。monitor01 というコンテナ(192.168.1.15)に Prometheus と Grafana、Alertmanager を同居させ、そこで評価したアラートをチャットへ飛ばす構成である。監視の正であるテンプレートは、監視サーバ上には無い。管理側のリポジトリにある。
もう一つの前提が、ハブスポーク型のコード管理である。1台のコントローラ(ハブ)に Playbook とインベントリを集約し、対象ノード(スポーク)は配備を受ける側にする。スポークは設定を持つが、正は持たない。ハブから見てスポークは複数ある。Proxmox の仮想化ノード、監視サーバ、チャットサーバ、顧客ごとのコンテナが、それぞれスポークとして台帳に並ぶ。
この形の利点は、ハブの1ファイルを直せば次の配備で全スポークへ伝わることである。逆に弱点もある。スポークだけを直接触ると、正と実体がずれる。ずれは正しい配備をやり直せば直るが、ずれていることに気づかなければ直しようがない。ハブスポーク型のコード管理は、ドリフトを検知する仕組みと組にして初めて安定すると考えている。
ドリフト検知を作るきっかけ — Prometheus 配備の偽陽性
この仕組みを作る直接のきっかけは、Prometheus の配備で起きた偽陽性だった。監視ルールを配備する作業のなかで、ApiLatencyCollectorStale というアラートが誤って発火した。実際には遅延していないのに、古いデータを根拠に警告が出た。
調べると、原因は配備のずれにあった。テンプレートには修正が入っていたが、monitor01 には届いていなかった。届いていれば鳴らなかったアラートが、古い定義のまま鳴ったわけである。修正そのものは正しかった。正しかったが、届いていなかった。この二つは別の問題である。
ここで得た教訓は単純である。ルールを直すだけでは足りない。直したルールが本当に配備されているかを、別の仕組みで確かめなければならない。対策は候補がいくつかあったが、既存の配備用 Playbook を流用するのではなく、検知専用の Playbook を新しく書く方針を選んだ。配備と検知は、役割が違うからである。一つのコードに両方を担わせると、片方が壊れたときにもう片方も道連れになる。
ドリフト検知の設計 — md5 で内容を直接比べる
設計の方針は、差分の有無を「changed の数」ではなく「内容のハッシュ」で判定する、という一点に絞った。
既存の配備用 Playbook には --check を付けて差分を見る方法がある。だがこの方法は、チェックモードでも実行される補助タスクの影響を受けやすい。検証用のレンダリングなど、check_mode: false を指定したタスクが混ざっていると、changed の集計が汚染され、常に「差分あり」と出てしまう。常に差分ありと出る検知は、検知として機能しない。ノイズを出すだけの警報は、いずれ読まれなくなる。
そこで、次の手順にした。
- ハブ側で、正であるテンプレートを実際にレンダリングする。変数展開を済ませた実ファイルを作る。
- その実ファイルの md5 を取る。
- スポークである monitor01 に配備済みのファイルの md5 を、読み取り専用で取る。
- 二つの md5 を比較し、違えば差分ありとして通知する。
監視サーバは一切変更しない。stat で読むだけである。配備の正しさを確かめるために監視対象へ手を入れるのは、本末転倒になる。読み取り専用であることは、この仕組みを本番環境へ入れやすいという実務上の利点にもつながる。
レンダリングをハブ側で行う点も重要である。変数を展開した結果を比べなければ、「テンプレートは同じだが変数の値が違う」という差分を取り逃す。配備の失敗には、テンプレートの違いだけでなく、環境ごとの値の違いも含まれる。比較は、最終的な成果物のレベルで行うのが正しい。
ドリフト検知の実装で踏んだ2つの落とし穴
実装は一発では通らなかった。ここでは、実際に踏んだ2つの落とし穴を記録しておく。どちらも、走らせて初めて分かる類のものである。
落とし穴1 — lookup が末尾の改行を落とし、md5 が食い違う
最初の実装は、md5 を lookup('file', path) | hash('md5') で計算していた。これが偽陽性を生んだ。
実測では、ハブ側のレンダリング結果と monitor01 の配備済みファイルは完全に一致していた。ところが、ハブ側で計算した md5 だけが食い違った。サイズも 27343 バイトと 27342 バイトで、1 バイトずれていた。わずか 1 バイトである。しかしハッシュは 1 バイト違えばまったく別の値になる。
原因は、lookup('file') が末尾の改行を strip(除去)する挙動にあった。テンプレートのレンダリング結果は末尾に改行を持つのに、lookup がそれを落としてハッシュを計算する。中身は同じなのに、ハッシュだけが別物になる。
これは質の悪い偽陽性である。差分があると警告が出るが、実際には何もずれていない。偽陽性が続けば、警告そのものが無視されるようになる。監視の信頼を失う一番の近道である。オオカミ少年の状態を作ってしまう。
対策は、ハッシュの取得方法を変えることだった。ansible.builtin.stat に checksum_algorithm: md5 と get_checksum: true を指定し、ファイルそのもののチェックサムを取る。この方法は末尾改行の影響を受けない。ファイルが存在しない場合は MISSING として扱い、それ自体を差分として検知できる。
教訓は、ハッシュを取る方法まで検証する必要がある、ということである。同じ「md5 を比べる」でも、計算の入口が違えば結果は変わる。ライブラリの便利な関数ほど、こうした暗黙の正規化を裏で行っていることが多い。
落とし穴2 — タスク名のコロンで Ansible Playbook が起動しない
二つ目の落とし穴は、もっと単純で、そして恥ずかしいものだった。偽陽性対策でタスク名に注記を足したところ、Playbook が起動しなくなった。
付けたタスク名は (stat: no rstrip, no truncation) である。この中の : (コロン+空白)が問題だった。YAML では、クォートされていない値の中のコロン+空白を、キーと値の区切りとして解釈する。そのためパーサが構文エラーを出し、Playbook は実行どころか読み込みの時点で失敗した。
エラーメッセージは「Colons in unquoted values …」だった。原因はメッセージにそのまま書かれている。にもかかわらず、最初はタスク名が原因だとは気づかなかった。Playbook の中身を直した覚えがないのに動かなくなった、という見え方をするからである。
対策は二つある。第一に、タスク名から : を除去する。第二に、全タスク名をダブルクォートで保護する。YAML の文字列は、迷ったらクォートする。これで再発しなくなった。地味な話だが、構成管理のコードを書くときは常に意識しておきたい。
この二つの落とし穴には共通点がある。どちらも「シェルで試したら動いた」では見つからない。実際に Playbook として走らせ、エラーを見て初めて分かる。構成管理のコードは、書いた時点では正しさが確定しない。走らせて確かめるところまでを一組にしたい。
ドリフト検知が扱う範囲と、扱わない範囲
この仕組みが確かめるのは、一つのことだけである。ハブのテンプレートから生成されるルールファイルの内容が、監視サーバ上のファイルと一致しているか。それ以外は扱わない。
扱わないものの例を挙げる。ルールの文法が正しいかどうか、閾値の設定が妥当かどうか、サイレンスが意図せず残っていないか、配備そのものが成功したか。これらは別の仕組みか、人の判断に属する。とくにルールの文法は、配備の側で promtool による検証を済ませている。
範囲を絞るのは、検知の意味を一義に保つためである。一つの仕組みが複数のことを同時に判定すると、警報が出たときに「何がずれたのか」が曖昧になる。曖昧な警報は、対処の初動を遅らせる。
とくにサイレンスの残存は、監視の沈黙という別の失敗を生む。監視の穴は一つではない。一つずつ、担当を決めて塞いでいくのが現実的である。
ドリフト検知の通知 — 差分があるときだけに絞る
通知の設計も決めておく。差分があるときだけ通知し、差分がないときは黙る。正常時に何も言わない仕組みは、異常時だけ人の注意を引く。逆に、毎回「正常です」と通知する仕組みは、すぐに読まれなくなる。
通知先はチャットにした。本文には、ハブ側の md5、監視サーバ側の md5、対象ファイルのパス、そして復旧手順を入れる。復旧手順まで書いておくと、通知を受け取った人がその場で動ける。検知して終わりではなく、解消までを一続きにする。復旧は、ハブから正しいルールを配備し直すコマンドを1行実行するだけである。
もう一つ、通知の宛先を絞ることも考えた。だが、特定の個人へ送ると、その人が休んだときに止まる。監視の通知は、属人性を下げる方向へ倒したい。誰が見ても同じ判断ができる形にするのが望ましい。
md5 を二つ並べて出すのは、目視での確認を可能にするためである。値そのものが正しいかを人が確かめられる余地を残す。自動化は便利だが、最終判断の材料は提示しておきたい。
ドリフト検知の実測 — 正常系と異常系の両方を確かめる
仕組みは、正常に動くことと、異常を検知することを両方確かめなければ意味がない。正常系だけでは、いつも「差分なし」と言うだけの飾りになる。
正常系では、ハブのテンプレートのレンダリング結果と、monitor01 に配備済みのファイルの md5 が一致し、差分なしと判定された。監視サーバへの変更はゼロである。
異常系の確認は、判定軸を一つに絞って行う。配備済みファイルの中身が正からずれていれば md5 が変わり、差分ありとして通知が飛ぶ。ファイルが存在しなければ MISSING となり、これも差分として扱う。どちらの経路も、md5 という一つの判定軸に集約されている。判定軸が一つだと、結果の解釈で迷わない。
通知が飛ぶ条件も確かめた。差分なしのときは送信されない。この「黙ること」の確認は忘れやすい。正常時に送ってしまう仕組みは、作った本人が気づかないままノイズを撒き続ける。
ハブスポーク構成でのドリフト検知の運用
ドリフト検知は、一度作って終わりではない。定期的に走らせて初めて価値が出る。タイマーで日次に実行し、差分が出たときだけ人へ届く形にした。実行は毎朝7時、実行できなかった日を飛ばさないよう Persistent を付けている。
ハブスポーク構成では、この定期実行の位置づけがはっきりしている。ハブに置いた検知の Playbook が、スポークの一つである監視サーバを読み取り専用で調べる。スポーク側に専用の常駐プログラムを足す必要はなく、ハブのスケジュールだけで完結する。スポークが増えても、監視対象の一覧をハブ側で管理すれば同じ仕組みが使える。
運用で見るべき点は三つある。第一に、通知の頻度である。差分の通知が毎回出るなら、それは配備が止まっているか、検知の判定が誤っているかのどちらかである。第二に、検知の対象範囲である。ルールファイルだけでなく、監視設定全般へ広げる余地がある。第三に、復旧の手順である。差分を検知したら正しい配備を実行して解消する。この手順を通知本文に書いておくと、受け取った人がすぐ動ける。
実行の頻度は、配備の頻度と対象の重要度で決める。ルールの変更が週に何度もあるなら日次、ほとんど動かないなら週次で足りる。検知そのものが負荷になるほど頻繁に回す必要はない。
検知は目的ではなく手段である。ずれを見つけること自体ではなく、ずれを放置しない状態を作ることが狙いになる。
まとめ — 監視ルールのドリフト検知で「配備したつもり」を潰す
まとめる。監視ルールのドリフト検知は、Prometheus のアラート定義について、ハブのテンプレートという正と、スポーク上の実体のずれを見つける仕組みである。ハブスポーク型のコード管理は、正を一箇所に集める代わりに、ずれに気づきにくいという弱点を持つ。検知はその弱点を補う。
作る過程で二つの罠を踏んだ。一つは、ハッシュの計算方法が末尾改行で変わること。もう一つは、タスク名のコロンで YAML が壊れること。どちらも、実際に走らせて確かめなければ気づけない類のものだった。
本質は、配備と検知を分けることにある。配備したつもり、を機械的に潰す。監視の信頼は、鳴ることではなく、鳴るべきときに確実に鳴ることに支えられていると考えている。
(参考)アラートを起点にした一次調査の自動化はアラート一次調査の自動化に書いた。日々の改修をどう記録しているかは私のAI環境の見取り図で扱っている。