新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第13回です。ここまでで、クラスタもアプリも動くようになりました。HA で構築し(第1部)、ワークロードを統制し(第2部)、外部へ公開し(第3部)、データを分散ストレージへ載せました(第4部)。では「いま元気に動いているか」は、どうやって知るのか。kubectl get pods を一日中眺めるわけにはいきません。本回から第5部「監視・運用」に入り、メトリクスは Prometheus + Grafana、ログは Loki + Fluent Bit で集めて、fanclub-api の健康状態をダッシュボードで見えるようにします。CKA でも D5(30%)の土台になる領域です。
操作は作業端末 k8s-ops(192.168.1.122)から kubectl/helm で行います。プロンプトの $ は一般ユーザー developer、第4回で配布した admin.conf(cluster-admin)を使います。本回で作る YAML は developer のホーム配下の ~/monitoring にまとめます(所有者は developer・sudo は使いません)。以降の kubectl apply はこのディレクトリにいる前提の相対パスです(chart を編集するときだけ ~ へ移動します)。
実行コマンド(k8s-ops・developer):
$ mkdir -p ~/monitoring && cd ~/monitoring
目次
今ここマップ(全 16 回中の現在地)
本シリーズは 6 部構成です。現在地は第5部「監視・運用」の第13回です。
- 第1部 クラスタ構築(第1〜5回)
- 第2部 ワークロード管理(第6〜8回)
- 第3部 ネットワーク(第9〜11回)
- 第4部 ストレージ(第12回)
- 第5部 監視・運用(第13〜14回)← 今ここ
- 第6部 トラブルシュート(第15〜16回)
- Kubernetes v1.36.2
- kubectl v1.35.6(作業端末)
- kube-prometheus-stack chart 84.5.0
- Loki chart 7.1.0
- Fluent Bit chart 2.6.0(3 つとも実機で確定)
- Traefik 39.0.9(第11回)
- Longhorn v1.11.1(第12回)
- Helm v4.1.4
- AlmaLinux 10.2
- 確認日 2026-07-17
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- kube-prometheus-stack(Prometheus + Grafana + Alertmanager)を Helm で導入できる
- ServiceMonitor で自作アプリをスクレイプ対象に追加できる(ラベルの要件を含む)
- Grafana でレイテンシ・エラーレートのダッシュボードを作れる
- Loki + Fluent Bit でクラスタ全体のログを集約し、Grafana で検索できる
監視スタックの全体像
監視には性格の違う 2 系統があります。混ぜて考えると設計を間違えるので、最初に分けておきます。
- メトリクス(数値の時系列):「CPU 使用率」「1 分あたりのリクエスト数」のように定期的に測った数値。Prometheus が各所から集めて保存し、Grafana がグラフにし、Alertmanager がしきい値超えを通知します。傾向と異常の検知が得意です。
- ログ(テキストのイベント):「いつ・何が起きたか」の記録。Fluent Bit が各ノードで拾って Loki へ送り、Grafana から検索します。原因の特定が得意です。
実務では「メトリクスで異常に気づき、ログで原因を特定する」と両輪で使います。だから Grafana は 1 つで両方を見られるようになっています。

kube-prometheus-stack を導入する
導入の前に、第8回で入れた metrics-server との違いをはっきりさせます。あのとき kubectl top nodes で CPU が見えたのに、なぜまた別のものを入れるのか——これは当然の疑問です。
- metrics-server:「今の値」だけを返す軽量な API。
kubectl topと HPA の判断材料のために存在します。履歴を保存しません——「1 時間前どうだったか」は答えられません。 - Prometheus:値を時系列で保存し、PromQL で「5 分間の平均」「95 パーセンタイル」を問い合わせでき、アラートも出せます。
つまり metrics-server は「体温計」、Prometheus は「カルテ」です。両方あって困りません(HPA は今も metrics-server を見ています)。
kube-prometheus-stack は、Prometheus・Grafana・Alertmanager・kube-state-metrics・node-exporter をまとめて入れてくれる chart です。その前に alma-proxy の whitelist に、本回で使う 2 つの chart 配信元を足します。prometheus-community.github.io(kube-prometheus-stack)と grafana.github.io(後半の Loki・Fluent Bit)です。第9回の MetalLB・第12回の Longhorn と同じで、忘れると helm repo add が Forbidden で弾かれます。
実行コマンド(alma-proxy・root):
# for d in prometheus-community.github.io grafana.github.io; do echo "$d" >> /etc/squid/whitelist.txt; done
# systemctl reload squid
k8s-ops に戻って導入します。
実行コマンド(k8s-ops・developer):
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm repo update
$ helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--version 84.5.0 \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=5Gi \
--set grafana.persistence.enabled=true \
--set grafana.persistence.size=2Gi
実行結果(抜粋):
NAME: kube-prometheus-stack
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
リリース名を kube-prometheus-stack にしているのには理由があります。この後 ServiceMonitor に付ける release: kube-prometheus-stack というラベルと一致させる必要があるためです(後述)。
そして --set の 3 行は永続化の指定です。この chart は既定でストレージを要求しません——つまり emptyDir(Pod が消えたら一緒に消える領域)にデータを書きます。監視は「あとから振り返る」ためのものなので、Pod の再作成でメトリクスが消えては困ります。そこで Prometheus に 5Gi、Grafana に 2Gi を要求させます。StorageClass を指定していないことに注目してください——第12回で既定を longhorn にしたので、何も書かなければ Longhorn に載ります。
サイズを控えめにしているのには理由があります。Longhorn が割り当てられる容量には上限があるからです。本環境の Workload Node のディスクは約 34Gi で、Longhorn は「最低 25% は空けておく」設定(storage-minimal-available-percentage)が既定なので、実際に配れるのは約 25Gi。ここに fanclub-db(1Gi)と、この後入れる Loki の分も同居させます。大きく要求しすぎると、後から入れるボリュームが作れなくなります(本番では要求量に見合うディスクを用意します)。Pod が出そろうまで数分かかります。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n monitoring
実行結果(全 10 Pod):
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running 0 3m30s
kube-prometheus-stack-grafana-555f6785c9-rwldr 3/3 Running 0 3m45s
kube-prometheus-stack-kube-state-metrics-857ccb48f8-lnndh 1/1 Running 0 3m45s
kube-prometheus-stack-operator-8568d847d5-2whhk 1/1 Running 0 3m45s
kube-prometheus-stack-prometheus-node-exporter-2pdm2 1/1 Running 0 3m45s
kube-prometheus-stack-prometheus-node-exporter-9vszb 1/1 Running 0 3m45s
kube-prometheus-stack-prometheus-node-exporter-hbvw8 1/1 Running 0 3m45s
kube-prometheus-stack-prometheus-node-exporter-j2zw6 1/1 Running 0 3m45s
kube-prometheus-stack-prometheus-node-exporter-txh5b 1/1 Running 0 3m45s
prometheus-kube-prometheus-stack-prometheus-0 2/2 Running 0 3m30s
役割はこうです。Prometheus が収集と保存、Grafana が可視化、Alertmanager が通知、kube-state-metrics が「Pod の数・再起動回数」などKubernetes の状態をメトリクス化、node-exporter が各ノードの OS の状態(CPU・メモリ・ディスク)を出します。operator は、この後使う ServiceMonitor のようなリソースを見て Prometheus の設定を書き換える係です。
node-exporter が 5 つあることに注目してください。DaemonSet で全ノードに 1 つずつ——Control Plane にも載っています。第7回で見たとおり CP には NoSchedule の taint があり、Longhorn(第12回)は載りませんでした。しかし node-exporter はその taint を許容する設定で配られます。当然です——CP ノードの CPU やディスクが埋まったらクラスタ全体が止まるのですから、監視しないわけにはいきません。「アプリは載せないが、監視は載せる」という使い分けが、DaemonSet の toleration で表現されています。
Grafana を開きます。外部公開はしません——192.168.1.200 は第11回で Traefik が使っており、外部公開は「Gateway 経由でアプリを出す」に統一したためです。管理用の UI は port-forward で覗くのが本巻の流儀です(第12回の Longhorn UI と同じ)。まず初期パスワードを Secret から取り出します。
実行コマンド(k8s-ops・developer):
$ kubectl get secret kube-prometheus-stack-grafana -n monitoring -o jsonpath='{.data.admin-password}' | base64 -d; echo
$ kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana --address 0.0.0.0 3000:80
手元のブラウザで http://192.168.1.122:3000 を開き、ユーザー admin と取り出したパスワードでログインします(--address 0.0.0.0 は k8s-ops 以外のマシンから見るために付けています)。Dashboards には Kubernetes 用のダッシュボードが最初から入っており、ノードや Pod のリソースがすぐ見えます。
port-forward は動かしっぱなしにします(Ctrl+C で止まり、ブラウザも切れます)。本回はこの後 Prometheus の画面も開くので、SSH のセッションをもう 1 つ開いて作業すると楽です。以降のコマンドは、この Grafana の port-forward を止めずに、別のシェルで実行してください。まず永続化を確認します。
実行コマンド(k8s-ops・developer・別のシェルで):
$ kubectl get pvc -n monitoring
実行結果(長いので折り返しています。2 つとも longhorn で Bound):
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
kube-prometheus-stack-grafana Bound pvc-ee66b9a3-974c-43e8-986d-3f9c40a6bf0d 2Gi RWO longhorn 66s
prometheus-kube-prometheus-stack-...-db-0 Bound pvc-8ca6ba3a-fac0-47e2-9978-599c887aa730 5Gi RWO longhorn 64s
StorageClass が longhorn になっています。第12回で既定を付け替えたので、監視データも自動的に分散ストレージへ載る——前回の作業がそのまま効いている場面です。
fanclub-api をスクレイプ対象にする(ServiceMonitor)
ここからが本回の中心です。Prometheus に自作アプリのメトリクスを集めさせます。まず、backend が何を出しているのかを実際に見ます。fanclub は第10回の default-deny 下にあるので使い捨ての Pod からは叩けません。すでに fanclub 名前空間で動いている frontend(Nginx・wget が入っている)から叩きます。
実行コマンド(k8s-ops・developer):
$ FE=$(kubectl get pod -n fanclub -l app=fanclub-frontend -o jsonpath='{.items[0].metadata.name}')
$ IP=$(kubectl get pod -n fanclub -l app=fanclub-backend -o jsonpath='{.items[0].status.podIP}')
$ kubectl exec $FE -n fanclub -- sh -c "wget -q -O - http://$IP:8080/metrics" | head -8
実行結果(抜粋):
# TYPE memory_usedHeap_bytes gauge
# HELP memory_usedHeap_bytes Displays the amount of used heap memory in bytes.
memory_usedHeap_bytes{mp_scope="base"} 9.4044888E7
# TYPE thread_count gauge
thread_count{mp_scope="base"} 65.0
# TYPE gc_total counter
gc_total{mp_scope="base",name="Copy"} 68.0
system_cpu_load{mp_scope="vendor"} 0.052921666666666665
# TYPE や # HELP が付いた Prometheus 形式で出ています。Payara Micro の MicroProfile Metrics の機能です。中身は JVM とシステムの状態(ヒープ・スレッド数・GC 回数・CPU 負荷)で、全部で 18 種類ほどあります。
ここで大事な事実を押さえてください。この中に「HTTP リクエスト数」や「レイテンシ」はありません。MicroProfile Metrics は 5.x で REST の自動計測を標準から外したため、アプリ側にコードを足さない限り出てこないのです。本巻はアプリのイメージを第1巻から引き継いだまま使う方針なので、ここでイメージを作り直すことはしません。ではレイテンシとエラーレートをどこから取るのか——第11回で入れた Traefik です(次の節で扱います)。アプリの内部は backend から、入口の通信は Traefik から取る、と考えてください。
では、この /metrics を Prometheus に拾わせます。使うのが ServiceMonitor です。「この Service の、このポートの、このパスを、この間隔で取りに行け」と宣言すると、Operator が Prometheus の設定に反映してくれます。ところが、ここに継承 chart の落とし穴があります。まず Service を見てください。
実行コマンド(k8s-ops・developer):
$ kubectl get svc fanclub-backend -n fanclub -o jsonpath='{.metadata.labels}'; echo
$ kubectl get svc fanclub-backend -n fanclub -o jsonpath='{.spec.selector}'; echo
実行結果(上が Service 自身の名札・下が Pod を選ぶ条件):
{"app.kubernetes.io/managed-by":"Helm"}
{"app":"fanclub-backend"}
この 2 つは別物です。ここが本回で一番間違えやすいところなので、はっきり分けます。
spec.selector:Service が Pod を選ぶ条件。app: fanclub-backendが入っており、これで backend Pod に転送しています(第9回で見た仕組み)。metadata.labels:Service 自身に貼られた名札。ServiceMonitor が見るのはこちら。実機ではapp.kubernetes.io/managed-by: Helmしかありません。
つまり ServiceMonitor に app: fanclub-backend を探させても、Service には その名札が無いので見つかりません(Pod にはあるのに、です)。継承 chart はこの名札を付けていないので、chart を直します——第12回で storageClassName を足したのと同じ、chart を育てる作業です。~/fanclub-chart/templates/backend-service.yaml を vi などで開き、metadata に labels の 2 行を足します。
apiVersion: v1
kind: Service
metadata:
name: fanclub-backend
namespace: {{ .Values.namespace }}
labels:
app: fanclub-backend # 追加:第13回の ServiceMonitor が選ぶ名札
spec:
selector:
app: fanclub-backend
ports:
- name: http
port: 8080
targetPort: 8080
ポートに name: http が付いていることも確認しておいてください。ServiceMonitor はポートを名前で指定します。helm upgrade で反映します。--set を毎回付けるのを忘れないでください——省くと第11回で有効化した Gateway が無効になり、https://fanclub.local が落ちます。
実行コマンド(k8s-ops・developer):
$ cd ~
$ helm upgrade fanclub ./fanclub-chart \
--namespace fanclub \
--set gateway.enabled=true \
--set frontend.image.tag=1.0.0
$ kubectl get svc fanclub-backend -n fanclub -o jsonpath='{.metadata.labels}'; echo
実行結果(名札が付いた):
{"app":"fanclub-backend","app.kubernetes.io/managed-by":"Helm"}
ServiceMonitor を作ります。helm upgrade のために ~ へ移動していたので、作業ディレクトリに戻ってから、次の内容を vi などのエディタで ~/monitoring/servicemonitor-fanclub.yaml として作成します(所有者は developer・sudo は不要です)。
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: fanclub-backend
namespace: fanclub
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: fanclub-backend
endpoints:
- port: http
path: /metrics
interval: 15s
ラベルが 2 か所に出てくるので整理します。metadata.labels の release: kube-prometheus-stack は「この ServiceMonitor を拾ってよい Prometheus はどれか」を示す名札で、Helm のリリース名と一致させます(既定でこの条件になっています)。spec.selector.matchLabels の app: fanclub-backend が、いま Service に付けた名札を指します。
実行コマンド(k8s-ops・developer):
$ cd ~/monitoring
$ kubectl apply -f servicemonitor-fanclub.yaml
$ kubectl get servicemonitor -n fanclub
実行結果:
servicemonitor.monitoring.coreos.com/fanclub-backend created
NAME AGE
fanclub-backend 5s
Prometheus が拾ったかを確認します。Prometheus の画面も port-forward で開きます(Grafana の port-forward は動かしたまま、さらに別のシェルで実行してください。ポート番号が違うので同時に動かせます)。
実行コマンド(k8s-ops・developer・3 つ目のシェル):
$ kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus --address 0.0.0.0 9090:9090
ブラウザで http://192.168.1.122:9090 を開き、Status → Target health を見ます。反映まで 30 秒ほどかかります。serviceMonitor/fanclub/fanclub-backend/0 が現れるはずです——ところが DOWN になっています。エラーはこう出ます。
Get "http://10.244.215.194:8080/metrics": context deadline exceeded
「対象は見つけたが、繋がらない」という状態です。心当たりがあるはずです——第10回の default-deny です。fanclub 名前空間は「明示的に許可した通信だけ」を通す設計で、backend への Ingress は frontend からの 8080 番しか開けていません。Prometheus は monitoring 名前空間の住人なので、当然弾かれます。
ゼロトラストの下では、監視の通信も「明示的に許可する」必要がある——これが本回で一番実務的な学びです。第11回で Traefik からの通信を allow-frontend-from-gateway で通したのと同じ要領で、monitoring 名前空間から backend への 8080 番を開けます。~/monitoring/allow-scrape.yaml を vi などで作成します。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-from-monitoring
namespace: fanclub
spec:
podSelector:
matchLabels:
app: fanclub-backend
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- {protocol: TCP, port: 8080}
実行コマンド(k8s-ops・developer):
$ kubectl apply -f allow-scrape.yaml
実行結果:
networkpolicy.networking.k8s.io/allow-backend-from-monitoring created
30 秒ほど待ってから Prometheus の Target health を再読み込みすると、UP に変わります。もし UP にならないときは、Service の metadata.labels(さっき足した名札)・ServiceMonitor の release ラベル・この NetworkPolicy の 3 つを順に見直してください——「対象が見つからない(Targets に現れない)」のはラベルの問題、「見つかるが DOWN」は通信の問題、と切り分けられます。この切り分けは第16回のトラブルシュートでも効いてきます。

Grafana でダッシュボードを作る
レイテンシとエラーレートを Traefik から取ります。Traefik は既定で Prometheus 形式のメトリクスを 9100 番に出しています(第11回で入れたときから、黙って出していました)。ただし注意点があります——リクエストが 1 度も通っていないと、リクエスト系のメトリクスは存在しません。カウンタは最初の 1 件で生まれるからです。先にトラフィックを作ります。
手元のブラウザで https://fanclub.local を数回リロードするのが一番簡単です。k8s-ops から curl でまとめて流すこともできますが、その場合はひと手間要ります——第11回で触れたとおり、k8s-ops のシェルにはプロキシが設定されており、no_proxy にはホスト名 fanclub.local が入っていないため、そのまま叩くと alma-proxy へ回されて弾かれます(応答コードが 000 になります)。no_proxy に足してから実行します。
実行コマンド(k8s-ops・developer):
$ export no_proxy="$no_proxy,fanclub.local"
$ for i in $(seq 1 20); do curl -sk -o /dev/null -w "%{http_code} " --resolve fanclub.local:443:192.168.1.124 https://fanclub.local/api/members; done; echo
実行結果(20 回とも 200 なら成功。000 が並ぶときは no_proxy の設定漏れです):
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
Traefik のメトリクスも Prometheus に拾わせます。ここでもう 1 つ壁があります。Traefik はメトリクスを Pod の 9100 番に出していますが、それを配る Service が既定では作られません(chart の metrics.prometheus.service.enabled が false)。確認してみます。
実行コマンド(k8s-ops・developer):
$ kubectl get svc -n traefik
実行結果(web と websecure だけ=メトリクス用の口が無い):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.104.8.146 192.168.1.200 80:32436/TCP,443:32709/TCP 21h
ServiceMonitor は Service を経由してスクレイプするので、この口を開けます。第11回で作った ~/gateway/traefik-values.yaml の末尾に次を追記して、helm upgrade します(chart を育てる作業がここでも出てきます)。
metrics:
prometheus:
service:
enabled: true # 追加:メトリクス用の Service を作る(既定は false)
実行コマンド(k8s-ops・developer):
$ cd ~/gateway
$ helm upgrade traefik traefik/traefik --namespace traefik --version 39.0.9 -f traefik-values.yaml
$ kubectl get svc -n traefik
実行結果(traefik-metrics という Service が増えた):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.104.8.146 192.168.1.200 80:32436/TCP,443:32709/TCP 21h
traefik-metrics ClusterIP 10.98.x.x <none> 9100/TCP 15s
メトリクス専用の Service が別に立ちました。外向けの traefik(LoadBalancer・.200)とは分けられており、監視用の口を外部公開しないという設計になっています。
最初から DOWN の Target がある理由
Target health を眺めていると、自分で作ったものではない Target がいくつも DOWN になっていることに気づくはずです。本環境では kube-etcd(3)・kube-scheduler(3)・kube-controller-manager(3)・kube-proxy(5)・node-exporter(5 のうち 4)が DOWN でした。これは壊れているのではなく、kubeadm クラスタに kube-prometheus-stack をそのまま入れると必ず起きる状態です。エラーメッセージを読むと、原因が 2 種類に分かれます。
no route to host(node-exporterの 9100・kube-etcdの 2381・kube-proxyの 10249):firewalld が塞いでいます。第2・4回で開けたのは 6443・10250・2379-2380・10257・10259 と NodePort だけで、メトリクス用のポートは開けていません。node-exporter が 1 つだけUPなのは、Prometheus 自身が載っているノード(本環境では wl-02)だけ firewalld を経由せず届くためです。connection refused(kube-schedulerの 10259・kube-controller-managerの 10257):ポートは開いているのに拒否されます。kubeadm が作る Static Pod は、これらのコンポーネントを--bind-address=127.0.0.1で起動するためです。ノードの外からは、そもそも待ち受けていません。
本回はこれらを直しません。直すには「firewalld に監視用ポートを足す」「Static Pod の manifest で --bind-address を 0.0.0.0 に変える」というクラスタ側の設定変更が要り、本回の主題(アプリを監視対象に加える)から外れるためです。ここで押さえてほしいのは、「監視を入れた=全部見えている」ではないということ、そして DOWN の理由はエラーメッセージが教えてくれる(no route to host なら経路/ファイアウォール、connection refused なら相手が待ち受けていない)という切り分けの型です。この読み分けは第16回のトラブルシュートでそのまま使います。
なお Traefik の chart には metrics.prometheus.serviceMonitor.enabled という値もあり、ServiceMonitor まで chart に作らせることもできます。ただし順序に注意です——これを有効にすると、chart は ServiceMonitor を作ろうとするため、先に kube-prometheus-stack(=ServiceMonitor の CRD)が入っていないと helm upgrade ごと失敗します(Error: UPGRADE FAILED: ... ERROR: You have to deploy monitoring.coreos.com/v1 first)。本回は「Prometheus を入れてから Traefik の設定を足す」順序なので有効にできますが、ここでは ServiceMonitor を手で書いて、何がどこを選んでいるかを目で追える形にします。この Service のラベルとポート名を確認します。
実行コマンド(k8s-ops・developer):
$ kubectl get svc traefik-metrics -n traefik -o jsonpath='{.metadata.labels}'; echo
$ kubectl get svc traefik-metrics -n traefik -o jsonpath='{.spec.ports}'; echo
実行結果:
{"app.kubernetes.io/component":"metrics","app.kubernetes.io/instance":"traefik-traefik","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"traefik","helm.sh/chart":"traefik-39.0.9"}
[{"name":"metrics","port":9100,"protocol":"TCP","targetPort":"metrics"}]
今度は名札(metadata.labels)が最初から付いています。app.kubernetes.io/component: metrics が「これがメトリクス用の Service だ」と示しているので、これを選びます(backend のときは名札が無くて自分で足しましたが、chart が作った Service には最初から付いている——同じ ServiceMonitor でも、相手の chart によって手間が違うわけです)。~/monitoring/servicemonitor-traefik.yaml を vi などで作成します。
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: traefik
namespace: traefik
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app.kubernetes.io/name: traefik
app.kubernetes.io/component: metrics
endpoints:
- port: metrics
path: /metrics
interval: 15s
honorLabels: true
最後の honorLabels: true がこの回で最も分かりにくい 1 行です。理由を説明します。Prometheus は ServiceMonitor でスクレイプするとき、「どの Kubernetes Service から取ったか」を service というラベルで付けます。ところが Traefik のメトリクスにも service ラベルが最初から入っています(fanclub-fanclub-frontend-http-80@kubernetesgateway のような値で、どのバックエンド宛ての通信かを表す、欲しい情報そのものです)。名前がぶつかると 既定では Prometheus 側が勝ち、service は traefik-metrics(Service 名)で上書きされてしまいます。これでは「fanclub 宛てだけ」を絞り込めません。honorLabels: true は「ぶつかったら取得元(Traefik)のラベルを優先しろ」という指定です。
実行コマンド(k8s-ops・developer):
$ cd ~/monitoring
$ kubectl apply -f servicemonitor-traefik.yaml
Prometheus の Status → Target health で serviceMonitor/traefik/traefik/0 が UP になったら、Grafana でパネルを作ります(Target への反映は 1〜2 分ほどかかることがあります。すぐに現れなくても少し待ってください)。Dashboards → New → New dashboard → Add visualization でデータソース Prometheus を選び、PromQL を入力します。この Prometheus データソースは chart が最初から登録済みなので、選ぶだけで使えます(この後の Loki は自分で登録します——そこが両者の違いです)。
PromQL(Grafana のクエリ欄に入力):
sum(rate(traefik_service_requests_total{service=~"fanclub-.*"}[1m]))
これが1 秒あたりのリクエスト数です。rate(...[1m]) が「1 分間の増加ペース」、sum(...) が全ラベルの合算。service=~"fanclub-.*" は正規表現で fanclub 宛てだけに絞っています(実際のラベル値は fanclub-fanclub-frontend-http-80@kubernetesgateway のような形で、Gateway API 経由であることが名前に出ています)。同じ要領で残りのパネルも作ります。
PromQL(エラーレート=5xx の割合):
sum(rate(traefik_service_requests_total{service=~"fanclub-.*",code=~"5.."}[5m]))
/
sum(rate(traefik_service_requests_total{service=~"fanclub-.*"}[5m]))
このパネルは正常なときは「No data」になります——分子(5xx の数)が 1 件も無いためです。エラーが出ていないのだから当然で、故障ではありません。ここで区別してほしいのは、5xx はサーバ側の失敗(backend が落ちる・タイムアウトするなど)だという点です。演習 2 で存在しないパスを叩くと 404 の系列は増えますが、404 は 4xx(クライアント側の誤り)なのでこの 5xx パネルは動きません——リクエスト数パネルに code="404" の系列が増えるだけです。この 5xx パネルが実際に跳ねるのは backend がエラーを返したときで、それは第16回のトラブルシュートで意図的に起こして観察します。
PromQL(P95 レイテンシ):
histogram_quantile(0.95, sum(rate(traefik_service_request_duration_seconds_bucket{service=~"fanclub-.*"}[5m])) by (le))
PromQL(JVM ヒープ使用量・backend の /metrics から):
memory_usedHeap_bytes{mp_scope="base"}
PromQL(Pod のリスタート回数・kube-state-metrics から):
kube_pod_container_status_restarts_total{namespace="fanclub"}
ここで気づいてほしいことがあります。この 5 枚のパネルは、3 つの違う出所から来ています。
- 入口の性能(リクエスト数・エラーレート・レイテンシ)→ Traefik(第11回)
- アプリの内部(JVM ヒープ)→ backend の
/metrics(ServiceMonitor で追加) - Kubernetes の状態(リスタート回数)→ kube-state-metrics(chart 同梱)
ダッシュボードとは、バラバラの出所を 1 枚に束ねたものです。「レイテンシが伸びた」と気づいたとき、同じ画面で「ヒープが張り付いていないか」「Pod が再起動していないか」を見られる——これが監視の実務です。作ったダッシュボードは右上の保存ボタンで名前を付けて保存します。
Loki + Fluent Bit でログを集約する
メトリクスで「おかしい」と気づいたら、次はログで原因を探します。いまは kubectl logs で 1 つずつ見るしかありません——Pod が再起動すれば消え、5 ノードに散らばったログを横断検索することもできません。そこで Loki(保存・検索)と Fluent Bit(各ノードで収集)を入れます。
その前に 1 つ整理を。fanclub には第4回から fanclub-logcollector という DaemonSet が動いていますが、これは第1巻で DaemonSet の形を学ぶために置いた「見本」で、実際にはログを集めていません(busybox が起動メッセージを 1 行出して眠っているだけです)。本物のログ収集は、これから入れる Fluent Bit が担います。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n fanclub -l app=fanclub-logcollector --tail=3
実行結果(起動メッセージだけ=何も集めていない):
log collector started on fanclub-logcollector-486jq
Loki と Fluent Bit を入れます。配信元 grafana.github.io は本回の冒頭で whitelist に足してあります。
実行コマンド(k8s-ops・developer):
$ helm repo add grafana https://grafana.github.io/helm-charts
$ helm repo update
$ helm install loki grafana/loki \
--namespace monitoring \
--version 7.1.0 \
--set deploymentMode=SingleBinary \
--set loki.auth_enabled=false \
--set loki.commonConfig.replication_factor=1 \
--set loki.storage.type=filesystem \
--set loki.schemaConfig.configs[0].from=2024-04-01 \
--set loki.schemaConfig.configs[0].store=tsdb \
--set loki.schemaConfig.configs[0].object_store=filesystem \
--set loki.schemaConfig.configs[0].schema=v13 \
--set loki.schemaConfig.configs[0].index.prefix=index_ \
--set loki.schemaConfig.configs[0].index.period=24h \
--set singleBinary.replicas=1 \
--set singleBinary.persistence.size=5Gi \
--set read.replicas=0 --set write.replicas=0 --set backend.replicas=0 \
--set chunksCache.enabled=false --set resultsCache.enabled=false \
--set lokiCanary.enabled=false --set test.enabled=false
実行結果(抜粋):
NAME: loki
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
--set が多いので、何を指定しているのかを整理します。この chart はクラウド前提の既定になっていて、そのまま入れると Please define loki.storage.bucketNames.chunks と怒られて失敗します——S3 のようなオブジェクトストレージを要求してくるのです。本回はオンプレなので、loki.storage.type=filesystem でノードのファイルシステムに保存させ、schemaConfig で保存形式(tsdb・v13)を明示します。replication_factor=1 と read/write/backend.replicas=0 は役割分割をやめて 1 つの Pod に寄せる指定、chunksCache/resultsCache の無効化はメモリを食うキャッシュを省くため、lokiCanary/test の無効化は学習に不要な付属品を省くためです。persistence.size=5Gi は先ほどの容量の事情に合わせています。
Loki は本番では複数の役割に分けて動かしますが、本回は学習用に SingleBinary(1 つの Pod で全部やる)にしています。次に Fluent Bit を入れ、出力先を Loki に向けます。Fluent Bit は各ノードのコンテナログ(/var/log/containers/*.log)を読むので、DaemonSet として配られます——第7回で見た「全ノードに 1 つずつ」の形です。ここで tolerations の 3 行を付けているのは、Control Plane にも配るためです。この chart は既定では taint を許容せず Workload Node の 2 台にしか載りませんが、API サーバや etcd のログは第15回のトラブルシュートで最も見たい対象です。先ほどの node-exporter と同じく、「アプリは載せないが監視・収集は載せる」ために CP の NoSchedule を許容させ、全 5 ノードに行き渡らせます。
実行コマンド(k8s-ops・developer):
$ helm install fluent-bit grafana/fluent-bit \
--namespace monitoring \
--version 2.6.0 \
--set loki.serviceName=loki \
--set "tolerations[0].key=node-role.kubernetes.io/control-plane" \
--set "tolerations[0].operator=Exists" \
--set "tolerations[0].effect=NoSchedule"
実行結果(抜粋):
NAME: fluent-bit
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
全ノードに配られたかを確認します。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n monitoring -o wide | grep fluent-bit
実行結果(5 ノードに 1 つずつ=Control Plane 3 台にも載っている):
fluent-bit-fluent-bit-loki-fxq8w 1/1 Running 0 3m 10.244.30.7 k8s-cp-01
fluent-bit-fluent-bit-loki-gqd7p 1/1 Running 0 3m 10.244.183.5 k8s-cp-02
fluent-bit-fluent-bit-loki-pcf6x 1/1 Running 0 3m 10.244.42.3 k8s-cp-03
fluent-bit-fluent-bit-loki-kxkh8 1/1 Running 0 3m 10.244.215.207 k8s-wl-01
fluent-bit-fluent-bit-loki-wt89r 1/1 Running 0 3m 10.244.119.163 k8s-wl-02
Grafana に Loki をデータソースとして登録し(Connections → Data sources → Add new → Loki、URL は http://loki:3100)、Explore を開きます。データソースに Loki を選び、LogQL で検索します。
LogQL(Grafana Explore に入力):
{namespace="fanclub"}
fanclub 名前空間の全 Pod のログが、1 つの画面に時系列で並びます。ここで使えるラベルを押さえておきます。Fluent Bit は各ログに Pod のメタデータをラベルとして付けており、namespace のほかに app(アプリ名=ここでは fanclub-backend)・container(コンテナ名)・instance(Pod 名そのもの)が使えます。Prometheus 側の pod ラベルとは名前が違う点に注意してください(収集経路が別物なので、ラベルの付き方も別々です)。app でアプリ単位に絞り込むのが一番読みやすいので、backend のログだけを見てみます。
LogQL(backend のログだけ・エラーを含む行だけ):
{namespace="fanclub", app="fanclub-backend"} |= "ERROR"
{} の中がラベルによる絞り込み、|= が行の中身の検索です。PromQL と似た書き味なのは狙ってそうなっています。正常なときはこの検索は 0 件になります——backend が ERROR を吐いていないからで、エラーレートのパネルが「No data」だったのと同じ理由です。障害時にはここに該当行が並びます。|= "ERROR" を外せば backend の全ログが見えます。これで「Grafana でグラフを見て異常に気づき、同じ画面でログを検索して原因を探す」という流れが手に入りました。第15・16回のトラブルシュートで効いてきます。
やってみよう
本回で入れた監視スタックは第14回以降もそのまま使いますので、後始末で削除しないでください。
ServiceMonitor が「何を見て」つながっているかを確認する
kubectl get svc fanclub-backend -n fanclub -o jsonpath='{.metadata.labels}' と、ServiceMonitor の spec.selector.matchLabels を見比べて、両者が一致していることを確認してください。そのうえで spec.selector(Pod を選ぶ側)とは別物であることを説明できれば、この回の核心は掴めています。
負荷をかけてグラフを動かす
k8s-ops で export no_proxy="$no_proxy,fanclub.local" を実行してから(本文と同じ理由です)、for i in $(seq 1 100); do curl -sk -o /dev/null --resolve fanclub.local:443:192.168.1.124 https://fanclub.local/api/members; done を実行し、Grafana の「リクエスト数」パネルが跳ね上がることを確認してください。存在しないパス(例 /api/nothing)を叩くと code ラベルが 404 の系列が増えるのも観察できます。
メトリクスとログを突き合わせる
Grafana の Explore で {namespace="fanclub", app="fanclub-backend"} を検索し、演習2 で流したリクエストの時刻あたりのログを探してください。グラフで見えた山と、ログの時刻が一致することを確かめられれば、メトリクスとログの両輪が使えています。
まとめ
本回では fanclub-api の監視ダッシュボードを作りました。要点は、①監視はメトリクス(Prometheus/Grafana)とログ(Loki/Fluent Bit)の二本柱で、前者で異常に気づき後者で原因を探す、②metrics-server は「今の値」・Prometheus は「時系列のカルテ」——役割が違うので併存する、③ServiceMonitor が見るのは Service の metadata.labels であり、Pod を選ぶ spec.selector とは別物。継承 chart には名札が無かったのでchart を育てて追加した(第12回の storageClassName と同じ流れ)、④メトリクスの出所は 3 つ——入口の性能は Traefik(第11回)、アプリ内部は backend の /metrics、K8s の状態は kube-state-metrics。アプリが REST の統計を出さないなら、入口で測ればよい、⑤カウンタはトラフィックが無いと存在しない(先に叩いてからパネルを作る)、⑥監視データの PVC は第12回の Longhorn に自動で載る、でした。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- Prometheus はメトリクス(数値の時系列)を、Loki はログ(テキスト)を扱う。
- metrics-server があれば Prometheus は要らない。
- ServiceMonitor の
spec.selector.matchLabelsは、Service のmetadata.labelsを選ぶ。 - Service の
spec.selectorにapp: fanclub-backendがあれば、ServiceMonitor もその Service を拾える。 - fanclub-backend の
/metricsから HTTP のレイテンシが取得できる。 traefik_service_requests_totalは、リクエストが 1 度も無くても値 0 で存在する。- Fluent Bit は各ノードのログを読むため DaemonSet で配る。
- kube-state-metrics は Pod の再起動回数など Kubernetes の状態をメトリクス化する。
- Grafana は Prometheus と Loki を同じ画面で扱える。
解答と解説
1=○(2 系統の役割分担)/2=×(metrics-server は「今の値」だけで履歴もアラートも無い。HPA と kubectl top 用)/3=○(本回の核心)/4=×(spec.selector は Service が Pod を選ぶ条件。ServiceMonitor が見るのは metadata.labels=だから chart に名札を足した)/5=×(MicroProfile Metrics 5.x は REST の自動計測を持たない。レイテンシは Traefik から取る)/6=×(カウンタは最初のリクエストで生まれる。無通信だと存在しない)/7=○(各ノードの /var/log/containers を読むため)/8=○/9=○(データソースを 2 つ登録して使い分ける)
次回予告
次回・第14回は第5部の後半、Helm + Kustomize + ArgoCD + Velero です。ここまで helm upgrade を手で叩いてきましたが、本番ではGit にコミットしたら自動で反映される(GitOps)形にします。あわせて Velero でクラスタのバックアップを取り、運用の仕上げに入ります。
