0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Grafana Alert を活用した Context-Aware なワークロード監視

Last updated at Posted at 2025-12-03

この記事は Grafana Advent Calendar 2025 の 3 日目になります。

株式会社サイバーエージェント 2024 年 新卒入社の @ren510dev です。
現在は、子会社の 株式会社 AbemaTV にて SRE をしています。

普段は Google Cloud / AWS を中心に各種 OSS や SaaS を用いたマルチクラウド基盤の構築・運用を担当しています。最近では Datadog を活用した Observability 基盤の改善や分散トレーシングの推進ミッションにも参入しはじめました。

さて、本アドベントカレンダーは「Grafana」がテーマとなっているわけですが、この記事では、集約したメトリクスデータをもとにアラートを配信する「Grafana Alert」の活用事例について紹介したいと思います。

はじめに

ABEMA では 300 以上のマイクロサービスを運用する主要基盤として、Google Kubernetes Engine(GKE)を採用しています。我々のチームでは、運用負荷軽減の観点からクラスタのオートアップグレードをはじめとする様々な運用自動化の仕組みを整備していますが、その中で避けて通れない課題の一つが ノードアップグレード時のワークロードステータス監視 でした。

今回、Google Cloud のイベント通知機能を活用し、ABEMA の主要監視基盤である Grafana と連携させることでアップグレード時の異常を即座に検知・通知する仕組みを整備したので、その事例について紹介したいと思います。

なお、ABEMA における監視基盤の構成や Grafana の活用事例については こちらの資料 で紹介されています。

クラスタオートアップグレードと監視のジレンマ

Kubernetes のエコシステムは非常に進化が速く、数ヶ月単位で新しいマイナーバージョンがリリースされます。新しいバージョンでは、セキュリティパッチの適用はもちろん、パフォーマンスの改善、新機能の追加が行われるため、プラットフォームを健全に保つためには継続的なアップグレードが不可欠です。

ABEMA のような大規模なメディアサービスでは、複数のクラスタに跨って数百のノードが稼働しています。これらを手動で順次アップグレードしていくのは運用負荷が非常に高く、ヒューマンエラーのリスクも伴います。

そこで我々は、GKE のオートアップグレード機能を全面的に採用しています。これにより、Google が管理するスケジュールに従って、ノードプール単位で自動的に OS や Kubernetes バージョンの更新が行われます。

アップグレード中に発生する「正常な異常」

オートアップグレードは運用を楽にしてくれますが、監視の観点からは非常に厄介な問題を引き起こします。GKE の サージアップグレード 等の戦略を用いてノードを更新する際、以下のようなプロセスが自動的に進行します。

  1. Node Creation:新しいバージョンのノードが作成され、クラスタに参加
  2. Cordon:古いノードがスケジュール禁止状態になる
  3. Drain / Eviction:古いノード上の Pod が Eviction され、新しいノードへ再スケジュールされる 👈
  4. Node Deletion:古いノードが削除される

このプロセスにおいて、アプリケーションの Pod は一時的に Terminating になったり、新しいノードでのイメージ Pull や起動待ちで PendingContainerCreating といったステータスになります。これらは Kubernetes の仕組み上、正常な再配置プロセスですが、単純な死活監視ルールから見ると Pod が起動していない異常事態と区別がつきません。

アラートの形骸化

もし、通常の監視ルール(例:Pod が Running 以外なら即アラート)をそのまま適用しているとどうなるでしょうか。オートアップグレードが実行される度に、Slack のアラートチャンネルは数十〜数百件の「Pod 異常」通知で埋め尽くされます。特に大規模クラスタでは、一度に数十台のノードが入れ替わることもあり、通知の量は膨大になります。

このような状況が続くと、運用担当者は「アップグレードに伴う一時的なエラーだろう」と判断し、アラートを確認せずに軽視しはじめます。いわゆる「アラートの形骸化」です。

アラートが常態化すると、本当に対応が必要な障害(例:クラスタアップグレード起因の互換性問題で CrashLoopBackOff している)が、アップグレードのノイズに紛れて見過ごされてしまいます。過去にはアップグレードの通知に埋もれて、一部の重要でないバッチジョブが失敗し続けていることに気づくのが遅れたケースもありました。

既存の解決策とその限界

この問題に対して、これまではアップグレードのタイミングで担当者が張り付き、目視で監視を行うという運用でカバーしていましたが、管理するクラスタ数やノード数が増加するにつれて、以下のような限界を迎えていました。

  • トイルの増大:そもそもこの作業自体がトイルになってしまっている
  • 判断の属人化:「このエラーは無視して良いか」の判断が担当者の経験に依存してしまう
  • 見逃しリスク:大量の通知の中から、本当に重要なエラーを目視で見つけ出すのは困難

一般的な対策として、PagerDuty の Maintenance Window や Alertmanager の Silence 機能を使うことも考えられますが、アップグレードの開始・終了時刻が予測しづらい GKE の仕様上、これらを静的に設定するのは困難です。

我々が求めていたのは、アップグレード中という 特定のコンテキストにおいてのみ専用の監視ロジックを適用し、それ以外は完全に無効化する という、動的でメリハリのある監視システムでした。

イベント駆動型の監視システム

これらの課題を解決するべく、GKE のアップグレードイベントをトリガとして Cloud Run を発火させ、アラート発報の最終決定を行う Grafana Alert を動的に制御する内製ツールを構築しました。

GKE Cluster Notifications

GKE にはクラスタアップグレードに関する情報を Pub/Sub 経由で公開する機能がデフォルトで用意されています。Cloud Run は、受け取ったイベント情報から、アップグレードの開始と終了のタイミングを検知して、Grafana Alert を動的に制御します。

GKE から受け取れるイベントの種類は以下の通りです。

イベントの種類 内容
SecurityBulletinEvent 構成またはバージョンに直接関連するセキュリティアラートを通知
UpgradeAvailableEvent 新しいバージョンが利用可能になったことをクラスタに通知
UpgradeEvent アップグレードが開始されたことを通知
UpgradeInfoEvent アップグレードの終了ステータスを通知

ここで、UpgradeEvent イベントには、対象となるコンポーネント、ノードプールの種類、アップグレード後のバージョン情報等が含まれています。

{
  "attributes": {
    "cluster_location": "asia-east1",
    "cluster_name": "my-cluster",
    "payload": {
      "resourceType": "NODE_POOL",
      "operation": "operation-1736935200000-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "operationStartTime": "2025-01-15T10:00:00.000000000Z",
      "currentVersion": "1.29.10-gke.1000",
      "targetVersion": "1.30.5-gke.1500",
      "resource": "projects/my-project/locations/asia-east1/clusters/my-cluster/nodePools/private-pool-blue"
    },
    "project_id": "123456789012",
    "type_url": "type.googleapis.com/google.container.v1beta1.UpgradeEvent"
  }
}

また、処理が完了すると UpgradeInfoEvent イベントから、オペレーションがどのようなステータスで終了したのかを受け取ることができます。

ステータス 内容
SUCCEEDED アップグレードが正常に完了した
FAILED (何らかの理由により)アップグレードが失敗した
CANCELED (技術的な理由またはユーザ起因により)アップグレードオペレーションがキャンセルされた
{
  "attributes": {
    "payload": {
      "resourceType": "NODE_POOL",
      "operation": "operation-1736935200000-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "startTime": "2025-01-15T10:00:00.000000000Z",
      "endTime": "2025-01-15T10:20:00.000000000Z",
      "currentVersion": "1.29.10-gke.1000",
      "targetVersion": "1.30.5-gke.1500",
      "resource": "projects/my-project/locations/asia-east1/clusters/my-cluster/nodePools/private-pool-blue",
      "state": "SUCCEEDED"
    }
  },
  "type_url": "type.googleapis.com/google.container.v1beta1.UpgradeInfoEvent"
}

Grafana Alert

grafana-alert-logo.png

Grafana Alert は Grafana に統合されている機能の一つで、監視しているデータの異常をクエリや閾値に基づいて検出、アラート配信するサービスです。

Grafana では、ダッシュボード上のグラフやメトリクスデータを活用して特定の閾値を設定し、条件を満たした際にアラートをトリガすることができます。Grafana Alert を活用することで、システムやパフォーマンスの異常を早期に検出して障害を未然に防ぐことが期待できます。

Grafana Alert の詳細な機能について こちらのブログ でも紹介しています。

今回は主に以下の理由から、アラート判定エンジンとして Grafana Alert を採用することにしました。

API による動的な制御が可能(Programmability)

Grafana Alert は、アラートルールの作成や編集だけでなく、ルールの一時停止(Pause)や再開(Unpause)といったステータス変更を含めたほぼ全ての操作を REST API 経由で行うことができます。

既存のエコシステムとの親和性

先で書いた通り、ABEMA は主要監視基盤として Grafana を利用しています。Grafana Alert を利用すればアラート機能のためだけに別のツールを導入するコストをかけず、使い慣れた PromQL を用いて条件閾値を記述することができます。

アーキテクチャ

architecture.png

ワークロード監視の開始

  1. GKE コンポーネントのアップグレードが開始されると UpgradeEvent が Pub/Sub 経由で Cloud Run Service に通知される

  2. Cloud Run Service はアップグレードが開始されたことを通知するメッセージを Slack に送信する
    01.png

  3. この時、Cloud Run Service は Grafana Alert Provision API を用いて Pod のステータス監視に関する Alerting rule の Pause を解除する

  4. Grafana Alert はアップグレード途中に発生した Pod の異常ステータスを Slack に通知する
    02.png

  5. アップグレードが終了すると UpgradeInfoEvent が再度 Pub/Sub 経由で Cloud Run Service に通知される

  6. Cloud Run Service はアップグレードが終了されたことを通知するメッセージを Slack に送信する

SUCCEEDED それ以外
03-1.png 03-2.png

ワークロード監視の終了

  1. Cloud Run Service はアップグレード終了後に Pod のステータス監視を継続するクラスタの情報を Eventarc API(gRPC) を経由して Cloud Tasks にキューイングする
  2. Cloud Tasks は一定期間経過後(※1)に Pod のステータス監視を終了するイベントフックをトリガする
  3. Cloud Run Service は Cloud Tasks のイベントを受信すると、Pod のステータス監視に関する Alerting rule の Pause を再度有効化する
  4. 最後に Cloud Run Service は Pod のステータス監視終了を Slack に通知する
    04.png

※1:アップグレード終了後、Pod のステータス監視を継続する時間について

Cloud Run Service は最大起動時間(1 時間)を超えて、ワークロードを実行することはできませんが、Cloud Tasks を用いることで 1 時間を超える監視期間を延ばすことが可能です。

実装の詳細

ワークロード監視のトリガ制御

ワークロード監視のトリガには Cluster Notifications を使用します。Terraform の場合以下のように設定するだけで、イベントが Pub/Sub Topic に飛んできます。

resource "google_container_cluster" "primary" {
  # ...
  notification_config {
    pubsub {
      enabled = true
      topic   = google_pubsub_topic.gke_upgrade_notify.id
    }
  }
}

通知されるイベントにはいくつか種類がありますが、重要なのはフィルタリングです。GKE のアップグレードには Control-Plane と Data-Plane(Node Pool)の 2 種類があります。Control-Plane のアップグレード中は API Server が瞬断することがあっても、ノード上のワークロードは稼働し続けるため、Pod の再起動監視は不要です。

Cloud Run アプリケーション内では、以下のようなロジックで NODE_POOL のイベントのみを抽出します。

// 実装例
func (h *Handler) ReceiveEvent(ctx context.Context, msg *PubSubMessage) error {
    event, err := parseGKEEvent(msg.Data)
    // ...

    // Master アップグレードの場合はログのみ保存
    if event.ResourceType == "MASTER" {
        log.Info("Skipping MASTER upgrade event")
        return nil
    }

    // Node Pool アップグレードの場合のみ処理続行
    if event.ResourceType == "NODE_POOL" {
        if isStartEvent(event) {
            return h.startMonitoring(ctx, event.ClusterName)
        } else if isEndEvent(event) {
            return h.scheduleStopMonitoring(ctx, event.ClusterName)
        }
    }
    return nil
}

メトリクスのフィルタリング

監視を有効化した後、実際にどのような条件でアラートを発火させるかは Alerting Conditions で設定します。アップグレード中は Pod の再起動が頻繁に行われるため、単に Running でないものを全て通知するとノイズだらけになります。

色々試した結果、以下の PromQL で本当に異常な状態だと判断したものだけを抽出するようにしています。

### Alerting Condition の一例
group by (platform, platform_id, region, cluster, exported_pod, exported_namespace, status) (
  (
    # 1. Pod Phase が Running/Succeeded 以外
    label_replace(
      last_over_time(kube_pod_status_phase{phase!="Running", phase!="Succeeded", platform!="aws"}[1m]),
      "status", "$1", "phase", "(.*)"
    )
    or
    # 2. Container Waiting (ImagePullBackOff 等)
    label_replace(
      last_over_time(kube_pod_container_status_waiting_reason{reason!="ContainerCreating", reason!="PodInitializing", platform!="aws"}[1m]),
      "status", "$1", "reason", "(.*)"
    )
    or
    # 3. Container Terminated (Error 等) ※OOMKilled は除く
    label_replace(
      last_over_time(kube_pod_container_status_terminated_reason{reason!="Completed", reason!="OOMKilled", platform!="aws"}[1m]),
      "status", "$1", "reason", "(.*)"
    )
    or
    # 4. その他の Terminated 理由
    label_replace(
      last_over_time(kube_pod_container_status_terminated_reason{reason!="Completed", platform!="aws"}[1m]),
      "status", "$1", "reason", "(.*)"
    )
  ) >= 1
)

label_replace によるラベルの正規化

異なるメトリクス(phase, reason)から取得した異常理由を、共通のラベル status に統一しています。これにより、通知時に何が原因で落ちているか(ImagePullBackOff なのか Error なのか)を一元的に管理します。

Pending Period によるフィルタリング

  • Pending period:5m
  • Evaluation interval:1m

アラートルール側で「5 分間継続して条件を満たした場合のみ発火」という設定を入れています。アップグレードに伴う通常の Pod 再起動(PendingContainerCreating)は、大抵数分以内に完了して Running に戻るため、このフィルタにより通知されません。

一方で、リソース不足や API 互換といったワークロードに潜在的に起因した問題の場合、5 分以上スタックしている Pod は明らかな異常と判断され、アラートが飛びます。

経過時間 クエリ評価 状態
00:00 1(条件を満たす) Pending 開始(まだ通知しない)
01:00 1(条件を満たす) Pending 継続
... ... ...
05:00 1(条件を満たす) Firing(発火)
06:00 0(状態が解消) Resolved(復帰)

このように、PromQL で論理的にエラー状態を定義しつつ、Pending Period で時間的なフィルタをかけることで、ノイズを極限まで減らしています。

通知頻度の制御

Alerting Conditions で検出した異常状態を最終的に配信するわけですが、すべてのレコードを通知してしまうと同じ内容が複数回飛んでしまう可能性があります。Grafana Alert では Notification Policy と呼ばれる仕組みが用意されており、これによって通知頻度を制御することができます。

alert-instance-state.png

Notification Policy における Grouping を設定しておくと、アラートのノイズを大幅に削減することができます。例えば、単一の Pod が CrashLoopBackOff になった場合、その Pod が異常状態にあることを示す通知は一件で十分です。

route:
  group_by: ["cluster_name", "alertname"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

この設定では、group_wait: 30s とすることで、最初のアラートが発生してから 30 秒間は通知を保留し、その間に発生した同一グループ(cluster_name, alertname)のアラートを一つにまとめます。

また、group_interval: 5m により、状態が継続している場合の更新通知も 5 分おきに間引かれます。

これにより、仮に数十の Pod が同時に異常となっても、Slack に届くのは「XX クラスタで Pod 異常が発生」という要約された 1 件の通知となり、大量の通知によるノイズを防ぐことができます。

ちなみに Grafana Alert には Alerting History という機能があり、アラートの発火履歴を確認することができます。

alerting-history.png

GSA と Grafana User の紐付け

ABEMA では Grafana を GKE(VPC)内でセルフホスティングしており、外部からのアクセスはすべて Identity-Aware Proxy(IAP) を経由します。また、Cloud Run から Grafana Alert を制御する場合には、IAP 認証とは別に Grafana のユーザ認証が必要になります。

Google Cloud SDK(idtoken で取得した Google Service Account(GSA)の ID Token は IAP の認証を通過できますが、Grafana はこのトークンをユーザ情報として扱えません。これは、IAP が GSA の ID Token によるバックエンド呼び出しを許可する一方で、Grafana の JWT 認証は subemail といった claim を既存の Grafana User にマッピングし、そのユーザの権限でアクセス可否を判定する仕様になっているためです。この claim が Grafana User に対応付かない限り、Grafana は権限を決定できずリクエストを拒否します。

そのため Cloud Run 側では、IAP を通過した後に Grafana が認識できる JWT を Authorization: Bearer として付与し、claim が事前に作成した Grafana User に一致する状態を作る必要があります。

// UpdateGrafanaAlertRule updates the Pause flag of a Grafana alert rule.
// - If isPaused is true, the alert is disabled.
// - If isPaused is false, the alert is enabled.
func (g *GrafanaClient) UpdateGrafanaAlertRule(isPaused PauseMode) error {
	ctx := context.Background()

	// IAP 認証トークンを取得
	tokenSource, err := idtoken.NewTokenSource(ctx, g.iapClientID)
	if err != nil {
		return fmt.Errorf("failed to get id token source: %w", err)
	}
	idToken, err := tokenSource.Token()
	if err != nil {
		return fmt.Errorf("failed to get id token: %w", err)
	}

	// 省略

	// isPaused フラグを更新
	alertRule["isPaused"] = isPaused
	requestBody, err := json.Marshal(alertRule)
	if err != nil {
		return fmt.Errorf("failed to encode json: %w", err)
	}

	req, err = http.NewRequest("PUT", g.endPoint, bytes.NewBuffer(requestBody))
	if err != nil {
		return fmt.Errorf("failed to create PUT request: %w", err)
	}

	// ここで IAP 認証ヘッダに登録
	req.Header.Set("Authorization", "Bearer "+idToken.AccessToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err = g.client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send PUT request: %w", err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			log.Printf("failed to close response body: %v", err)
		}
	}()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("failed to update alerting rule: %s - %s", resp.Status, string(body))
	}

	return nil
}

Cloud Run の GSA と Grafana User を JWT の claim で紐付けることで Service Account Token を発行することなく、IAP を通過した後も一貫したアイデンティティでアクセスを保証できるため、セキュアで管理しやすいアーキテクチャになります。

  1. Grafana User の追加(実体は GSA 名にする)

    • Name:gke-upgrade-notify-cloudrun
    • Email:gke-upgrade-notify-cloudrun@[PROJECT_ID].iam.gserviceaccount.com
    • Username:gke-upgrade-notify-cloudrun@[PROJECT_ID].iam.gserviceaccount.com
    • Password:password(JWT 認証となるため仮で良い)
  2. Grafana User に対象フォルダへの Editor ロールを付与

  3. リクエストの確認

    • GSA がアラーティングルールに正常にアクセスすると Synced via JWT に表記が変わります。
    • Pause が解除され、アラーティングルールの評価が開始されていれば、GSA と Grafana User が正常に紐付いています。

※ GSA と Grafana User を正しく紐付けられていない場合、JWT claim から Grafana User を特定できず、対象フォルダの Alerting rule を読む権限が無いものとして以下のエラーを返します。

[E] failed to update grafana alert rule: failed to get alerting rule: 403 Forbidden - {\"extra\":{\"permissions\":\"all(all(action:alert.rules:read scopes:folders:uid:XXXXX action:folders:read scopes:folders:uid:XXXXX))\"},\"message\":\"user is not authorized to access rules in folder 'XXXXX'\",\"messageId\":\"alerting.unauthorized\",\"statusCode\":403,\"traceID\":\"\"}",

Grafana User 管理の課題

IAP Token と Grafana Service Account Token は併用することができないため、今回は Grafana User を採用することにしましたが、Grafana の認証機構の制約 により、Grafana User を作成する際には Basic 認証を使用する 必要があります。

You can’t authenticate to the User HTTP API with service account tokens. Service accounts are limited to an organization and an organization role. They can’t be granted Grafana server administrator permissions.

grafana.ini:
  auth.basic:
    enabled: true # Basic 認証を許可する
  auth.jwt:
    enabled: true
    header_name: x-goog-iap-jwt-assertion
    jwk_set_url: https://www.gstatic.com/iap/verify/public_key-jwk
    email_claim: email
    username_claim: email
    auto_sign_up: true

この点で Basic 認証を有効化することがセキュリティリスクとなる懸念がある場合は、素直にコンソールから Grafana User を作成すれば良いですが、Alerting rule(Pause)更新ために付与したフォルダ権限は永続化されない ため、これとは別に Terraform で管理する等、IaC のためには一工夫する必要がありそうです。

resource "grafana_folder_permission" "cloud_platform_permission" {
  folder_uid = grafana_folder.default.uid
  permissions {
    # Grafana User に紐付けた GSA に直接権限を付与する
    user_id    = "gke-upgrade-notify-cloudrun@[GCP_PROJECT_ID].iam.gserviceaccount.com"
    permission = "Edit"
  }
}

まとめ

クラスタアップグレード中に発生する Pod の再起動は Kubernetes としては正常な振る舞いですが、従来の監視ルールでは異常として検知され、アラートの形骸化を招く原因となっていました。この課題に対し、ABEMA では GKE Cluster Notifications によるイベント検知と Grafana Alert API による動的なルール制御を組み合わせる Context-Aware な監視システムを導入しています。

本ブログの事例が、Kubernetes 運用における監視設計の参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?