05 - 安全與排程 (Security & Scheduling)¶
目標:控制「誰能對叢集做什麼」(身分與授權)、「Pod 該被放到哪台節點」(排程)、以及「資源怎麼配給與自動擴縮」。讀完你要能設定 RBAC、調控資源、配置探針與 HPA、用排程機制把 Pod 放到對的地方。
第一部分:身分與授權 (Identity & Authorization)¶
1. Namespace:邏輯隔離的分區¶
第 1 章提過 Namespace 是「叢集內的邏輯分區」。它的價值:
- 資源分組:把
dev、staging、prod,或不同團隊的資源隔開。 - 名稱作用域:同一個 Namespace 內名字不能重複,不同 Namespace 可同名。
- 權限與配額的邊界:RBAC 與資源配額 (ResourceQuota) 常以 Namespace 為單位套用。
kubectl create namespace dev
kubectl get ns
kubectl get pods -n dev # -n 指定命名空間
kubectl config set-context --current --namespace=dev # 設定預設命名空間,省得每次打 -n
注意:Namespace 是邏輯隔離,不是網路或安全的硬牆。預設 Pod 仍可跨 Namespace 互連——要真的隔離網路得用 NetworkPolicy(第 3 章)。
2. 認證 vs 授權:兩個不同的問題¶
回到第 1 章:API Server 處理每個請求時會問兩個問題:
- 認證 (Authentication):你是誰? — 驗證身分(憑證、token)。
- 授權 (Authorization):你能做這件事嗎? — 由 RBAC 決定。
K8s 有兩種「身分」:
| 身分類型 | 給誰用 | 怎麼認證 |
|---|---|---|
| 使用者 (User) | 真人 / 外部系統 | 憑證、OIDC 等(K8s 本身不存使用者) |
| ServiceAccount (SA) | 跑在 Pod 裡的程式 | 自動掛載的 token |
關鍵差異:User 由叢集外部管理(K8s 沒有「使用者」這個物件);ServiceAccount 是 K8s 物件,專給叢集內的工作負載用。
3. ServiceAccount:給 Pod 的身分¶
每個 Pod 都會有一個 ServiceAccount(沒指定就用該 Namespace 的 default)。它的 token 會被自動掛進 Pod,讓 Pod 內的程式能呼叫 API Server——例如一個需要列出 Pod 的控制器程式。
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: dev
---
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
serviceAccountName: app-sa # 指定這個 Pod 用哪個 SA
containers:
- name: app
image: my-app:1.0
安全建議:不要讓應用 Pod 用 default SA 並給它過大權限。 為需要呼叫 API 的應用建專屬 SA,只授予剛好夠用的權限(最小權限原則)。不需要呼叫 API 的 Pod,可關掉自動掛 token(
automountServiceAccountToken: false)。
4. RBAC:基於角色的存取控制 (Role-Based Access Control)¶
RBAC 用四種物件,核心是兩兩配對的「角色 + 綁定」:
| 物件 | 作用範圍 | 定義什麼 |
|---|---|---|
| Role | 單一 Namespace | 一組「可對哪些資源做哪些動作」的權限 |
| ClusterRole | 整個叢集 | 同上,但跨 Namespace / 含叢集級資源 |
| RoleBinding | 單一 Namespace | 把 Role(或 ClusterRole)綁給某身分 |
| ClusterRoleBinding | 整個叢集 | 把 ClusterRole 綁給某身分,全叢集生效 |
心法:Role 定義「能做什麼」,Binding 定義「誰能做」。 兩者分開,讓同一組權限可以重複綁給不同身分。
flowchart LR
SUBJ["主體 Subject<br/>(User / ServiceAccount / Group)"]
RB[RoleBinding]
R["Role<br/>(權限規則)"]
SUBJ -->|被綁定| RB
RB -->|引用| R
R -->|授予| ACT["對 pods 可 get/list/watch"]
4.1 範例:讓某 SA 只能讀 dev 命名空間的 Pod¶
# Role:定義「能做什麼」——只能讀 pods
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: dev
name: pod-reader
rules:
- apiGroups: [""] # "" 代表核心 API 群組(Pod 在這裡)
resources: ["pods"]
verbs: ["get", "list", "watch"] # 只允許讀,不能 create/delete
---
# RoleBinding:定義「誰能做」——把上面的 Role 綁給 app-sa
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: dev
name: read-pods
subjects:
- kind: ServiceAccount
name: app-sa
namespace: dev
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
# 用 auth can-i 驗證權限(超實用,CKA 必會)
kubectl auth can-i list pods --as=system:serviceaccount:dev:app-sa -n dev # yes
kubectl auth can-i delete pods --as=system:serviceaccount:dev:app-sa -n dev # no
Role vs ClusterRole 怎麼選? 權限只在單一 Namespace → Role。需要跨 Namespace、或操作叢集級資源(nodes、persistentvolumes、namespaces 本身)→ ClusterRole。常見技巧:用 ClusterRole 定義「可重用的權限集」,再用 RoleBinding 把它限縮綁到特定 Namespace。
第二部分:資源管理與健康 (Resources & Health)¶
5. 資源 requests 與 limits¶
每個容器可以宣告對 CPU 與記憶體的需求,這是叢集調度與穩定性的基礎。
resources:
requests: # 「至少要保留給我」——Scheduler 依此找有空位的節點
cpu: "250m" # 250 millicores = 0.25 顆 CPU
memory: "256Mi"
limits: # 「最多用到這」——超過會被限制
cpu: "500m"
memory: "512Mi"
兩者的角色完全不同:
- requests 影響排程:Scheduler 把 requests 加總,確保節點剩餘容量裝得下,才把 Pod 放上去。它是「保證會保留的量」。
- limits 影響執行限制:
- CPU 超過 limit → 被節流 (throttled),變慢但不會死。
- 記憶體超過 limit → 被 OOMKilled(直接殺掉容器),因為記憶體不可壓縮。
這就是為什麼記憶體沒設好很容易看到
OOMKilled,而 CPU 不足只是變慢。CKA / 線上除錯常見題。
5.1 服務品質等級 (QoS Classes)¶
K8s 依 requests/limits 設定把 Pod 分成三級,節點記憶體不足要驅逐 Pod 時,先犧牲低等級的:
| QoS 等級 | 條件 | 被驅逐優先序 |
|---|---|---|
| Guaranteed | 每個容器 requests == limits(CPU 與記憶體都設且相等) | 最後才動(最穩) |
| Burstable | 有設 requests,但不滿足 Guaranteed | 中間 |
| BestEffort | 完全沒設 requests/limits | 最先被驅逐(最不穩) |
重要服務想要最穩,就把 requests 設成等於 limits,進入 Guaranteed 等級。
5.2 用配額管住整個 Namespace¶
# ResourceQuota:限制整個 dev 命名空間的資源總量上限
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
pods: "20"
---
# LimitRange:為沒寫 requests/limits 的容器自動補上預設值
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: dev
spec:
limits:
- default: # 預設 limit
cpu: "500m"
memory: 256Mi
defaultRequest: # 預設 request
cpu: "100m"
memory: 128Mi
type: Container
6. 探針 (Probes):讓 K8s 知道容器的死活與就緒¶
容器「在跑」不代表「健康」或「能接客」。探針讓 kubelet 主動探測:
| 探針 | 問什麼 | 失敗時 | 用途 |
|---|---|---|---|
| livenessProbe(存活) | 還活著嗎? | 重啟容器 | 偵測死鎖/卡死,救回卡住的程式 |
| readinessProbe(就緒) | 能接流量了嗎? | 從 Service Endpoints 移除(不殺) | 暫時不可用時先停止導流(回想第 3 章) |
| startupProbe(啟動) | 啟動完成了嗎? | 重啟容器 | 給慢啟動的程式緩衝,期間不跑 liveness |
spec:
containers:
- name: app
image: my-app:1.0
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # 啟動後等 10 秒才開始探
periodSeconds: 10 # 每 10 秒探一次
failureThreshold: 3 # 連續失敗 3 次才判定不健康
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
startupProbe: # 慢啟動程式:最多給 30*10=300 秒啟動
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
liveness vs readiness 最關鍵的區別:liveness 失敗會重啟容器(以為它壞了);readiness 失敗只是暫時不導流量(它沒壞,只是還沒準備好,例如正在載入快取)。把兩者搞反——例如把「暫時忙碌」當成 liveness 失敗——會導致容器被無謂地一直重啟。
探針類型除了
httpGet,還有tcpSocket(能不能建立 TCP 連線)與exec(執行指令看回傳碼)。
7. HPA:水平自動擴縮 (Horizontal Pod Autoscaler)¶
HPA 依據指標(最常見是 CPU 使用率)自動增減 Deployment 的副本數。流量高就多開 Pod,流量低就收。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef: # 要自動擴縮哪個工作負載
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2 # 至少維持 2 個
maxReplicas: 10 # 最多開到 10 個
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 目標:平均 CPU 維持在 requests 的 70%
# HPA 需要 metrics-server 提供指標(minikube 用 addon 啟用)
minikube addons enable metrics-server
kubectl top pods # 確認能看到 CPU/記憶體用量(代表 metrics 通了)
kubectl get hpa -w # 觀察副本數隨負載變化
前提:HPA 的 CPU 百分比是相對於 requests 算的,所以容器一定要設 requests,否則 HPA 算不出使用率。這把第 5 節與這節串起來了。
補充:HPA 改「Pod 數量」(水平);VPA 改「單一 Pod 的 requests/limits」(垂直);Cluster Autoscaler 改「節點數量」。三者解決不同層級。
第三部分:排程 (Scheduling)¶
8. 排程基礎:Pod 怎麼被放到節點上¶
回想第 1 章:Scheduler 經過「過濾 → 評分」幫每個 Pod 選節點。下面這些機制讓你能介入這個決定。
flowchart TB
NEW["新 Pod (未排程)"] --> FILTER["過濾:刷掉不符合的節點<br/>(資源不足、taint 擋住、nodeSelector 不符)"]
FILTER --> SCORE["評分:對剩下節點打分"]
SCORE --> BIND["綁定到最高分節點"]
9. 主動「吸引」:nodeSelector 與 Affinity¶
這類機制是 Pod 主動表達「我想去哪種節點」。
9.1 nodeSelector:最簡單的硬性指定¶
9.2 nodeAffinity:更有彈性的節點偏好¶
nodeSelector 只能「硬性、全等」。Affinity 支援「硬性必須 (required)」與「軟性偏好 (preferred)」、以及更豐富的運算子。
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬性:不滿足就排不上去
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values: ["ssd", "nvme"]
preferredDuringSchedulingIgnoredDuringExecution: # 軟性:盡量,但不行也接受
- weight: 50
preference:
matchExpressions:
- key: zone
operator: In
values: ["zone-a"]
9.3 podAffinity / podAntiAffinity:依「其他 Pod」決定位置¶
- podAffinity:想跟某些 Pod 放近一點(例如跟快取放同節點降延遲)。
- podAntiAffinity:想跟某些 Pod 分開(例如同一個服務的副本分散到不同節點,避免單點故障)。
spec:
affinity:
podAntiAffinity: # 讓同 app 的副本盡量分散到不同節點(高可用)
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname # 以「節點」為分散單位
10. 主動「排斥」:汙點與容忍 (Taints & Tolerations)¶
Affinity 是 Pod 挑節點;Taint 則是節點挑 Pod——反過來的方向。
- 汙點 (Taint) 打在節點上,意思是「預設排斥所有 Pod,除非該 Pod 明確容忍我」。
- 容忍 (Toleration) 寫在Pod上,意思是「我能忍受帶這個汙點的節點」。
直覺:Taint 是節點門口貼的「閒人勿入」告示;Toleration 是 Pod 手上的「通行證」。有通行證不代表一定會進去(那是 affinity 的事),只代表「不會被這個告示擋住」。
# 給節點打汙點:effect 有三種
kubectl taint nodes worker-1 dedicated=gpu:NoSchedule
# NoSchedule — 不容忍的 Pod 不准排上來(已在上面的不動)
# PreferNoSchedule — 盡量別排上來(軟性)
# NoExecute — 不容忍的 Pod 連已經在跑的也會被驅逐
# 移除汙點(最後加減號)
kubectl taint nodes worker-1 dedicated=gpu:NoSchedule-
# Pod 上的容忍:有了這張通行證,才可能被排到帶該汙點的節點
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
經典用途:
- 專用節點:給 GPU 節點打汙點,只有需要 GPU 且帶對應容忍的 Pod 能上去,一般 Pod 不會浪費這些昂貴節點。
- 控制平面節點:control-plane 節點預設帶汙點
node-role.kubernetes.io/control-plane:NoSchedule,所以你的應用 Pod 不會被排到大腦上。DaemonSet(第 2 章)若要連 control-plane 也跑,就得加容忍。
Affinity 與 Taint/Toleration 對照¶
| 機制 | 誰主動 | 語意 | 沒滿足時 |
|---|---|---|---|
| nodeSelector / nodeAffinity | Pod 挑節點 | 我「想要」這種節點 | required 排不上;preferred 換別的 |
| Taint / Toleration | 節點拒 Pod | 我「排斥」沒通行證的 Pod | 被擋下 / 被驅逐 |
兩者常搭配使用:用 Taint 把節點圈起來「保留」,再用 nodeAffinity 把目標 Pod「吸引」過去。只有容忍是不夠的(那只是允許,不保證會去)。
其他排程手段(知道即可)¶
nodeName:直接寫死 Pod 跑哪台,跳過 Scheduler(除錯用,不建議常用)。- Topology Spread Constraints:更精細地控制 Pod 跨可用區/節點的均勻分布。
- Pod Priority & Preemption:高優先序 Pod 可在資源不足時擠掉低優先序 Pod。
動手練習¶
- 建
dev命名空間與一個 ServiceAccount,寫 Role + RoleBinding 讓它只能 list pods,用kubectl auth can-i ... --as=...驗證能讀不能刪。 - 給一個 Pod 設定 requests==limits 進入 Guaranteed,另一個完全不設進入 BestEffort,用
kubectl describe pod看 QoS Class 欄位。 - 故意把容器記憶體 limit 設很小並讓它吃記憶體,觀察
OOMKilled。 - 為一個服務設 liveness(指向不存在的路徑)觀察它一直重啟,再修好;設 readiness 觀察未就緒時不被列入 Endpoints。
- 啟用 metrics-server,對一個 Deployment 設 HPA,用壓力工具灌 CPU,
kubectl get hpa -w看副本自動增加再回落。 - (多節點)給一個節點貼標籤,用 nodeSelector 把 Pod 指過去;再給節點打
NoSchedule汙點,觀察沒容忍的 Pod 變Pending,加上容忍後排上去。
本章檢核點 (Checklist)¶
- [ ] 能說明 Namespace 是邏輯隔離,並用它分組資源與設定預設命名空間
- [ ] 能區分認證與授權,以及 User 與 ServiceAccount 的差異
- [ ] 能為應用建專屬 ServiceAccount 並理解最小權限原則
- [ ] 能寫 Role + RoleBinding 限制權限,並用
kubectl auth can-i驗證 - [ ] 能說明 Role 與 ClusterRole / RoleBinding 與 ClusterRoleBinding 的選用時機
- [ ] 能解釋 requests 影響排程、limits 影響執行限制,以及記憶體超限會 OOMKilled
- [ ] 能說出 Guaranteed / Burstable / BestEffort 三種 QoS 與驅逐優先序
- [ ] 能正確配置 liveness / readiness / startup 三種探針並說明失敗後行為差異
- [ ] 能設定 HPA 並理解它依賴 metrics-server 與容器的 requests
- [ ] 能用 nodeSelector / nodeAffinity / podAntiAffinity 控制 Pod 落點
- [ ] 能解釋汙點與容忍的方向(節點排斥 Pod),三種 effect 的差異,以及與 affinity 的搭配
- [ ] 理解「容忍只是允許、不保證會去」,需搭配 affinity 才能真正吸引 Pod
你已完成 Kubernetes 的核心主線(1–5 章)。接下來三章是緊接其後的深化,建議一併讀完再進入 EKS: - 下一章:06-pod-lifecycle.md — Pod 從生到死的完整生命週期與優雅關閉(接續本章的探針與 OOMKilled)。 - 07-security-context.md — 容器安全與加固(本章管「誰能操作叢集」,第 7 章管「容器以什麼身分、能做什麼」)。 - 08-helm-debug-observability.md — Helm/Kustomize、進階除錯與可觀測性。
全部走完後,回到 README.md 對照整體檢核點,就可以挑戰 CKA 或前進 EKS 階段。