K8s 実戦 #5 モニタリング・アラーム — Prometheus / CloudWatch / Alertmanager

K8s 実戦シリーズの 5 番目の記事です。#4 まで経て myshop-api は新しいバージョンが入ってくる流れまで自動化されましたが、運用段階の半分は その動作を観測すること です。CPU・メモリ・リクエスト latency・エラー率がどこでどう変わるかが見えなければ、カナリー自動 promote も不可能で事故対応も遅れます。この記事はオブザーバビリティスタックを EKS の上に載せる流れです。上級 #5 で扱った標準スタック(Prometheus + Grafana + Loki + Alertmanager)を EKS 環境に合わせて具体化し、AWS 管理型オプションである CloudWatch Container Insights との結合も一緒に見ます。

このシリーズは K8s 実戦 6 編です。

2 軸の結合 — in-cluster Prometheus + 管理型 CloudWatch #

EKS 環境のオブザーバビリティは通常 2 軸の結合で行われます。

責務
In-cluster (Prometheus + Grafana + Loki)ワークロードメトリクス、ビジネスメトリクス、アラーム、ダッシュボード
CloudWatch (Container Insights + Logs)AWS 管理型メトリクス、ログ長期保管、AWS コンソール統合

どちらか一方だけ使う方式も可能ですが、運用クラスタの標準は両方の結合です。Prometheus が運用メトリクスとアラームの source of truth であり、CloudWatch が長期保管と AWS 自体のリソース(RDS、ALB、EBS)メトリクスの統合ポイントとなります。AWS の管理型 Prometheus(AMP)と管理型 Grafana(AMG)が in-cluster 運用負担を減らすオプションとして位置づけられつつありますが、この記事ではもっとも一般的な in-cluster モデルを中心に見ます。

kube-prometheus-stack — 一度に入る標準の束 #

上級 #5 で押さえた標準 Helm chart です。1 つのコマンドに Prometheus + Grafana + Alertmanager + kube-state-metrics + node-exporter + Prometheus Operator の CRD がすべて入ってきます。

インストール #

kube-prometheus-stack インストール
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace \
  --values prometheus-values.yaml
prometheus-values.yaml — 核心部分
prometheus:
  prometheusSpec:
    retention: 30d
    retentionSize: "50GB"

    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 100Gi

    serviceMonitorSelectorNilUsesHelmValues: false
    podMonitorSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false

    additionalScrapeConfigs:
      - job_name: ec2-spot-instance
        ec2_sd_configs:
          - region: ap-northeast-2

    remoteWrite:
      - url: https://aps-workspaces.ap-northeast-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write
        sigv4:
          region: ap-northeast-2

grafana:
  adminPassword: ""
  ingress:
    enabled: true
    ingressClassName: alb
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
    hosts:
      - grafana.myshop.example.com
  persistence:
    enabled: true
    storageClassName: gp3
    size: 10Gi

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          resources:
            requests:
              storage: 10Gi
  config:
    route:
      receiver: default
    receivers:
      - name: default

主要設定をいくつか押さえます。

  • retention: 30d + storageSpec — メトリクス 30 日保管 + EBS 100GB。30 日以上保管するには remoteWrite で AMP や Thanos に一緒に送る。
  • serviceMonitorSelectorNilUsesHelmValues: false — すべての namespace の ServiceMonitor を自動認識。myshop namespace の ServiceMonitor が monitoring namespace なしでも動作。
  • remoteWrite — AWS Managed Prometheus(AMP)にメトリクス long-term 保存。30 日以上の分析が必要なケース。

インストール直後の点検 #

基本ヘルスチェック
kubectl get pods -n monitoring
kubectl get servicemonitors -A
kubectl get prometheusrules -A
期待出力
NAME                                                 READY   STATUS    RESTARTS
prometheus-grafana-xxx                               3/3     Running   0
prometheus-kube-prometheus-operator-xxx              1/1     Running   0
prometheus-kube-state-metrics-xxx                    1/1     Running   0
prometheus-prometheus-kube-prometheus-prometheus-0   2/2     Running   0
prometheus-prometheus-node-exporter-xxx              1/1     Running   0
alertmanager-prometheus-kube-prometheus-alertmanager-0  2/2  Running   0

基本 PrometheusRule が約 100 個自動で入ってきます。ノードダウン、etcd 障害、kubelet 問題のような K8s 自体のアラームがその中に事前定義されているので、クラスタ事故は別途作業なしですぐにアラームになります。

myshop-api にメトリクス公開を追加 #

標準スタックがインストールされたクラスタに myshop-api のメトリクスを公開する 1 サイクルです。

1. アプリケーションが /metrics を公開 #

Prometheus クライアントライブラリがほぼすべての言語にあります。Python(FastAPI)なら次の 1 行で始めます。

myshop-api/main.py — Prometheus メトリクス公開
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()
Instrumentator().instrument(app).expose(app)

この 1 行が次のメトリクスを自動で公開します。

  • http_requests_total{handler, method, status} — リクエストカウンター
  • http_request_duration_seconds_bucket{handler, method} — latency ヒストグラム
  • http_request_size_bytes / http_response_size_bytes — ペイロードサイズ
  • 標準 Python runtime メトリクス(GC、threads、memory)

ドメインメトリクス(例: 注文生成カウンター、決済成功率)はその上に追加します。

ドメインメトリクス追加
from prometheus_client import Counter, Histogram

orders_created = Counter(
    "myshop_orders_created_total",
    "Total orders created",
    ["status"]
)

checkout_duration = Histogram(
    "myshop_checkout_duration_seconds",
    "Checkout flow duration"
)

2. ServiceMonitor マニフェスト #

Prometheus Operator が watch する ServiceMonitor を作ります。

charts/myshop-api/templates/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: {{ include "myshop-api.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    app.kubernetes.io/name: myshop-api
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: myshop-api
  endpoints:
    - port: http
      interval: 30s
      path: /metrics

このマニフェストが適用された瞬間から Prometheus が 30 秒ごとに myshop-api のすべての Pod の /metrics を集め始めます。Grafana の Explore で http_requests_total{namespace="myshop"} でデータが見えればメトリクス収集が正常です。

4 golden signals — アラームルールセットの骨格 #

上級 #5 で扱った 4 golden signals(Latency / Traffic / Errors / Saturation)を myshop-api の PrometheusRule で書きます。

charts/myshop-api/templates/prometheusrule.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: {{ include "myshop-api.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    release: prometheus
spec:
  groups:
    - name: myshop-api.golden-signals
      interval: 30s
      rules:
        # Errors — 5xx 比率
        - alert: MyshopApiHighErrorRate
          expr: |
            sum(rate(http_requests_total{app="myshop-api",status=~"5.."}[5m]))
              / sum(rate(http_requests_total{app="myshop-api"}[5m])) > 0.05
          for: 5m
          labels:
            severity: critical
            team: backend
          annotations:
            summary: "myshop-api 5xx rate > 5% ({{ "{{ $value | humanizePercentage }}" }})"
            description: "5xx 比率が 5% を超えた状態が 5 分以上維持された。"
            runbook_url: "https://runbooks.myshop.example.com/myshop-api-5xx"

        # Latency — P95
        - alert: MyshopApiHighLatencyP95
          expr: |
            histogram_quantile(0.95,
              sum by (le) (rate(http_request_duration_seconds_bucket{app="myshop-api"}[5m]))
            ) > 1.0
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api P95 latency > 1s ({{ "{{ $value | printf \"%.2f\" }}" }}s)"

        # Traffic — トラフィック急減 (downstream 障害シグナル)
        - alert: MyshopApiTrafficDrop
          expr: |
            sum(rate(http_requests_total{app="myshop-api"}[5m]))
              < 0.3 * sum(rate(http_requests_total{app="myshop-api"}[5m] offset 1h))
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api トラフィック急減 (過去 1 時間比 30% 未満)"

        # Saturation — Pod メモリ使用率
        - alert: MyshopApiPodMemoryHigh
          expr: |
            sum by (pod) (
              container_memory_working_set_bytes{namespace="myshop",pod=~"myshop-api-.*"}
            ) / sum by (pod) (
              kube_pod_container_resource_limits{namespace="myshop",pod=~"myshop-api-.*",resource="memory"}
            ) > 0.85
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api Pod memory > 85% of limit"

各ルールの重要なパターンを 3 つ押さえます。

  • for 期間 — 短いスパイクにアラームが鳴らないように 5~10 分の持続時間要求。
  • severity ラベルcritical は即時呼び出し、warning は次の営業日に検討。Alertmanager ルーティングのキー。
  • runbook_url — アラームを受けた人が即時に追える対応手順ドキュメントです。アラーム 1 件 = 明確な対応 1 つという原則に従います。

Alertmanager ルーティング — Slack と PagerDuty の分岐 #

アラームの流れは Prometheus → Alertmanager → チャンネルです。Alertmanager がラベルを見てルーティングを決定します。

alertmanager.yaml — 運用ルーティング
route:
  receiver: default
  group_by: ['alertname', 'team', 'namespace']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
    - matchers:
        - severity = "critical"
      receiver: pagerduty-backend
      continue: true
      routes:
        - matchers:
            - severity = "critical"
            - team = "backend"
          receiver: pagerduty-backend
        - matchers:
            - severity = "critical"
            - team = "platform"
          receiver: pagerduty-platform

    - matchers:
        - severity = "warning"
      receiver: slack-warnings
      group_wait: 1m
      repeat_interval: 12h

receivers:
  - name: default
    slack_configs:
      - api_url: ${SLACK_WEBHOOK}
        channel: '#alerts'

  - name: slack-warnings
    slack_configs:
      - api_url: ${SLACK_WEBHOOK}
        channel: '#alerts-warning'
        title: '⚠️  {{ "{{ .GroupLabels.alertname }}" }}'

  - name: pagerduty-backend
    pagerduty_configs:
      - service_key: ${PAGERDUTY_BACKEND_KEY}

  - name: pagerduty-platform
    pagerduty_configs:
      - service_key: ${PAGERDUTY_PLATFORM_KEY}

inhibit_rules:
  - source_matchers: [severity = "critical"]
    target_matchers: [severity = "warning"]
    equal: [alertname, namespace]

主なパターンが 3 つあります。

  • severity 別分岐 — critical は PagerDuty で呼び出し、warning は Slack で通知。
  • team 別分岐 — 同じ critical でも backend / platform チームを別々に呼び出し。
  • inhibit_rules — 同じ alertname の critical が鳴っている間は同じ namespace の warning を束ねて無音処理。アラーム氾濫防止。

秘密(SLACK_WEBHOOKPAGERDUTY_*)は #3 で扱った External Secrets で注入します。

Loki — ログスタック追加 #

メトリクス以外にログも一緒に確保することが標準です。上級 #5 で見た Loki スタックをそのまま適用します。

Loki + Promtail Helm インストール
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  -n monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.storageClassName=gp3 \
  --set loki.persistence.size=100Gi

インストール後 Grafana に Loki データソースが自動で追加され、Explore で LogQL クエリが可能になります。

myshop-api の ERROR ログ
{namespace="myshop", app="myshop-api"} |= "ERROR"
エラー率をメトリクスに換算 (Loki → メトリクスのように)
sum(rate({namespace="myshop", app="myshop-api"} |= "ERROR" [5m]))

長期保管を S3 に置くには Loki の storage backend を S3 に設定します。標準運用セットアップです。

CloudWatch Container Insights — 2 つ目の軸 #

同じ EKS クラスタに CloudWatch Container Insights を一緒にインストールすれば AWS コンソールでクラスタ・ノード・Pod・コンテナメトリクスを即座に確認できます。運用チームが AWS コンソールに慣れていれば日常点検負担が減ります。

CloudWatch Container Insights — Helm
helm repo add aws-observability https://aws-observability.github.io/helm-charts
helm install amazon-cloudwatch-observability \
  aws-observability/amazon-cloudwatch-observability \
  -n amazon-cloudwatch --create-namespace \
  --set clusterName=myshop-prod \
  --set region=ap-northeast-2

この chart が Fluent Bit を DaemonSet として立てて各ノードの stdout/stderr を CloudWatch Logs に送り、CloudWatch Agent でメトリクスも一緒に収集します。

Fluent Bit の役割 #

Fluent Bit がノードの /var/log/containers/ を読んで次の 2 か所にルーティングするのが標準です。

Fluent Bit の 2 つの出口
コンテナログ
   ├─→ Loki (in-cluster、短期検索)
   └─→ CloudWatch Logs (S3 export、長期保管)

同じログを 2 か所に送る理由は責務が違うからです — Loki は日常デバッグ、CloudWatch はコンプライアンス・監査・長期分析。コスト面では Loki だけ使う方式がより軽いですが、規制環境では CloudWatch が通常一緒に入ります。

Grafana ダッシュボード標準 #

運用クラスタの Grafana に入る標準ダッシュボードセットを整理します。

ダッシュボードsource
Kubernetes / Compute Resources / Clusterkube-prometheus-stack 標準 (ID 7249)
Kubernetes / Compute Resources / Namespace (Workloads)標準 (ID 7250)
Kubernetes / Compute Resources / Pod標準 (ID 7251)
Kubernetes / Networking / Cluster標準 (ID 7253)
Node Exporter / Nodes標準 (ID 1860)
myshop-api 運用ダッシュボード自作 — golden signals + ビジネスメトリクス

標準ダッシュボード 5 つは kube-prometheus-stack が自動で登録してくれるので別途作業なしですぐに見えます。自作ダッシュボード 1 つだけドメインに合わせて作れば日常点検の視野がほぼ完成します。

自作ダッシュボードの標準パネルセット #

myshop-api ダッシュボード — 9 つのパネル
Row 1: Latency P50 / P95 / P99
Row 2: Request rate (ドメイン別、status 別)
Row 3: Error rate (4xx / 5xx)
Row 4: Pod CPU / メモリ使用率
Row 5: HPA 現在 replicas
Row 6: PgBouncer 活性接続 / 待機キュー
Row 7: ビジネスメトリクス (orders/min、checkout success rate)
Row 8: 上位 ERROR ログ (Loki)
Row 9: 最近のデプロイ (annotations)

最後のパネルの「最近のデプロイ annotations」は Grafana に ArgoCD または GitHub Actions イベントを annotation として注入したものです。メトリクスグラフの上にデプロイ時点が縦線で表示されて、「この latency スパイクはどのデプロイ直後に発生したか」を一目で見られます。

on-call 流れ — runbook と一緒に #

アラームが鳴ること自体が終わりではありません。受けた人が 5 分以内にどこを見るべきかが明確でなければなりません。次が運用の標準的な流れです。

on-call アラームを受けた直後の標準 5 分
1. PagerDuty でアラーム本文を確認 (alertname、team、severity)
2. annotation の runbook_url をクリック
3. Runbook の「1 次点検」セクションを追う — 関連 Grafana ダッシュボード / ログクエリ / kubectl コマンド提示
4. 1 次対応 (スケールアップ、再起動、トラフィック遮断など)
5. Slack 事故チャンネルに状態共有

Runbook は別途の git repo の markdown で管理するのが標準です。アラームルールの runbook_url がその repo の 1 ページを指し、新しいアラームを追加するときに Runbook も一緒に PR で入ってきます。

最初の運用 1 サイクル後の点検 #

スタックをインストールして数日回した時点で点検する項目です。

Prometheus の時系列数 (カーディナリティ点検)
kubectl exec -n monitoring prometheus-prometheus-kube-prometheus-prometheus-0 -c prometheus -- \
  promtool tsdb analyze /prometheus/wal | head -50
アラームの発生頻度 (一度も鳴らないルール / 多すぎるルール)
kubectl exec -n monitoring alertmanager-prometheus-kube-prometheus-alertmanager-0 -c alertmanager -- \
  amtool alert query --alertmanager.url=http://localhost:9093
Loki のディスク使用量
kubectl exec -n monitoring loki-0 -- df -h /data

運用 1 か月が過ぎるとカーディナリティ急増、アラーム SNR 低下、ログディスク圧迫が通常一度ずつ来ます。この点検を定期点検として置くのが標準です。

締めくくり #

myshop-api の上にオブザーバビリティスタックを載せる 1 サイクルを追いました。kube-prometheus-stack で Prometheus + Grafana + Alertmanager を一度にインストールし、ServiceMonitor + PrometheusRule で myshop-api の 4 golden signals アラームを標準化し、Alertmanager ルーティングで severity・team 別 Slack / PagerDuty 分岐まで押さえました。Loki と CloudWatch の 2 軸を一緒に置き、runbook_url でアラームと対応手順を組み合わせる運用パターンまで押さえました。この時点で myshop-api はコードからデプロイ・運用・観測まで 1 サイクルがすべて自動化された状態です。次の記事でありシリーズの最後の記事ではこのクラスタを 1 か月、四半期、1 年単位で安全に回す定期運用サイクルを扱います — EKS アップグレード、RDS バックアップ・リカバリ、コスト点検、セキュリティ点検のチェックリストと K8s 実戦 6 編の振り返りまでが最後の記事の範囲です。

X