↓メインコンテンツへスキップ
  1. Blogs/

Alerting入門 第3回|Prometheus Alerting rulesとAlertmanagerで通知を送る

4 分
Kubernetes Prometheus Alertmanager
0222-nnn
著者
0222-nnn
猫が好き
目次

概要
#

第1回・第2回では、人がPromQLを実行して状態を確認するところまでを扱った。条件を満たしたときに自動で通知する仕組みはまだ扱っていない。実際の運用では、条件を式として登録しておき、一定時間満たされ続けたら通知が届く、という流れが必要になる。

この記事では、Prometheusのalerting ruleとAlertmanagerを使って、scrape失敗を検知してから通知が届くまでの流れを実際に動かして確認する。対象読者は、第1回・第2回でPrometheusへのクエリ実行を経験した人。alerting ruleやAlertmanagerに初めて触れる人を想定している。

この記事では、複数のレシーバーを使い分けるような複雑なルーティングは扱わない。Slack・PagerDutyなど外部サービスとの連携も扱わない。認証情報が必要になり、この記事の主題である「pending・firing・通知」の流れから外れるためである。

Grafana Alertingも扱わない。Grafana自体が持つ別の仕組みであり、Prometheus Alertmanagerとは別物である。

今回使う環境
#

第1回・第2回で構築したminikube v1.39.0(Kubernetes v1.37.0)上のmonitoring Namespace(Node Exporter・Prometheus・Grafana)をそのまま使う。Prometheus prom/prometheus:v3.14.0、Node Exporter prom/node-exporter:v1.12.1は版を変えていない。

今回は、本記事の主題であるAlertmanagerを新規に追加する。検証時点でprom/alertmanager:latestを実行し、docker run --rm prom/alertmanager:latest --versionでv0.34.1であることを確認した。再現性のため、Manifestではprom/alertmanager:v0.34.1に固定して使う。

もう1つ、webhook-receiverという小さなPodを追加する。Alertmanagerが実際に通知を送ったことを、Slackなど外部サービスに依存せず確認するためである。python:3.13-alpine上でPOSTを受け取り、受信時刻(UTC)と本文を1行にまとめて標準出力へ出すだけの、20行に満たないスクリプトをConfigMapでマウントしている。Deployment・Service・ConfigMapの3つに留め、認証や永続化は持たない。

Prometheus側にも変更を加える。alerting ruleを読み込むためのrule_filesと、通知先を指定するalerting.alertmanagersをprometheus-configのConfigMapに追加した。alerting ruleの本体は新しいConfigMap(prometheus-rules)に分け、PrometheusのDeploymentにrulesというVolumeとして追加でマウントしている。

flowchart LR
    subgraph monitoring["monitoring Namespace"]
        NE["node-exporter
(DaemonSet)"] PR["Prometheus"] AM["Alertmanager"] WH["webhook-receiver"] NE -- "scrape :9100" --> PR PR -- "firing alert" --> AM AM -- "webhook POST" --> WH end

追加するマニフェストは、記事ディレクトリのsample/にある。記事ディレクトリを作業ディレクトリとして、次の順に適用する。

kubectl apply -f sample/webhook-receiver/
kubectl apply -f sample/alertmanager/
kubectl apply -f sample/prometheus-rules-configmap.yaml \
  -f sample/prometheus-configmap.yaml \
  -f sample/prometheus-deployment.yaml

すべてのPodがRunningになったことを確認してから読み進める。

kubectl get pods -n monitoring
NAME                                READY   STATUS    RESTARTS   AGE
alertmanager-6c98bf598d-wchpf       1/1     Running   0          21s
grafana-7d9d4457b8-lrjhj            1/1     Running   1          24h
node-exporter-nh7g5                 1/1     Running   1          20h
prometheus-79ffd859f6-b4x24         1/1     Running   0          20s
webhook-receiver-767b589958-4njt2   1/1     Running   0          21s

すべてローカルのminikube上で完結し、クラウドの課金は発生しない。今回の構成は、Alerting rulesからAlertmanager・通知までの流れを理解するための学習用である。AlertmanagerのHA構成、状態の永続化、receiverの認証・TLSといった本番運用向けの設計は扱わない。

Alerting ruleを定義する
#

Prometheusの公式ドキュメントでは、alerting ruleの主なフィールドを次のように説明している。

  • alert: アラート名
  • expr: アラート条件を定義するPromQLの式
  • for(省略可): "causes Prometheus to wait for a certain duration between first encountering a new expression output vector element and counting an alert as firing"(条件を満たす要素を最初に検出してから、発火とみなすまで待つ期間)
  • labels / annotations: アラートに付与するラベルと注釈

同じドキュメントでは、条件は満たしているがまだ発火していない状態をpending、発火している状態をfiringと呼んでいる。

今回定義したalerting ruleは、Node Exporterへのscrapeが失敗し続けたことを検知するものである。

groups:
  - name: node-exporter
    rules:
      - alert: NodeExporterDown
        expr: up{job="node-exporter"} == 0
        for: 45s
        labels:
          severity: warning
        annotations:
          summary: "node-exporter target is down"
          description: 'up{job="node-exporter", instance="{{ $labels.instance }}"} has been 0 for more than 45s'

up == 0は、Prometheusがscrape対象として認識しているtargetへのscrapeが失敗したことを検知する式である。今回はstatic_configsで固定したtargetを対象にしているため、この式だけで十分だった。Service Discoveryでtarget自体が検出対象から外れるケース(次回・第4回で扱う)は別の問題であり、up == 0では検知できない。

for: 45sは、pendingからfiringへの遷移を短い待ち時間で観測するために選んだ値である。本番で使う値は、障害検出までの目標時間と、一時的なscrape失敗をどこまで許容するかに応じて決めるものであり、この記事の実測からは導けない。同じ観測目的で、globalのevaluation_intervalも既定の1mから15sに短縮した。

global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - /etc/prometheus/rules/*.yml

このConfigMapを適用すると、Prometheusは起動時にルールを読み込む。kubectl port-forward -n monitoring svc/prometheus 9090:9090で手元に転送したうえで、/api/v1/rulesを確認する。レスポンスはdata.groups[].rules[]の配列なので、jqで対象のルールだけを絞り込む。追加した直後はpendingでもfiringでもなくinactive(条件を満たしていない)になっている。

curl -s http://localhost:9090/api/v1/rules \
  | jq '.data.groups[].rules[]
        | select(.name == "NodeExporterDown")
        | {name, query, duration, state, health}'
{
  "name": "NodeExporterDown",
  "query": "up{job=\"node-exporter\"} == 0",
  "duration": 45,
  "state": "inactive",
  "health": "ok"
}

なお、今回はrule_filesを追加するためにrulesという新しいVolumeをPrometheusのDeploymentへ加えた。第2回ではConfigMapの内容だけを変更した。ConfigMapの変更がマウント先のファイルへ反映されることと、Prometheusがその設定を読み直すことは別である。ファイルへの反映はKubernetesが自動で行う。一方、Prometheus自身に設定を読み直させる操作は別途必要で、kubectl rollout restartもその1つだった。

今回はDeployment自体(Podテンプレート)を変更したため、kubectl applyだけで新しいReplicaSetが作られ、Podが置き換わった。TSDBの保存先は第1回からemptyDirのままなので、Pod置き換えでそれまで蓄積したメトリクスは失われる。以降の確認は、新しいPodが収集したメトリクスを使う。

PrometheusからAlertmanagerへの連携
#

alerting.alertmanagersに、通知先のAlertmanagerを指定する。

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - alertmanager:9093

Prometheus自身の/api/v1/alertmanagersで、実際に検出できているかを確認できる。レスポンスはstatusとdataを持つ構造なので、data部分だけを取り出す。

curl -s http://localhost:9090/api/v1/alertmanagers | jq '.data'
{
  "activeAlertmanagers": [
    { "url": "http://alertmanager:9093/api/v2/alerts" }
  ],
  "droppedAlertmanagers": []
}

activeAlertmanagersに設定したAlertmanagerが表示され、droppedAlertmanagersは空だった。

for句によるpending→firingを観測する
#

up{job="node-exporter"} == 0を実際に発生させるには、Node Exporterへのscrapeを確実に失敗させる必要がある。

最初にNode ExporterのServiceのselectorを実在しないラベルへ書き換えて疎通を切ろうとした。だがupは、書き換えてから1分近く経っても1のままだった(実測)。原因は追跡せず、確実に失敗させる方法に切り替えた。

今回はNode ExporterのDaemonSetのnodeSelectorを、存在しないノード名に一時的に変更した。単一ノードのクラスタなので、これでPodがスケジュールされなくなり、既存のPodも削除される。

kubectl patch daemonset node-exporter -n monitoring \
  -p '{"spec":{"template":{"spec":{"nodeSelector":{"kubernetes.io/hostname":"does-not-exist"}}}}}'

実行するとPodは即座にTerminatingになり、数秒後にはmonitoring Namespaceから消えた。ServiceのEndpointsも空になる。

kubectl get endpoints node-exporter -n monitoring
NAME            ENDPOINTS   AGE
node-exporter   <none>      24h

以降、upと/api/v1/rulesを10秒おきに確認した(時刻はすべてUTC)。

curl -s 'http://localhost:9090/api/v1/query?query=up%7Bjob%3D%22node-exporter%22%7D' \
  | jq '.data.result[] | {metric, value}'

07:35:54の時点で、upはすでに0になっていた。

{
  "metric": {
    "__name__": "up",
    "instance": "node-exporter:9100",
    "job": "node-exporter"
  },
  "value": [1790408154.963, "0"]
}

/api/v1/rulesは、ルール本体だけでなく、実際に発火中のアラート(alerts[])も返す。ここも対象のルールだけに絞り込む。

curl -s http://localhost:9090/api/v1/rules \
  | jq '.data.groups[].rules[]
        | select(.name == "NodeExporterDown")
        | .alerts[]
        | {state, activeAt, labels, value}'
sequenceDiagram
    participant NE as node-exporter
    participant PR as Prometheus
    participant AM as Alertmanager
    participant WH as webhook-receiver

    NE--xPR: scrape失敗(up=0)
    Note over PR: pending開始(activeAt 07:35:53.66)
    Note over PR: 45秒経過してもup=0
    Note over PR: firingへ遷移(startsAt 07:36:38.66)
    PR->>AM: firing alert
    Note over AM: group_wait 10秒待機
    AM->>WH: webhook POST(firing)(07:36:48.67)
    NE->>PR: scrape成功(up=1)
    Note over PR: firing解除(endsAt 07:37:38.66)
    PR->>AM: resolved alert
    AM->>WH: webhook POST(resolved)(07:37:48.67)

07:36:04の時点で、ルールはpendingに遷移していた。

{
  "state": "pending",
  "activeAt": "2026-09-26T07:35:53.662963455Z",
  "labels": {
    "alertname": "NodeExporterDown",
    "instance": "node-exporter:9100",
    "job": "node-exporter",
    "severity": "warning"
  },
  "value": "0e+00"
}

activeAtから45秒強が経過した07:36:46には、firingに変わっていた(同じコマンドを再実行)。

{
  "state": "firing",
  "activeAt": "2026-09-26T07:35:53.662963455Z",
  "labels": {
    "alertname": "NodeExporterDown",
    "instance": "node-exporter:9100",
    "job": "node-exporter",
    "severity": "warning"
  },
  "value": "0e+00"
}

activeAt(07:35:53.66)にforの45秒を足すと07:36:38.66になる。これはAlertmanager側で確認したstartsAt(後述)とミリ秒単位で一致しており、forが「条件を満たし続けた時間」を起点にしていることを裏付けている。ただし今回は10秒おきの確認しか行っておらず、forの途中で条件が一時的に解消した場合の挙動までは確認していない。

alerting ruleは、rule group単位のinterval(既定はglobal.evaluation_interval)ごとに評価される。今回はfor: 45sがevaluation_interval: 15sの3倍だったため、activeAt + 45sがちょうど評価タイミングと重なり、startsAtと完全に一致した。一般には、評価はforの期間経過後の直近の評価タイミングで行われるため、forが評価間隔の倍数でない場合、activeAt + forとstartsAtは完全には一致しない。

Alertmanagerの通知グループ化と、通知が届いたことの確認
#

Alertmanager公式ドキュメントでは、route配下の主なパラメータを次のように説明している。

パラメータ 説明 既定値
group_by どのラベルでアラートをまとめるか 子routeでは親routeから継承(今回のroot routeには継承元が無いため明示的に指定)
group_wait 新しいグループの最初の通知を送るまでの待ち時間 30s
group_interval 既存グループへ後続の通知を送る間隔 5m
repeat_interval 同じ内容の通知を再送するまでの間隔 4h

今回のAlertmanager設定は次の通り。group_waitとgroup_intervalは、観測のために既定値(30s・5m)より短い10s・30sにしている。repeat_intervalも1hに短縮しているが、今回の観測時間内では再通知(同じ内容の繰り返し送信)までは待っておらず、実際に短縮の効果を確認したわけではない。

route:
  receiver: webhook-receiver
  group_by: ['alertname']
  group_wait: 10s
  group_interval: 30s
  repeat_interval: 1h

receivers:
  - name: webhook-receiver
    webhook_configs:
      - url: http://webhook-receiver:8080/
        send_resolved: true

send_resolvedはwebhook宛先の既定値がtrueであり、指定しなくても解消時の通知は届く。今回は「resolved通知まで観測する」ことが記事の主題であるため、意図を明示する目的で明記した。

通知が実際に届いたかどうかは、2段階に分けて確認できる。1段階目は、AlertmanagerがPrometheusからアラートを受け取ったかどうかである。kubectl port-forward -n monitoring svc/alertmanager 9093:9093で転送し、Alertmanager自身の/api/v2/alertsを確認する。レスポンスはalertの配列なので、対象のアラートだけに絞り込む。

curl -s http://localhost:9093/api/v2/alerts \
  | jq '.[]
        | select(.labels.alertname == "NodeExporterDown")
        | {status, startsAt, receivers, labels}'

firingへ遷移した直後の応答は次の通り。

{
  "status": {
    "inhibitedBy": [],
    "mutedBy": [],
    "silencedBy": [],
    "state": "active"
  },
  "startsAt": "2026-09-26T07:36:38.662Z",
  "receivers": [{"name": "webhook-receiver"}],
  "labels": {
    "alertname": "NodeExporterDown",
    "instance": "node-exporter:9100",
    "job": "node-exporter",
    "severity": "warning"
  }
}

2段階目は、Alertmanagerが実際に通知先へPOSTを送ったかどうかである。webhook-receiverは、受け取った時刻(UTC、マイクロ秒精度)とPOSTの本文を1行にまとめて標準出力へ残す。この時刻はwebhook-receiver自身が付与したもので、Kubernetesのログ収集やAlertmanager側の記録とは別の時計である。

kubectl logs -n monitoring deployment/webhook-receiver --tail=1
2026-09-26T07:36:48.668138+00:00 {"receiver":"webhook-receiver","status":"firing","notification_reason":"first notification","groupLabels":{"alertname":"NodeExporterDown"},"alerts":[{"status":"firing","startsAt":"2026-09-26T07:36:38.662Z"}]}

受信時刻(07:36:48.668138)はstartsAt(07:36:38.662)から約10.006秒後である。設定したgroup_wait: 10sとほぼ一致する。

firingの通知を確認できたので、DaemonSetのnodeSelectorを元に戻してNode Exporterを復旧する。

kubectl patch daemonset node-exporter -n monitoring --type=json \
  -p='[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'

Node Exporterへのscrapeが再び成功し、ルールはinactiveに戻った。

Alertmanager公式ドキュメントによると、group_intervalは「group_waitが経過した時点から動き出す繰り返しタイマー」であり、各周期で新規発火・解消の有無を確認し、変化があれば通知する。最初の通知の基準時刻(startsAt + group_wait = 07:36:48.662)からgroup_interval: 30sで並べると、次のタイミングは07:37:18.662・07:37:48.662になる。アラートが解消した時刻(endsAt)は07:37:38.662だった。その次のタイミングである07:37:48.662の直後、07:37:48.668865にresolved通知が届いた。

kubectl logs -n monitoring deployment/webhook-receiver --tail=1
2026-09-26T07:37:48.668865+00:00 {"receiver":"webhook-receiver","status":"resolved","notification_reason":"all alerts resolved","alerts":[{"status":"resolved","endsAt":"2026-09-26T07:37:38.662Z"}]}

元に戻す
#

この節は、前節でnodeSelectorを削除してNode Exporterを復旧済みであることを前提とする。以下の再適用は、第1回のManifestとの差分がないことを確認するために行うものであり、nodeSelectorを削除する手段ではない。

kubectl applyの3-way mergeは、以前のkubectl apply時点の内容(last-applied-configuration)と新しいManifestの差分から削除対象を決める。kubectl patchで後から追加したフィールドはlast-appliedに記録されないため、Manifestを再適用しても消えるとは限らない。

kubectl apply -f ../2026_09_25_prometheus_grafana_minikube_basics/sample/node-exporter/daemonset.yaml

今回追加したAlertmanager・webhook-receiverは、次回以降で使う予定がないため、確認が終わったら削除できる。対象はmonitoring Namespace内のこの2つのDeployment・Service・ConfigMapのみで、Node Exporter・Grafanaには影響しない。

kubectl delete -f sample/alertmanager/ -f sample/webhook-receiver/

Prometheus側は、第2回・第1回で使っていたConfigMap・Deploymentがそのまま「戻すべき完成形」なので、それぞれのサンプルを再適用すれば、手作業での編集なしに第2回終了時点の構成へ戻せる。prometheus-rules ConfigMapは、Deploymentの更新で参照が外れたあとに削除する。

kubectl apply -f ../2026_09_26_prometheus_promql_cheatsheet/sample/prometheus-configmap.yaml
kubectl apply -f ../2026_09_25_prometheus_grafana_minikube_basics/sample/prometheus/deployment.yaml
kubectl rollout status deployment/prometheus -n monitoring --timeout=60s
kubectl delete configmap prometheus-rules -n monitoring

この操作でもPrometheusのDeployment(Podテンプレート)が変わるためPodが置き換わり、TSDB(emptyDir)に蓄積したメトリクスは失われる。Node Exporter・Grafana・DaemonSet・Serviceには変更を加えていない。復旧手順は無く、必要な値は削除前に控えておく。

まとめ
#

alerting ruleはalert・expr・for・labels・annotationsで条件を定義し、forで指定した時間だけ条件を満たし続けるとpendingからfiringに変わる。今回の実測では、activeAtにforの45秒を足した時刻と、Alertmanagerが記録したstartsAtが一致した。

Prometheusはfiringになったアラートをalerting.alertmanagersで指定したAlertmanagerへ送る。Alertmanagerはrouteのgroup_byでアラートをまとめ、group_waitで指定した時間だけ待ってから最初の通知を送る。今回はgroup_wait: 10sに対し、約10秒後の通知を確認できた。

group_intervalはgroup_wait経過後に始まる繰り返しタイマーであり、各周期で新規発火・解消の有無を確認し、変化があれば通知する。今回のresolved通知も、この周期の計算どおりのタイミングで届いた。repeat_intervalは設定したが、今回の観測時間内では再通知までは確認していない。

通知が実際に届いたことは、外部サービスを使わなくても2段階で確認できる。AlertmanagerがPrometheusからアラートを受け取ったことは自身の/api/v2/alertsで、実際に通知先へPOSTが届いたことは自前で用意したwebhook受信側のログで確認する。

次回(第4回)は、Kubernetes Service Discoveryとkube-state-metricsを扱う。今回はNode Exporter 1台をstatic_configsで直接指定したが、Podの増減に自動で追従する仕組みはまだ扱っていない。

参考資料
#

関連記事

PromQL入門 第2回|チートシート形式で早引きするrate・increase・topk・histogram_quantile
6 分
Kubernetes Prometheus PromQL
Prometheus・Grafana入門 第1回|minikubeでメトリクス収集からダッシュボード作成まで
8 分
Kubernetes Prometheus Grafana Minikube
GKEでPodの退避を起こしたら、監視に必要なメトリクスがCloud Monitoringに来なかった
8 分
Terraform GoogleCloud GKE Kubernetes CloudLogging CloudMonitoring
TerraformでGKE Standard ClusterとSpot Node Poolを作成してみた
3 分
Terraform GoogleCloud Kubernetes
nginx WebサーバーでのDocker Live Restore検証
8 分
Docker Kubernetes
Kubernetes postStartフックの終了コード検証:exit 0とexit 1の実際の挙動を検証してみた
2 分
Kubernetes Minikube