前言

當你把服務一個個丟進 Kubernetes 叢集之後,一定會遇到這些讓人半夜驚醒的時刻:

  • 「使用者說網站很慢,但我怎麼知道是哪個節點的 CPU 爆了?」
  • 「某個 Pod 一直重啟,到底是記憶體不夠,還是被 OOMKill 了?」
  • 「我想做一張漂亮的儀表板給老闆看,但數據要從哪裡來?」

這些問題的答案,其實都指向同一套解法:Prometheus 負責『收集與儲存』數據,Grafana 負責『把數據畫成人看得懂的圖』。 這兩個工具幾乎是雲原生世界的監控黃金組合。

網路上大部分教學都叫你直接 helm install 一鍵安裝,但那樣你永遠不會知道「箱子裡面到底裝了什麼」。這篇文章反其道而行——我們用手寫 YAML manifests 的方式,在一個 k3s 輕量叢集上,一塊磚一塊磚地把整套監控系統疊起來。等你讀完,你不只會「用」,更會「懂」。

📦 本文所有範例都跑在 k3d 叢集(namespace 為 monitoring)。k3s 是輕量級的 Kubernetes 發行版,而 k3d 則是把 k3s 跑在 Docker 裡的工具,很適合本機學習。指令換成任何標準 K8s 叢集也都通用。

準備工作:在本機開一個 k3s 叢集

⏭️ 已經有 K8s 叢集了嗎? 如果你手邊已經有可用的叢集(不管是 k3s、k3d、minikube、kind 還是雲端的 EKS/GKE),這一節可以直接跳過,跳到下一章開始部署監控系統即可。

我們用 k3d 在本機開叢集——它把 k3s 打包進 Docker,一行指令就能開出多節點叢集,用完刪掉也乾淨,最適合學習與實驗。

1. 安裝 k3d 與 kubectl

先確認兩個工具都裝好了(Docker 也要先開著):

1
2
3
4
5
6
7
8
9
# 安裝 k3d(macOS / Linux 通用的官方腳本)
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash

# macOS 也可以用 Homebrew:brew install k3d kubectl

# 確認版本(順便確認 Docker 有在跑)
k3d version
kubectl version --client
docker info | head -3

2. 建立一個 3 節點叢集

本文範例輸出是 1 個 server + 2 個 agent(共 3 個節點),這樣才能看到 node-exporter「每節點各一個」的效果。同時把 80 埠映射出來,稍後才能用 prometheus.localhost / grafana.localhost 這類網址從瀏覽器連進去:

1
2
3
k3d cluster create mycluster \
--agents 2 \
--port "80:80@loadbalancer"
  • --agents 2:開 2 個 worker 節點(加上預設的 1 個 server,共 3 個)。
  • --port "80:80@loadbalancer":把主機的 80 埠接到叢集的負載平衡器,讓 Traefik Ingress 收得到 *.localhost 的請求。

💡 k3d 建立的叢集內建 Traefik 當 Ingress Controller(這也是 k3s 的預設),所以我們後面 Prometheus / Grafana 的 Ingress 直接就能用,不必額外安裝。

3. 確認叢集就緒

1
2
# 應看到 3 個節點,狀態都是 Ready
kubectl get nodes

理想輸出:

1
2
3
4
NAME                      STATUS   ROLES                  AGE   VERSION
k3d-mycluster-server-0 Ready control-plane,master 40s v1.30.x+k3s1
k3d-mycluster-agent-0 Ready <none> 35s v1.30.x+k3s1
k3d-mycluster-agent-1 Ready <none> 35s v1.30.x+k3s1

看到這三個節點,就代表叢集準備好了——後面文章裡出現的 k3d-mycluster-server-0k3d-mycluster-agent-0 這些 node 名稱,就是從這裡來的。

🧹 實驗完想清乾淨? 一行就能把整個叢集連同容器刪掉:k3d cluster delete mycluster

核心概念:這套系統長什麼樣子?

在動手之前,先建立一張心智地圖。整套監控系統其實是「指標來源收集者視覺化」的資料流:

flowchart LR
    subgraph 指標來源["📊 指標來源 (Exporters)"]
        NE["node-exporter
主機 CPU/記憶體/磁碟
:9100"] KSM["kube-state-metrics
K8s 物件狀態
:8080"] end subgraph 收集者["🔍 收集與儲存"] PROM["Prometheus
抓取 + 儲存 + 查詢
:9090"] end subgraph 視覺化["📈 視覺化"] GRAF["Grafana
儀表板
:3000"] end K8SAPI["Kubernetes API"] NE ==>|/metrics| PROM KSM ==>|/metrics| PROM PROM -.->|服務發現
「現在有哪些目標?」| K8SAPI KSM -.->|讀取物件狀態| K8SAPI PROM ==>|PromQL 查詢| GRAF

📖 怎麼看這張圖?

  • 粗實線(➡)= 指標數據的流動:誰把 metrics 交給誰、誰查詢誰的數據。這是「數據」實際走的路。
  • 虛線(⇢)= 對 Kubernetes API 的詢問:不是在傳指標,而是「打聽情報」——Prometheus 問 API「現在有哪些目標可以抓」(服務發現),KSM 問 API「各個物件現在是什麼狀態」。

我們可以把每個角色想像成一間餐廳:

角色 餐廳比喻 實際功能
node-exporter 每個廚房裡的溫度計 回報每台機器的 CPU、記憶體、磁碟
kube-state-metrics 訂單系統的狀態面板 回報每個 K8s 物件(Pod、Deployment)的狀態
Prometheus 巡邏的店長 定時去每個溫度計「抄數字」並記錄下來
Grafana 給老闆看的營運大螢幕 把店長的紀錄畫成漂亮圖表

理解了這張圖,接下來我們就照著資料流的順序,從指標來源開始,一路蓋到 Grafana。整趟旅程分成六步:

flowchart LR
    S1["Step 1
node-exporter"] --> S2["Step 2
kube-state-metrics"] --> S3["Step 3
Prometheus RBAC"] --> S4["Step 4
Prometheus Config"] --> S5["Step 5
Prometheus 本體"] --> S6["Step 6
Grafana"]

Step 1:node-exporter — 讓每台主機開口說話

🗂️ 先建立 namespace:本文所有資源都放在 monitoring 這個 namespace,動手前先建好它。

1
kubectl create namespace monitoring

第一個問題:Prometheus 要去哪裡抓「這台機器的 CPU 用了多少」?答案是 node-exporter——一個專門把主機層級指標(CPU、記憶體、磁碟、網路)輸出成 Prometheus 格式的小程式。

📄 每個 Step 我會把 YAML 拆開講解;完整、可直接複製的檔案都收在文末的**「附錄:完整 manifests」**。你可以把每步的完整 YAML 存成對應檔名(如 node-exporter.yaml),再用下面的 kubectl apply 部署。

為什麼用 DaemonSet?

我們希望每個節點剛好跑一個 node-exporter,這正是 DaemonSet 的專長。不像 Deployment 要你指定「跑幾個副本」,DaemonSet 會自動保證「叢集裡每加一個節點,就補一個 Pod 上去」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: DaemonSet # ← 關鍵:每個節點剛好一個 Pod
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true # 看見「主機」的網路
hostPID: true # 看見「主機」的行程
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.2

容器怎麼看見「主機」的真實數據?

這裡有個很容易踩坑的觀念:容器預設是被隔離的,它讀到的 /proc(Linux 存放系統資訊的地方)只有容器自己的資訊,看不到宿主機。所以我們得動兩個手腳:

  1. 打開隔離開關hostNetwork: truehostPID: true 讓容器能「看見」主機的網路流量與行程。
  2. 把主機的目錄掛進來,再叫程式去讀掛進來的路徑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    args:
# 告訴 node-exporter:別讀你自己的 /proc,去讀我掛給你的 /host/proc
- --path.procfs=/host/proc # CPU、記憶體、核心資訊
- --path.sysfs=/host/sys # 硬體驅動、設備狀態
- --path.rootfs=/host/root # 整個系統的磁碟掛載點
# 過濾掉虛擬檔案系統,避免出現「/proc 佔用 0 bytes」這種垃圾數據
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)
ports:
- containerPort: 9100
name: metrics
volumeMounts:
- { name: proc, mountPath: /host/proc, readOnly: true }
- { name: sys, mountPath: /host/sys, readOnly: true }
- { name: root, mountPath: /host/root, readOnly: true }
volumes:
- { name: proc, hostPath: { path: /proc } }
- { name: sys, hostPath: { path: /sys } }
- { name: root, hostPath: { path: / } }

💡 為什麼要 --collector.filesystem.mount-points-exclude
/proc/sys/dev 這些是 Linux 的「偽檔案系統」——它們活在記憶體裡,根本沒有真實硬碟。如果不排除,你的磁碟監控圖表就會被一堆「佔用 0 bytes 的假硬碟」洗版。

用 Headless Service 讓每個節點都被發現

node-exporter 跑起來後,還要給它一個 Service 當「對外窗口」,好讓 Prometheus 找得到它。但這裡藏著一個致命細節:必須用 headless Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter # ← Prometheus 稍後會用這個 label 過濾
annotations:
# 對 Prometheus 喊話:「請來抓我!我的 metrics 在 :9100/metrics」
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
prometheus.io/path: "/metrics"
spec:
clusterIP: None # ← headless!關鍵中的關鍵
selector:
app: node-exporter
ports:
- { name: metrics, port: 9100, targetPort: 9100 }

為什麼 clusterIP: None 這麼重要?看這張對照圖:

flowchart TB
    subgraph 普通["❌ 普通 Service (有 clusterIP)"]
        P1["Prometheus"] -->|只給一個 VIP| VIP["虛擬 IP
負載平衡"] VIP -.->|隨機挑一個| N1["節點1"] VIP -.-> N2["節點2"] VIP -.-> N3["節點3"] end subgraph 無頭["✅ Headless Service (clusterIP: None)"] P2["Prometheus"] -->|拿到完整 IP 清單| L["節點1 IP
節點2 IP
節點3 IP"] L --> M1["直接抓節點1"] L --> M2["直接抓節點2"] L --> M3["直接抓節點3"] end

普通 Service 只給一個虛擬 IP 並做負載平衡,Prometheus 每次只會隨機抓到一台節點——這樣你就漏掉了其他機器!Headless Service 不做負載平衡,而是把背後每一個 Pod 的真實 IP 都攤開給 Prometheus,這樣才能「一台都不漏地」監控全部節點。

🚀 部署

把上面的 DaemonSet 與 Service 合併存成 node-exporter.yaml(完整內容見附錄),然後套用:

1
2
3
4
kubectl apply -f node-exporter.yaml

# 確認每個節點都起了一個 node-exporter Pod
kubectl -n monitoring get pod -l app=node-exporter -o wide

✅ 驗證:node-exporter 有在吐數據嗎?

部署後,我們可以開一個臨時容器去 curl 它的 /metrics

1
2
3
4
kubectl -n monitoring run ne-test --rm -i --restart=Never \
--image=curlimages/curl:8.10.1 -- \
curl -s http://node-exporter:9100/metrics 2>&1 \
| grep -E "^node_(memory_MemAvailable|cpu_seconds_total|filesystem_avail)" | head -5

看到 node_memory_MemAvailable_bytes ... 這類輸出,就代表成功了。這行指令的逐字拆解可以參考 [[Kubernetes 常用測試指令]]。

Step 2:kube-state-metrics — 監控「K8s 物件」的狀態

node-exporter 顧的是「機器健不健康」,但它不知道「Pod 有沒有在跑」「Deployment 該有 3 個副本現在只剩 1 個」。這種 Kubernetes 物件層級的狀態,交給 kube-state-metrics(簡稱 KSM)。

它會輸出像這樣的指標:

  • kube_pod_status_phase — Pod 現在是 Running 還是 Pending?
  • kube_deployment_status_replicas — Deployment 實際有幾個副本?

KSM 為什麼需要 RBAC 權限?

關鍵差異來了:KSM 要輸出這些狀態,它得先去問 Kubernetes API「現在有哪些 Pod、哪些 Deployment」。而在 K8s 裡,凡是要碰 API 都得先有「身分證」和「許可證」——這就是 RBAC(Role-Based Access Control,角色權限控制)。

我們給 KSM 配三樣東西:身分(ServiceAccount)→ 權限(ClusterRole)→ 綁定(ClusterRoleBinding)

flowchart LR
    SA["ServiceAccount
(身分證:我是 KSM)"] CR["ClusterRole
(許可證:可唯讀這些資源)"] CRB["ClusterRoleBinding
(把許可證發給這個身分)"] SA --- CRB CR --- CRB CRB --> API["✅ 可以呼叫 K8s API"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1) 身分:給 KSM 一個 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-state-metrics
namespace: monitoring
---
# 2) 權限:一個「唯讀」的叢集角色(只給 list/watch,最小權限原則)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-state-metrics
rules:
- apiGroups: [""]
resources: [nodes, pods, services, endpoints, namespaces, persistentvolumeclaims]
verbs: ["list", "watch"]
- apiGroups: ["apps"]
resources: [deployments, replicasets, statefulsets, daemonsets]
verbs: ["list", "watch"]
# ... 其餘 batch/jobs、networking/ingresses 同理
---
# 3) 綁定:把角色綁到身分上
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kube-state-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-state-metrics
subjects:
- kind: ServiceAccount
name: kube-state-metrics
namespace: monitoring

🔐 最小權限原則:這裡只給 listwatch(唯讀),完全沒有 create/delete。而且授權的資源清單刻意跟下方 Deployment 的 --resources 參數一一對應——你收集什麼,才授權什麼,多一個都不給。

本體 Deployment

KSM 只需要一個副本就夠了(它不是每節點都要跑),所以用普通的 Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-state-metrics
namespace: monitoring
labels:
app: kube-state-metrics
spec:
replicas: 1
selector:
matchLabels: { app: kube-state-metrics }
template:
metadata:
labels: { app: kube-state-metrics }
spec:
serviceAccountName: kube-state-metrics # ← 套用上面授權的身分!
containers:
- name: kube-state-metrics
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.13.0
args:
# 只收集這些資源,其餘不碰(log 乾淨 + 呼應上面的最小權限)
- --resources=nodes,pods,deployments,replicasets,daemonsets,statefulsets,services,endpoints,ingresses,namespaces,jobs,persistentvolumeclaims
ports:
- { containerPort: 8080, name: metrics }
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10

Service 的寫法跟 node-exporter 幾乎一樣——一樣貼上 prometheus.io/scrape: "true" 的 annotation,只是 port 換成 8080。注意 KSM 不需要 headless(它只有一個 Pod,沒有「漏抓節點」的問題),用普通 Service 即可。

🚀 部署

把 RBAC(SA/ClusterRole/Binding)+ Deployment + Service 一起存成 kube-state-metrics.yaml(完整內容見附錄)後套用:

1
2
3
4
kubectl apply -f kube-state-metrics.yaml

# 等 Pod Ready(readinessProbe 通過)
kubectl -n monitoring rollout status deployment/kube-state-metrics

Step 3:Prometheus 的 RBAC — 服務發現的鑰匙

現在輪到主角 Prometheus 登場。但在跑它之前,我們得先給它權限——原因和 KSM 類似但目的不同,這是很多人搞混的點:

flowchart TB
    subgraph KSM_RBAC["KSM 需要 API 是為了…"]
        A1["📤 輸出指標
(讀取物件狀態變成 metrics)"] end subgraph PROM_RBAC["Prometheus 需要 API 是為了…"] B1["🔍 服務發現
(問:現在有哪些節點/Pod/endpoint 可以抓?)"] end

Prometheus 需要唯讀 nodesservicesendpointspods,這正是下一步 kubernetes_sd_configs(服務發現)能運作的基礎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prometheus
rules:
# 服務發現要讀這些物件,才知道「該去哪些位址抓 metrics」
- apiGroups: [""]
resources: ["nodes", "nodes/metrics", "services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring

🚀 部署

存成 prometheus-rbac.yaml(完整內容見附錄)後套用。RBAC 沒有 Pod,套用後可用 get 確認物件建立成功:

1
2
3
4
kubectl apply -f prometheus-rbac.yaml

kubectl -n monitoring get serviceaccount prometheus
kubectl get clusterrole prometheus

Step 4:Prometheus Config — 整套系統最聰明的一步

這一步是整篇文章的靈魂。Kubernetes 環境最頭痛的地方是:Pod 隨時擴充、重啟、刪除,IP 每次都在變。如果你要手動維護一份「監控目標清單」,那簡直是惡夢。

Prometheus 的解法是 Service Discovery(服務發現,kubernetes_sd_configs)——它不要你手動列清單,而是直接「問」Kubernetes API:「現在有哪些目標?」清單自動更新,你完全不用管。

設定檔全貌

設定檔以 ConfigMap 形式掛進容器的 /etc/prometheus/prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s # 每 15 秒抓一次
evaluation_interval: 15s

scrape_configs:
# ── Job 1:抓 Prometheus 自己(健康自我監控)──
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

# ── Job 2:自動發現所有貼了 annotation 的服務 ──
- job_name: kubernetes-service-endpoints
kubernetes_sd_configs:
- role: endpoints # ← 深入到每個 Pod,而非只看 Service VIP
namespaces:
names: ["monitoring"]
relabel_configs:
# 四段 relabel 魔法,下面詳解
...

🎯 為什麼是 role: endpoints 而不是 role: service
如果用 role: service,Prometheus 只會抓到 Service 的虛擬 IP,經過負載平衡後數據會被分散、抓不完整。改用 role: endpoints,Prometheus 會深入到 Service 背後,把對應的每一個 Pod 都揪出來逐一抓取。(跟 Step 1 的 headless Service 是同一個道理!)

relabel_configs:四段「改寫魔法」

relabel_configs 是最讓新手頭暈的部分,但它其實就是一條「加工生產線」:把 K8s API 回傳的原始資料,一步步改寫成 Prometheus 好用的樣子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
relabel_configs:
# (1) 篩選器:只保留貼了 prometheus.io/scrape=true 的目標,其餘丟棄
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
regex: "true"
action: keep
# (2) 覆寫路徑:若服務指定了 prometheus.io/path,就用它(預設 /metrics)
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
regex: "(.+)"
target_label: __metrics_path__
action: replace
# (3) 覆寫 Port:把抓取位址改成 <PodIP>:<annotation 指定的 port>
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
regex: "([^:]+)(?::\\d+)?;(\\d+)"
replacement: "$1:$2"
target_label: __address__
action: replace
# (4) 美化標籤:把機器看的中繼標籤,整理成人看得懂的最終標籤
- source_labels: [__meta_kubernetes_service_label_app]
target_label: job # 用 Service 的 app label 當 job 名稱
- source_labels: [__meta_kubernetes_pod_node_name]
target_label: node # 哪一台機器
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod

具體會發生什麼事?一個實例走一遍

光看規則太抽象,我們拿一個真實服務 my-api-service 走一遍。假設它的 metadata 是這樣:

1
2
3
4
5
6
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/api/metrics"
prometheus.io/port: "8080"
labels:
app: my-api

四段規則加工後的「後果」:

規則 動作 效果
(1) keep 檢查 scrape=true?✅ 是 放進監控隊列。若是 false 或沒貼,連出現的資格都沒有
(2) 覆寫路徑 讀到 path=/api/metrics 改抓 http://<PodIP>:8080/api/metrics,而非預設 /metrics
(3) 覆寫 Port 讀到 port=8080 把預設 port 換成 8080,解決「服務埠口不統一」的問題
(4) 美化標籤 讀取各種 meta 標籤 見下表 👇

第 (4) 步把「機器語言」翻譯成「Grafana 語言」:

原始 Metadata(機器看的) 轉化後的 Label(人看的)
__meta_kubernetes_service_label_app job="my-api"
__meta_kubernetes_pod_node_name node="k3d-mycluster-server-0"
__meta_kubernetes_namespace namespace="monitoring"
__meta_kubernetes_pod_name pod="my-api-74895fbc9-xyz12"

最終,Prometheus 介面上這個目標會長成這樣:

1
2
3
4
5
6
7
目標位址:10.42.0.5:8080
Labels:
job: "my-api"
node: "k3d-mycluster-server-0"
namespace: "monitoring"
pod: "my-api-74895fbc9-xyz12"
instance: "10.42.0.5:8080"

這套設計對你的三大好處

  • 極度解耦:監控配置完全脫離應用程式碼。要監控新服務?只要在它的 Service 貼上 prometheus.io/* annotation,完全不用改這個設定檔
  • 自動擴展:當 my-api 從 1 個 Pod 水平擴展到 10 個,Prometheus 透過 role: endpoints 自動偵測到新的 10 個 IP,自動套用規則,你什麼都不用做。
  • Grafana 超好用:因為有了 jobnamespace 這些標籤,你在 Grafana 能輕鬆寫出這種查詢:
    1
    sum(rate(http_requests_total{job="my-api"}[1m]))

🚀 部署

把這個 ConfigMap 存成 prometheus-config.yaml(完整內容見附錄)後套用。注意:ConfigMap 本身只是「一份設定資料」,要等 Step 5 的 Prometheus 把它掛載進去才會真正生效:

1
2
3
kubectl apply -f prometheus-config.yaml

kubectl -n monitoring get configmap prometheus-config

Step 5:Prometheus 本體 — 抓取、儲存、查詢一次搞定

前面鋪陳的權限和設定都是為了這一步。Prometheus 本體同時扮演四個角色:抓取器 + 時序資料庫(TSDB)+ 查詢引擎 + 內建 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: monitoring
labels: { app: prometheus }
spec:
replicas: 1
selector:
matchLabels: { app: prometheus }
template:
metadata:
labels: { app: prometheus }
spec:
serviceAccountName: prometheus # ← 用 Step 3 的身分,才有服務發現權限
containers:
- name: prometheus
image: prom/prometheus:v2.54.1
args:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=7d # 資料只留 7 天(學習夠用)
- --web.enable-lifecycle # 開啟 /-/reload 熱重載端點
ports:
- { containerPort: 9090, name: web }
readinessProbe:
httpGet: { path: /-/ready, port: 9090 }
initialDelaySeconds: 10
volumeMounts:
- { name: config, mountPath: /etc/prometheus } # 掛入 Step 4 的設定
- { name: data, mountPath: /prometheus } # TSDB 資料
volumes:
- name: config
configMap: { name: prometheus-config }
- name: data
emptyDir: {} # ← 注意:沒掛 PVC,重啟資料清空

⚠️ 持久化策略:Prometheus 故意「不」持久化
這裡 data 用的是 emptyDir(記憶體/暫存空間),重啟就清空。為什麼?因為這是學習環境,時序資料重啟後 15 秒內就會重新抓滿,沒必要為它掛 PVC。等一下你會看到 Grafana 的策略正好相反。

搭配一個普通 Service(:9090)和一個 Ingress,讓你用瀏覽器就能進 Prometheus UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prometheus
namespace: monitoring
spec:
ingressClassName: traefik # k3s 內建 Traefik 當 Ingress Controller
rules:
- host: prometheus.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: prometheus, port: { number: 9090 } }

🚀 部署

把 Deployment + Service + Ingress 存成 prometheus.yaml(完整內容見附錄)後套用:

1
2
3
kubectl apply -f prometheus.yaml

kubectl -n monitoring rollout status deployment/prometheus

✅ 驗證:Prometheus 到底抓到了哪些目標?

這是確認「設定有沒有生效」最直接的方法——把所有「正在監控中的目標」清單拉出來:

1
2
3
4
5
6
7
8
9
10
11
12
13
POD=$(kubectl -n monitoring get pod -l app=prometheus -o jsonpath='{.items[0].metadata.name}')

kubectl -n monitoring exec $POD -- \
wget -qO- "http://localhost:9090/api/v1/targets?state=active" > /tmp/t.json 2>/dev/null

python3 -c "
import json
d = json.load(open('/tmp/t.json'))
ts = d['data']['activeTargets']
print(f'共 {len(ts)} 個 active target:')
for t in ts:
print(f\" [{t['health']:6}] job={t['labels'].get('job','?'):22} node={t['labels'].get('node','')}\")
"

理想的輸出(3 節點叢集):

1
2
3
4
5
6
共 5 個 active target:
[up ] job=kube-state-metrics node=k3d-mycluster-agent-0
[up ] job=node-exporter node=k3d-mycluster-server-0
[up ] job=node-exporter node=k3d-mycluster-agent-0
[up ] job=node-exporter node=k3d-mycluster-agent-1
[up ] job=prometheus node=

看到 node-exporter ×3(每節點一個,證明 headless Service 生效了)+ KSM + Prometheus 自己,全部 up,就大功告成了!你也可以直接開瀏覽器進 http://prometheus.localhostStatus → Targets 用圖形介面看同樣的東西。

Step 6:Grafana — 把數字變成漂亮的圖

Prometheus 的內建 UI 只能做簡單查詢,真正給人看的儀表板要交給 Grafana。Grafana 做的事很單純:連上 Prometheus 當資料來源,把 PromQL 查詢畫成圖表。

兩個關鍵設計決策

Grafana 的 YAML 比較長,但核心只有兩個要理解的決策:

決策一:資料來源用 provisioning 自動接好。 我們不想每次都手動在 UI 裡填 Prometheus 網址,所以用一個 ConfigMap 預先寫好,開機自動載入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources
namespace: monitoring
data:
datasources.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090 # 同 namespace,直接用短服務名
isDefault: true

決策二:持久化策略跟 Prometheus 相反——Grafana 要掛 PVC。 因為你在 UI 裡辛苦 import 的儀表板會存進 Grafana 內部的 SQLite(/var/lib/grafana/grafana.db),這個絕對不能重啟就消失:

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-data
namespace: monitoring
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests: { storage: 1Gi }
Prometheus Grafana
儲存 emptyDir(不持久化) PVC(持久化)
理由 時序資料重啟秒補回 手動 import 的儀表板不能丟

本體 Deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
labels: { app: grafana }
spec:
replicas: 1
strategy:
type: Recreate # PVC 是 RWO,避免新舊 Pod 同時掛載
selector:
matchLabels: { app: grafana }
template:
metadata:
labels: { app: grafana }
spec:
securityContext:
fsGroup: 472 # grafana 以 uid 472 執行,給它 PVC 寫入權
containers:
- name: grafana
image: grafana/grafana:11.2.0
env:
- name: GF_SECURITY_ADMIN_USER
valueFrom: { secretKeyRef: { name: grafana-admin, key: admin-user } }
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom: { secretKeyRef: { name: grafana-admin, key: admin-password } }
ports:
- { containerPort: 3000, name: web }
volumeMounts:
- { name: data, mountPath: /var/lib/grafana } # → PVC
- { name: datasources, mountPath: /etc/grafana/provisioning/datasources } # → 資料來源
volumes:
- name: data
persistentVolumeClaim: { claimName: grafana-data }
- name: datasources
configMap: { name: grafana-datasources }

帳密用一個 Secret 存放(本文 dev 用途設 admin/admin正式環境務必改掉),再配上和 Prometheus 同款的 Service(:3000)+ Ingress(grafana.localhost)。

🚀 部署

把 Secret + PVC + ConfigMap + Deployment + Service + Ingress 存成 grafana.yaml(完整內容見附錄)後套用:

1
2
3
kubectl apply -f grafana.yaml

kubectl -n monitoring rollout status deployment/grafana

⚠️ 踩坑警示:改了 datasource 為什麼沒生效?

這是一個超級常見的困惑:「我改了 ConfigMap 裡的 datasource,怎麼 Grafana 沒反應?」

答案是:用 provisioning(ConfigMap)方式定義的 datasource,改完必須重啟 Grafana Pod 才會生效。 這牽涉到兩種不同的管理模式:

flowchart TB
    subgraph P["方法1:Provisioning 模式(本文用的)"]
        C["改 ConfigMap"] --> K["K8s 自動同步檔案進 Pod"]
        K --> X["❌ 但 Grafana 開機才讀,不會主動偵測"]
        X --> R["必須 kubectl rollout restart deployment grafana"]
    end
    subgraph U["方法2:UI/Database 模式"]
        UI["在 UI 手動新增 datasource"] --> DB["存進內部 SQLite (grafana.db)"]
        DB --> OK["✅ 即時生效,無需重啟"]
    end
  • Provisioning 模式:Grafana 把掛進來的檔案當「唯讀」,只在啟動那一刻讀取。所以改了 ConfigMap 後,得 kubectl rollout restart deployment grafana 讓它重讀。
  • UI 模式:在介面上手動新增的 datasource 會存進 SQLite,Grafana 內部機制會即時偵測,**不用重啟。

💡 進階解法:Grafana Sidecar
如果你真的很介意重啟,社群有個熱門的 grafana-sidecar,它跟 Grafana 一起跑,專門監聽 ConfigMap 變動,一有更新就打 HTTP API 通知 Grafana「快刷新!」,這樣就不用重啟 Pod。

✅ 驗證 + 匯入現成儀表板

先確認 Grafana 活著:

1
2
kubectl -n monitoring exec deploy/grafana -- \
wget -qO- "http://localhost:3000/api/health"

接著開瀏覽器進 http://grafana.localhost(帳密 admin / admin)。你不用自己從零畫圖——Grafana 社群有海量現成儀表板,用 import ID 一鍵匯入即可:

Dashboard ID 內容
1860 Node Exporter Full(主機指標大全,最經典)
13332 kube-state-metrics
13473 Kubernetes 叢集總覽

進 Grafana → Dashboards → New → Import,填入 ID 1860,資料來源選 Prometheus,你就會看到一整片 CPU、記憶體、磁碟的漂亮儀表板——而且因為 Grafana 有 PVC,這些 import 進來的儀表板重啟也不會消失

完整回顧:六步串起來

我們從頭到尾走了一遍,把六個 manifest 的角色與依賴關係串成一張總圖:

flowchart TB
    subgraph 指標層["指標來源"]
        NE["① node-exporter
DaemonSet + Headless Service"] KSM["② kube-state-metrics
RBAC + Deployment + Service"] end subgraph 收集層["收集與儲存"] RBAC["③ Prometheus RBAC
服務發現權限"] CFG["④ Prometheus Config
relabel 自動發現"] PROM["⑤ Prometheus
抓取/儲存/UI (emptyDir)"] RBAC --> PROM CFG --> PROM end subgraph 視覺層["視覺化"] GRAF["⑥ Grafana
provisioning + PVC"] end NE -->|annotation 被發現| PROM KSM -->|annotation 被發現| PROM PROM -->|datasource| GRAF

部署順序建議:指標來源(①②)→ 權限與設定(③④)→ Prometheus(⑤)→ Grafana(⑥)。如果你已經把六個檔案都存好了,也可以一次全部套用(kubectl apply 會自動處理同一目錄下的檔案,順序不影響最終結果):

1
2
3
4
5
6
7
8
9
10
11
kubectl create namespace monitoring    # 若尚未建立

kubectl apply -f node-exporter.yaml \
-f kube-state-metrics.yaml \
-f prometheus-rbac.yaml \
-f prometheus-config.yaml \
-f prometheus.yaml \
-f grafana.yaml

# 或者把六個檔案放在同一資料夾,一行搞定:
# kubectl apply -f ./monitoring/

全部套用後,用 Step 5 的 targets 驗證指令確認 5 個 target 全 up,再進 Grafana import 1860,一套完整的監控系統就上線了。

結語

恭喜你!你剛剛不是「安裝」了一套監控系統,而是真正理解了它的每一塊:

  • 🌡️ node-exporter 用 DaemonSet + headless Service,一台不漏地收集主機指標。
  • 📋 kube-state-metrics 靠 RBAC 拿到唯讀權限,輸出 K8s 物件狀態。
  • 🔍 Prometheus 用服務發現 + relabel_configs 四段魔法,零維護地自動追蹤所有貼了 annotation 的服務。
  • 📈 Grafana 用 provisioning 自動接好資料來源,用 PVC 保住你的儀表板。

最值得帶走的一課是那套 annotation 自動發現機制:以後你要監控任何新服務,完全不用碰 Prometheus 設定檔,只要在它的 Service 貼上三行 annotation:

1
2
3
4
5
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"

Prometheus 就會自動把它納入監控。這種「解耦」的設計思維,正是雲原生系統優雅的地方。動手把它跑起來,然後試著監控你自己的服務吧!🚀


附錄:完整 manifests(可直接複製部署)

前面各章節為了逐段講解,把 YAML 拆開了。這裡附上每個檔案的完整版,可以直接複製存檔部署。建議依序 kubectl apply -f 套用(順序同六步:先指標來源、再權限與設定、然後 Prometheus、最後 Grafana)。所有資源都在 monitoring namespace,記得先建立它:

1
kubectl create namespace monitoring

1. node-exporter.yaml(DaemonSet + Headless Service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true
hostPID: true
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.2
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)
ports:
- containerPort: 9100
name: metrics
resources:
requests: { memory: "32Mi", cpu: "50m" }
limits: { memory: "128Mi", cpu: "200m" }
volumeMounts:
- { name: proc, mountPath: /host/proc, readOnly: true }
- { name: sys, mountPath: /host/sys, readOnly: true }
- { name: root, mountPath: /host/root, readOnly: true }
volumes:
- { name: proc, hostPath: { path: /proc } }
- { name: sys, hostPath: { path: /sys } }
- { name: root, hostPath: { path: / } }
---
apiVersion: v1
kind: Service
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
prometheus.io/path: "/metrics"
spec:
clusterIP: None # headless
selector:
app: node-exporter
ports:
- name: metrics
port: 9100
targetPort: 9100

2. kube-state-metrics.yaml(RBAC + Deployment + Service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-state-metrics
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-state-metrics
rules:
- apiGroups: [""]
resources: [nodes, pods, services, endpoints, namespaces, persistentvolumeclaims]
verbs: ["list", "watch"]
- apiGroups: ["apps"]
resources: [deployments, replicasets, statefulsets, daemonsets]
verbs: ["list", "watch"]
- apiGroups: ["batch"]
resources: [jobs]
verbs: ["list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: [ingresses]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kube-state-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-state-metrics
subjects:
- kind: ServiceAccount
name: kube-state-metrics
namespace: monitoring
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-state-metrics
namespace: monitoring
labels:
app: kube-state-metrics
spec:
replicas: 1
selector:
matchLabels: { app: kube-state-metrics }
template:
metadata:
labels: { app: kube-state-metrics }
spec:
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.13.0
args:
- --resources=nodes,pods,deployments,replicasets,daemonsets,statefulsets,services,endpoints,ingresses,namespaces,jobs,persistentvolumeclaims
ports:
- { containerPort: 8080, name: metrics }
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests: { memory: "64Mi", cpu: "50m" }
limits: { memory: "256Mi", cpu: "200m" }
---
apiVersion: v1
kind: Service
metadata:
name: kube-state-metrics
namespace: monitoring
labels:
app: kube-state-metrics
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
selector:
app: kube-state-metrics
ports:
- name: metrics
port: 8080
targetPort: 8080

3. prometheus-rbac.yaml(ServiceAccount + ClusterRole + Binding)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prometheus
rules:
- apiGroups: [""]
resources: ["nodes", "nodes/metrics", "services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring

4. prometheus-config.yaml(ConfigMap,完整 relabel_configs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

- job_name: kubernetes-service-endpoints
kubernetes_sd_configs:
- role: endpoints
namespaces:
names: ["monitoring"]
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
regex: "true"
action: keep
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
regex: "(.+)"
target_label: __metrics_path__
action: replace
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
regex: "([^:]+)(?::\\d+)?;(\\d+)"
replacement: "$1:$2"
target_label: __address__
action: replace
- source_labels: [__meta_kubernetes_service_label_app]
target_label: job
- source_labels: [__meta_kubernetes_pod_node_name]
target_label: node
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod

5. prometheus.yaml(Deployment + Service + Ingress)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: monitoring
labels: { app: prometheus }
spec:
replicas: 1
selector:
matchLabels: { app: prometheus }
template:
metadata:
labels: { app: prometheus }
spec:
serviceAccountName: prometheus
containers:
- name: prometheus
image: prom/prometheus:v2.54.1
args:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=7d
- --web.enable-lifecycle
ports:
- { containerPort: 9090, name: web }
readinessProbe:
httpGet: { path: /-/ready, port: 9090 }
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests: { memory: "256Mi", cpu: "100m" }
limits: { memory: "512Mi", cpu: "500m" }
volumeMounts:
- { name: config, mountPath: /etc/prometheus }
- { name: data, mountPath: /prometheus }
volumes:
- name: config
configMap: { name: prometheus-config }
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: monitoring
spec:
selector:
app: prometheus
ports:
- name: web
port: 9090
targetPort: 9090
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prometheus
namespace: monitoring
spec:
ingressClassName: traefik
rules:
- host: prometheus.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prometheus
port: { number: 9090 }

6. grafana.yaml(Secret + PVC + ConfigMap + Deployment + Service + Ingress)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
apiVersion: v1
kind: Secret
metadata:
name: grafana-admin
namespace: monitoring
type: Opaque
stringData:
admin-user: admin
admin-password: admin # ⚠️ dev 用途,正式環境務必改掉
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-data
namespace: monitoring
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests: { storage: 1Gi }
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources
namespace: monitoring
data:
datasources.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
labels: { app: grafana }
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels: { app: grafana }
template:
metadata:
labels: { app: grafana }
spec:
securityContext:
fsGroup: 472
containers:
- name: grafana
image: grafana/grafana:11.2.0
env:
- name: GF_SECURITY_ADMIN_USER
valueFrom: { secretKeyRef: { name: grafana-admin, key: admin-user } }
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom: { secretKeyRef: { name: grafana-admin, key: admin-password } }
ports:
- { containerPort: 3000, name: web }
readinessProbe:
httpGet: { path: /api/health, port: 3000 }
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests: { memory: "128Mi", cpu: "100m" }
limits: { memory: "512Mi", cpu: "500m" }
volumeMounts:
- { name: data, mountPath: /var/lib/grafana }
- { name: datasources, mountPath: /etc/grafana/provisioning/datasources }
volumes:
- name: data
persistentVolumeClaim: { claimName: grafana-data }
- name: datasources
configMap: { name: grafana-datasources }
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
spec:
selector:
app: grafana
ports:
- name: web
port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: monitoring
spec:
ingressClassName: traefik
rules:
- host: grafana.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port: { number: 3000 }

相關連結

  • [[Kubernetes 常用測試指令]] - 本文用到的臨時容器 curl 除錯指令詳解。
  • [[Index]] - 回到知識庫目錄。