【テクニカル・上級編】ZabbixのWebシナリオ監視で高度な認証(OAuth2/JWT)を突破してWebアプリケーションを死活監視する方法 – 運用監視・オブザーバビリティ活用バイブル

Zabbixの限界を撃ち抜け:OAuth2/JWT認証を完全攻略するモダンWebシナリオ監視の極意

システムがマイクロサービス化し、認証基盤がOIDC(OpenID Connect)やOAuth2、JWT(JSON Web Token)へ移行した現代において、かつての「URLを叩いてステータスコードを200にするだけ」の監視は、もはや何の価値も生まない。死活監視をしているつもりで、実際には「認証基盤の死によってアプリケーションが生きていても検知できない、あるいはその逆」という偽りの平穏に浸っている現場があまりにも多すぎる。

Zabbixは偉大なツールだ。しかし、標準の「Webシナリオ」機能(HTTPエージェント等)は、古き良きセッションクッキーやベーシック認証の世界で止まっている。動的なトークン発行、リクエストごとの署名、JSONペイロードの構築を伴うモダンな認証フローを、Zabbix標準機能だけで美しくハンドリングするのは至難の業だ。

今回は、Zabbixの内部機構とJavaScript(Pre-processing / Custom JavaScript)のポテンシャルを極限まで引き出し、OAuth2(Client Credentials / Authorization Code)およびJWTを用いた高度な認証フローを完全に突破し、真のビジネスロジック死活監視を構築する設計思想と実践コードを授けよう。

—

1. アーキテクチャの解剖:なぜ標準のWebシナリオでは太刀打ちできないのか?

ZabbixのWebシナリオは、一連のHTTPリクエストをシーケンシャルに実行し、クッキーの保存や文字列マッチを行う。しかし、以下のモダンな要件に直面した途端に破綻する。

1. トークンの動的ライフサイクル: アクセストークンには有効期限(TTL)があり、ハードコードはできない。リクエストごとに有効性を担保するか、キャッシュ戦略が必要。
2. 暗号学的署名とペイロード: JWTの場合、特定のクレーム(`sub`, `aud`, `iat`, `exp`など)を含めたJSONを構築し、必要に応じて秘密鍵で署名する必要がある。
3. 多段のフェッチ:

  • ステップ1: 認可サーバーへClient ID / Secretを送り、OAuth2トークンを取得。
  • ステップ2: 取得したJWTを `Authorization: Bearer ` ヘッダーにインジェクトし、保護されたAPIエンドポイントを叩く。
  • ステップ3: 返却されたレスポンスのJSON構造をパースし、内部ステータスが「Healthy」か検証する。

これを実現するためには、Zabbixの「HTTPエージェント」アイテムと、その強力な「前処理(Preprocessing)」としてのJavaScript実行エンジンを組み合わせる必要がある。Zabbixサーバーの内部でV8(あるいはそれに準ずるJSエンジン)を回し、完全なステートフルな認証セッションをコードとしてエミュレートするのだ。

—

2. 実装の全貌:OAuth2 (Client Credentials) 突破スクリプト

以下に、Zabbixの1つの「HTTPエージェントアイテム」だけで完結させ、失敗時には詳細なエラー理由をトリガーに引き渡すための、洗練されたカスタムJavaScriptの全コードを提示する。

このアプローチの美しさは、Zabbixのマクロ(`{$OAUTH_TOKEN_URL}`, `{$API_ENDPOINT}`, `{$CLIENT_ID}`, `{$CLIENT_SECRET}`)を動的に参照しつつ、外部スクリプトファイルに依存せずZabbixのデータベース内で完結する点にある。

ステップ1: Zabbixアイテムの設定方針

  • タイプ: HTTPエージェント
  • URL: `{$OAUTH_TOKEN_URL}` (最初にトークンエンドポイントを叩くが、実質的な処理はプリプロセスで行うため、プレースホルダー的なURLでも可)
  • リクエスト型式: POST
  • データ型: テキスト
  • 前処理 (Preprocessing):
  • 「JavaScript」を追加し、以下のコードを記述する。

ステップ2: 究極のカスタムJavaScript(前処理用)

// =================================================================
// Zabbix Advanced OAuth2 / JWT Dead-Man Sniffer
// Architecture: HTTP Agent + JS Preprocessing
// =================================================================

// 1. マクロからのパラメータ取得(Zabbix環境変数)
var tokenUrl = value; // HTTPエージェントのURLにトークンエンドポイントを指定した場合
var clientId = “{$OAUTH_CLIENT_ID}”;
var clientSecret = “{$OAUTH_CLIENT_SECRET}”;
var apiEndpoint = “{$API_PROTECTED_ENDPOINT}”;
var scope = “api:read”;

// 2. HTTPクライアントの初期化
var request = new CurlHttpRequest();
request.AddHeader(“Content-Type: application/x-www-form-urlencoded”);

// タイムアウト設定(接続3秒、読み込み5秒)
// ※障害時にZabbixのポーラープロセスをブロックさせないためのフェイルセーフ
request.SetTimeout(8);

// —————————————————————–
// Phase 1: OAuth2 トークン取得 (Client Credentials Flow)
// —————————————————————–
var postData = “grant_type=client_credentials” +
“&client_id=” + encodeURIComponent(clientId) +
“&client_secret=” + encodeURIComponent(clientSecret) +
“&scope=” + encodeURIComponent(scope);

Z.log(4, “[OAuth2 Watcher] Requesting token from: ” + tokenUrl);

var tokenResponse = request.Post(tokenUrl, postData);
var tokenStatus = request.GetInfo().http_code;

if (tokenStatus !== 200) {
throw “Failed to obtain OAuth2 token. HTTP Status: ” + tokenStatus + “, Response: ” + tokenResponse;
}

var tokenJson;
try {
tokenJson = JSON.parse(tokenResponse);
} catch (error) {
throw “Failed to parse token JSON: ” + error + “. Raw response: ” + tokenResponse;
}

var accessToken = tokenJson.access_token;
if (!accessToken) {
throw “OAuth2 response does not contain ‘access_token’. Response: ” + tokenResponse;
}

// —————————————————————–
// Phase 2: JWT / Bearer トークンを用いた保護リソースへのアクセス
// —————————————————————–
// ヘッダーをリセットし、AuthorizationにJWT/Bearerを設定
request.ClearHeader();
request.AddHeader(“Authorization: Bearer ” + accessToken);
request.AddHeader(“Accept: application/json”);
request.AddHeader(“X-Zabbix-Monitor: HealthCheck”);

Z.log(4, “[OAuth2 Watcher] Fetching protected resource: ” + apiEndpoint);

var apiResponse = request.Get(apiEndpoint);
var apiStatus = request.GetInfo().http_code;

if (apiStatus !== 200) {
throw “Protected API returned non-200 status. HTTP Status: ” + apiStatus + “, Response: ” + apiResponse;
}

// —————————————————————–
// Phase 3: ビジネスロジックの検証 (Deep Health Validation)
// —————————————————————–
var apiJson;
try {
apiJson = JSON.parse(apiResponse);
} catch (error) {
throw “Failed to parse API response JSON: ” + error + “. Raw response: ” + apiResponse;
}

// 例外的なアプリケーション層のエラーハンドリング(例: {“status”: “DEGRADED”, “db”: “disconnected”})
if (apiJson.status !== “UP” && apiJson.status !== “HEALTHY”) {
throw “Application health check failed. Internal status: ” + (apiJson.status || “UNKNOWN”) + “, Details: ” + apiResponse;
}

// すべての関門を突破。正常時は監視対象の主要なメトリクスやレスポンスタイムを返す
// ここでは監視成功の証としてAPIのレスポンスタイム(ミリ秒)を返す
var responseTimeMs = request.GetInfo().total_time 1000;

return JSON.stringify({
status: “OK”,
http_code: apiStatus,
response_time_ms: Math.round(responseTimeMs),
server_time: apiJson.timestamp || “N/A”
});

—

3. エキスパート向け最適化ハック:Zabbix内部アーキテクチャの制御

この高度な監視を導入するにあたり、現場のアーキテクトが絶対に押さえておかなければならない「パフォーマンスとリソースの罠」が存在する。これを無視すると、Zabbixサーバー全体のパフォーマンスが崩壊する。

A. ポーラープロセス(Poller Process)の枯渇を防ぐ非同期・タイムアウト設計

JavaScript内での外部HTTP通信(`CurlHttpRequest`)は、同期的(Blocking)に実行される。
もし認可サーバーやAPIエンドポイントがスローダウンした場合、Zabbixの `StartPollers` プロセスがそのスレッドに縛り付けられ、他の数千のICMP監視や標準Zabbixエージェントの収集が遅延する。

  • 対策:

1. `request.SetTimeout()` は必ず厳格に設定すること(上記コードでは8秒)。
2. この高度な監視を行うアイテム専用のカスタムプロセスポール(Zabbix 5.4以降で導入された `Pre-processing workers` や専用のカスタムプロキシ構成、あるいは `StartPollers` のサイジング見直し)を設計する。通常の死活監視と同じプールに混ぜてはならない。

B. JWTのキャッシュ戦略:認可サーバーへのDDoSを防ぐ

OAuth2の `client_credentials` は、トークンを発行するたびに認可サーバー(Keycloak, Auth0, Azure ADなど)のDBや暗号化処理に負荷をかける。
もしこの監視を 1分間隔 で数十個のマイクロサービスに対して実行した場合、認可サーバー自体を監視トラフィックで叩き落すという本末転倒なインシデント(セルフDDoS)が発生する。

  • エキスパートの知見:

Zabbixのキャッシュ機能(グローバル変数や、Zabbix 6.0+の `User Macros` やカスタムキャッシュ機構はJS内では直接永続化できない)の限界を突破するため、「トークンの有効期限(TTL)を考慮したインメモリ・ステート維持」をJSのスコープ外(Zabbixのローカルキャッシュ領域、またはエフェメラルな仕組み)で考える必要がある。
現実的な落としどころとして、トークン取得のコストが無視できない大規模環境では、Zabbixの標準機能ではなく、専用の軽量なプロキシデーモン(Go製など)をローカルに立てて、Zabbixはそのローカルプロキシの `/health` を叩くだけにするのがベストプラクティスだ。しかし、どうしてもZabbix単体で完結させたい場合は、監視間隔を 5分以上 に引き離し、認可サーバー側のレートリミットに引っかからない設計にすることを強く推奨する。

—

4. 障害予兆検知とトリガー設計の神髄

「APIが死んだ」ことを検知するだけでは二流だ。真のオブザーバビリティは、「トークン発行の遅延」や「APIレスポンスの劣化」から障害の予兆を検知することにある。

上記スクリプトは最終的にJSON文字列を返すため、Zabbixの「JSONpath」前処理を組み合わせることで、返却されたオブジェクトを個別のメトリクスとして抽出できる。

1. アイテムの複製とJSONpath抽出:

  • メインアイテムのキー: `app.oauth.health`
  • レスポンスタイム抽出用アイテム(依存アイテム):
  • プレプロセス: JSONpath `$.response_time_ms`

2. 高度なトリガー式の構築:

{app.oauth.health:last()#”OK”}
and
{app.oauth.health:nodata(3m)}=0

(※スクリプトが例外を吐いた場合、アイテムは「Not supported」になるか、エラー文字列を返すため、それをトリガーで捕捉する)

さらに、レスポンスタイムのトレンド異常検知:

{app.oauth.health.response_time_ms:avg(10m)} > 1500
and
{app.oauth.health.response_time_ms:avg(1h)} 1.5 < {app.oauth.health.response_time_ms:avg(10m)} 「過去1時間の平均と比較して、直近10分の平均レスポンスタイムが1.5倍以上に跳ね上がり、かつ1.5秒を超えている場合」にのみ発報する。これにより、OAuth2基盤やAPIサーバーの突発的なボトルネック(コネクションプールの枯渇やGCの頻発など)を、完全停止する前に予兆として検知できる。 ---

5. 結び:監視とは「システムとの対話」である

ZabbixのWebシナリオやHTTPエージェントをここまで追い込むエンジニアは少ない。「動かないから別の高額なSaaS監視ツールを入れる」という安易な選択に走る前に、ZabbixのコアにあるJavaScriptエンジンの力を信じろ。

OAuth2やJWTという「現代の城壁」であっても、そのプロトコル仕様を正確に理解し、スクリプトのなかに論理的な道筋さえ描いてやれば、Zabbixは従順かつ最強の番犬へと生まれ変わる。

コードを書き、ログレベルを上げ(`Z.log(4, …)`)、Zabbixサーバーの `/var/log/zabbix/zabbix_server.log` を監視しながら、手塩にかけて育て上げた監視パイプラインが緑色のステータスを灯した瞬間――それこそが、インフラストラクチャー・エンジニアリングの至福の時である。

タイトルとURLをコピーしました