← 回文章列表

2022年1月11日

Kubernetes 入門:Pod、Deployment、Service、Ingress 是怎麼把一個請求送到容器的

Kubernetes容器DevOps部署

Kubernetes 在解決什麼問題

用 Docker 把服務包成容器之後,接下來的問題是:

  • 要跑 3 份,其中一份掛了,誰負責把它重新開起來?
  • 3 份容器的 IP 都不一樣、重開還會變,前面的人要連哪一個?
  • 要更新版本時,怎麼一份一份換,而不是全部停掉再開?
  • 設定檔、密碼要怎麼給容器,又不要寫死在映像檔裡?

Kubernetes(K8s)做的事情,就是讓你用 YAML 描述「期望的狀態」——「我要 3 份這個版本的 API,對外開 80 Port」——然後由它持續比對實際狀態,不一樣就自動修正。你不再是一步步下指令,而是宣告結果。

一個請求的路徑

Kubernetes 請求路徑示意圖:使用者的請求先到 Ingress Controller,依 Ingress 規則中的網域 api.example.com 與路徑 /houses 轉給 house-api 這個 Service;Service 有固定的 ClusterIP 與 DNS 名稱,以 Label Selector app=house-api 找到三個 Pod 並做負載平衡;三個 Pod 由 Deployment 透過 ReplicaSet 維持數量,Deployment 指定映像檔版本與副本數;Pod 的設定來自 ConfigMap,密碼來自 Secret;全部位於 realty 這個 Namespace 中

從外到內,一個請求會經過:

  1. Ingress:依網域和路徑,決定要轉給哪個 Service
  2. Service:一個固定的入口,把請求平均分給後面的 Pod
  3. Pod:真正在跑容器的地方
  4. 而 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 層的路由規則:

text
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:

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

套用與檢查:

Shell
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 是不是空的。