2022年1月11日
Kubernetes 入門:Pod、Deployment、Service、Ingress 是怎麼把一個請求送到容器的
Kubernetes 在解決什麼問題
用 Docker 把服務包成容器之後,接下來的問題是:
- 要跑 3 份,其中一份掛了,誰負責把它重新開起來?
- 3 份容器的 IP 都不一樣、重開還會變,前面的人要連哪一個?
- 要更新版本時,怎麼一份一份換,而不是全部停掉再開?
- 設定檔、密碼要怎麼給容器,又不要寫死在映像檔裡?
Kubernetes(K8s)做的事情,就是讓你用 YAML 描述「期望的狀態」——「我要 3 份這個版本的 API,對外開 80 Port」——然後由它持續比對實際狀態,不一樣就自動修正。你不再是一步步下指令,而是宣告結果。
一個請求的路徑
從外到內,一個請求會經過:
- Ingress:依網域和路徑,決定要轉給哪個 Service
- Service:一個固定的入口,把請求平均分給後面的 Pod
- Pod:真正在跑容器的地方
- 而 Pod 的數量和版本,由 Deployment 管理
下面由內到外,一個一個看。
Pod:最小的部署單位
Pod 是 K8s 裡最小的單位,裡面放一個(偶爾多個)容器。同一個 Pod 裡的容器共用網路,可以用 localhost 互相連線。
Pod 最重要的特性是:它是用完即丟的。
- Pod 掛了,K8s 不會「修好」它,而是建立一個新的 Pod 取代
- 新的 Pod 會有新的名稱和新的 IP
- Pod 裡寫入的檔案,Pod 被刪掉就沒了
所以兩件事要記住:不要依賴 Pod 的 IP,不要把資料存在 Pod 裡。需要保存的資料,要放到資料庫、物件儲存,或是掛載 PersistentVolume。
實務上幾乎不會直接建立 Pod,而是透過 Deployment。
Deployment 與 ReplicaSet:維持數量、管理版本
Deployment 描述「我要哪個映像檔、跑幾份」:
- 它會建立一個 ReplicaSet,由 ReplicaSet 負責「隨時維持 3 個 Pod」。一個 Pod 被刪掉,ReplicaSet 馬上補一個
- 更新映像檔版本時,Deployment 會建立一個新的 ReplicaSet,逐步把新版本的 Pod 加上去、舊版本的減下來,這就是滾動更新(Rolling Update)
- 新版本有問題,
kubectl rollout undo就能退回上一個 ReplicaSet
一般只需要操作 Deployment,ReplicaSet 由它自動管理。
Label 與 Selector:物件之間怎麼找到彼此
K8s 的物件之間不是用名稱互相指定,而是用 Label(標籤):
- Pod 身上貼著
app: house-api這個 Label - Deployment 用
selector.matchLabels: app: house-api來認領「哪些 Pod 歸我管」 - Service 也用
selector: app: house-api決定「要把流量送給哪些 Pod」
這個設計很有彈性,但也是新手最常出錯的地方:Service 的 Selector 和 Pod 的 Label 只要有一個字不一樣,Service 就找不到任何 Pod,請求一律失敗,而且 YAML 套用時不會有任何錯誤。
Service:固定的入口
Pod 的 IP 會變、數量會變,所以前面需要一個固定的入口,這就是 Service:
- 有一個固定的虛擬 IP(ClusterIP),和一個 DNS 名稱,例如同一個 Namespace 裡直接用
http://house-api就能連到 - 依 Selector 找到所有符合的 Pod,只把請求送給已經就緒(Ready)的 Pod
- Pod 增加、減少、重建時,Service 自動更新後面的清單
Service 有幾種類型:
| 類型 | 誰能連 | 用途 |
|---|---|---|
ClusterIP(預設) | 只有叢集內部 | 服務之間互相呼叫,最常用 |
NodePort | 叢集外,透過任一 Node 的 IP 加固定 Port(30000~32767) | 測試、沒有負載平衡器的環境 |
LoadBalancer | 叢集外,透過雲端建立的負載平衡器 | 在雲端直接對外開放單一服務 |
如果每個要對外的服務都用 LoadBalancer,雲端上就會建立一堆負載平衡器,成本很高。所以通常內部都用 ClusterIP,對外統一經過 Ingress。
Ingress:依網域與路徑分流
Ingress 是 HTTP 層的路由規則:
api.example.com/houses → house-api Service
api.example.com/members → member-api Service
admin.example.com → admin-web Service
要注意,Ingress 只是「規則」,真正處理流量的是 Ingress Controller(例如 NGINX Ingress Controller),它要另外安裝在叢集裡。只寫了 Ingress、卻沒有安裝 Controller,規則不會有任何作用。
TLS 憑證也通常在 Ingress 這一層處理,後面的服務只需要處理 HTTP。
ConfigMap 與 Secret:設定與密碼
設定不應該寫死在映像檔裡,否則每個環境都要建一個不同的映像檔:
- ConfigMap:一般設定,例如外部 API 的網址、功能開關
- Secret:敏感資料,例如資料庫連線字串、API Key
兩者都可以用環境變數或掛載成檔案的方式給 Pod。ASP.NET Core 會自動讀取環境變數,而且 ConnectionStrings__Default 這種用兩個底線的名稱,會對應到設定裡的 ConnectionStrings:Default,不用改程式。
Namespace:分隔環境與團隊
Namespace 把叢集切成多個邏輯區塊,例如 realty、member、monitoring。不同 Namespace 可以有同名的 Service,也可以設定各自的資源配額與權限。
同一個 Namespace 裡用 house-api 就能連到 Service;跨 Namespace 要寫完整名稱 house-api.realty.svc.cluster.local(或簡寫 house-api.realty)。
使用情境:部署一個 .NET API
把物件查詢 API 部署上去,需要的完整 YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: house-api
namespace: realty
spec:
replicas: 3
selector:
matchLabels:
app: house-api # 認領帶有這個 Label 的 Pod
template:
metadata:
labels:
app: house-api # Pod 的 Label,要和上面、和 Service 一致
spec:
containers:
- name: house-api
image: registry.example.com/house-api:1.3.0 # 不要用 latest
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_URLS
value: http://+:8080
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: house-api-secret
key: db-connection
envFrom:
- configMapRef:
name: house-api-config
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 512Mi
readinessProbe:
httpGet:
path: /health/ready
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: house-api
namespace: realty
spec:
selector:
app: house-api
ports:
- port: 80 # Service 對內的 Port
targetPort: 8080 # 轉到容器的 Port
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: house-api
namespace: realty
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /houses
pathType: Prefix
backend:
service:
name: house-api
port:
number: 80
套用與檢查:
kubectl apply -f house-api.yaml
kubectl get pods -n realty -l app=house-api # Pod 有沒有起來
kubectl get endpoints house-api -n realty # Service 有沒有找到 Pod
kubectl describe pod <pod-name> -n realty # 起不來時看 Events
kubectl logs <pod-name> -n realty # 看應用程式的 Log
kubectl get endpoints 是我最常用的除錯指令:如果這裡是空的,代表 Service 沒有找到任何就緒的 Pod,要嘛 Label 對不上,要嘛 Pod 的 Readiness 一直沒通過。
剛開始最常踩到的坑
- Label 對不上:Service 的 Selector、Deployment 的 Selector、Pod Template 的 Label 三者要一致。請求一直失敗時,先看
kubectl get endpoints - 使用
latest標籤:image: house-api:latest無法知道現在跑的是哪一版,也無法退版,而且映像檔在某些拉取策略下不會更新。每次建置都用明確的版本號或 Commit SHA targetPort和容器的 Port 不一致:Service 的targetPort要對到應用程式實際監聽的 Port。.NET 的容器映像檔預設監聽 80,自己改成 8080 時兩邊都要改- Secret 不是加密:Secret 的內容只是 Base64 編碼,任何能讀取它的人都能還原。要控管誰能讀取 Secret,敏感資料也不要把 YAML 直接放進 Git
- 沒有設定 Resource Requests:Scheduler 不知道 Pod 需要多少資源,可能把太多 Pod 塞到同一台 Node 上,大家一起變慢,甚至被系統砍掉
- 沒有 Readiness Probe:容器一啟動就會被 Service 加進清單,應用程式還在初始化,請求就進來了,部署時會出現一波錯誤
- 把資料寫在容器裡:上傳的圖片存在容器的資料夾,Pod 重建就全部消失,而且 3 個 Pod 各自只有一部分的檔案
- Pod 一直重啟(CrashLoopBackOff):先
kubectl logs --previous看上一次當掉前的 Log,通常是設定或連線字串錯誤
一句話總結
Deployment 透過 ReplicaSet 維持 Pod 的數量與版本,Service 用 Label Selector 找到就緒的 Pod 並提供固定的入口,Ingress 依網域與路徑把外部的 HTTP 請求分給不同的 Service,ConfigMap 與 Secret 讓同一個映像檔在不同環境使用不同的設定;物件之間全靠 Label 連結,請求不通時,先檢查 Endpoints 是不是空的。