iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
DevOps

30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記系列 第 23

【Day23】從 Init Container 到 Sidecar:Pod 進階設計實戰

  • 分享至 

  • xImage
  •  

gh

前情提要

昨天我們實戰 Ingress,理解了如何透過 Host-BasedPath-Based 路由,將外部流量智慧地導向 Cluster 內不同的 Service,解決了 NodePort 的侷限,並讓我們對 Kubernetes 南北向流量 (North-South Traffic) 管理有更清楚的認識。

不過,當服務之間的流量都解決了,我們得回到 Kubernetes 的最小部署單位 Pod 本身從 【Day03】Kubernetes 最小單位 Pod:不只是容器這麼簡單 開始,我們就知道一個 Pod 其實可以同時運行多個 Container。如果主應用程式啟動前需要一些前置檢查,或是需要輔助容器處理日誌與連線代理,就不能只靠單一容器完成。

查了一些 CKAD 考試心得後發現,Multi-Container Pod 幾乎是必考題,考點就是如何透過 Init ContainerSidecar 模式解決實務問題。所以,今天我們就來好好看看 Init ContainerMulti-Container 的設計模式,並透過實戰來加深印象。

為何需要 Multi-Container 設計?

前幾天的實戰裡,我們一直維持「一個容器跑一個進程」的原則,這讓架構簡單、模組化。但實務上會遇到兩類難題:

  1. 啟動前的依賴問題:主程式啟動前,可能需要等待資料庫先起來、下載設定檔或初始化系統參數。
  2. 執行中的輔助任務:主程式在跑時,可能需要另一個容器幫忙,例如:處理 Log、代理網路流量,或轉換格式。

對應的解法,就是在 Pod 中引入 Init ContainerSidecar / Adapter / Ambassador 這些設計模式。

Init Container (初始化容器)

Init Container 是為了解決「啟動前」的需求而設計。顧名思義,它是在 Pod 的主應用容器 (App Container) 啟動之前 運行的專用容器,其主要任務就是完成我們所設定的各種前置準備工作。它們的特點是:

  • 依序執行:有多個 Init Container 時,必須一個完成後才會啟動下一個。
  • 成功才繼續:只有當所有 Init Container 都成功執行完畢,主容器才會啟動。
  • 失敗會重試:若任何一個 Init Container 失敗,Pod 會依 restartPolicy 重啟,直到成功為止。

這些特性,讓 Init Container 非常適合進行環境檢查與初始化工作。

Multi-Container Pod 設計模式 (Sidecar, Adapter, Ambassador)

對於需要「同步運行」的輔助任務,就要用 Multi-Container 模式,最常見的就是 Sidecar,還有 AdapterAmbassador

Sidecar

Sidecar 就像摩托車旁的邊車,主容器是摩托車,負責核心邏輯;而 Sidecar Container 則是邊車,負責提供輔助功能。它與主容器 並行運行,共享生命週期、網路和儲存。這種模式的強大之處在於,它可以在不侵入主應用的前提下,擴展或增強其功能,完美符合 職責分離 的原則。

一個常見的應用就是 日誌收集。假設有一個舊的應用程式,它習慣將 access log 寫入本地檔案 (例如 /var/log/app.log),而不是現代雲原生應用推薦的標準輸出 (stdout)。這會導致 kubectl logs 指令完全失效,因為它只能抓取 stdout 的內容。

Sidecar 的運作方式

  1. 在 Pod 中建立一個共享的 emptyDir Volume
  2. 主應用容器 (main-app) 照常運行,將共享 Volume 掛載到它的日誌路徑 /var/log
  3. Sidecar 容器(例如輕量的 busybox)同時運行,掛載同一個共享 Volume
  4. Sidecar 執行 tail -f /var/log/app.log,持續讀取日誌並輸出到自己的 stdout

如此一來,我們就可以透過 kubectl logs <pod-name> -c <sidecar-container-name> 間接看到主應用程式的日誌,完全不需要修改原本的應用程式。

Adapter

Adapter 容器就像一個「翻譯官」,讓主應用程式可以繼續用自己最熟悉的方式輸出資訊,而由 Adapter 負責將這些資訊轉換成外界監控系統看得懂的標準格式。

假設主應用程式(例如 Java)輸出的監控指標是 JMX 格式,但外部的 Prometheus 監控系統只認得自己的標準格式。

Adapter 的運作方式

  1. java-app 容器照常運行,在 localhost 的某個 Port 上暴露 JMX 指標
  2. jmx_exporter (Adapter Container) 同時運行,存取 localhostjava-app 的 JMX Port
  3. jmx_exporter 將 JMX 格式指標轉換 (Adapt) 成 Prometheus 格式
  4. 最後在自己的 Port (例如 9113) 上暴露轉換後的指標

外部的 Prometheus 系統只需要抓取 Adapter Container 的 9113 Port 即可。

Ambassador

Ambassador 模式將與外部服務溝通的複雜邏輯從主應用中抽離出來。主應用程式只需要簡單地與 localhost 上的 Ambassador Container 通訊。

假設主應用程式需要連線到外部資料庫,而連線位址在不同環境(開發、生產)中都不同。如果把這些複雜的連線邏輯、重試機制、故障轉移都寫在主應用程式裡,程式碼會變得非常笨重且複雜。

Ambassador 的運作方式

  1. main-app 的資料庫連線位址寫死localhost:5432
  2. ambassador Container 監聽 localhost:5432
  3. main-app 嘗試連線時,實際上是連到同一 Pod 內的 ambassador Container
  4. ambassador 根據設定(透過環境變數或 ConfigMap),將請求轉發到真正的外部資料庫

主應用程式所有對外的溝通都只需要跟 Ambassador Container 交談,完美地解除了主應用與外部環境的耦合。

實戰 🔥

Init Container

首先,我們來看一個需要修改系統參數的例子。這模擬了某些資料庫(如 PostgreSQL、Elasticsearch)啟動前需要調整系統參數的場景:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: ubuntu
  name: ubuntu
spec:
  replicas: 1
  selector:
    matchLabels:
      run: ubuntu
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: ubuntu
    spec:
      containers:
      - image: ubuntu
        name: ubuntu
        resources: {}
        command: ['sh', '-c', 'tail -f /dev/null']
      initContainers:
        - name: init-sysctl
          image: busybox
          command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
          securityContext:
            privileged: true
status: {}

執行後可以看到 Status 會顯示 Init Container 的進程與數量,Init Container 執行成功後,主應用程式的 Container 才會運行起來。

gh

接下來看一個 Init 失敗的例子。這裡設定要存取 Service,但暫時不建立 Service,就會在 Initialize 階段一直失敗,類似 Web 應用需要依賴 Database 的場景:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

因為 Init Container 無法成功,Pod 會一直無法啟動:

gh

加上對應的 Service 後:

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9377

隨著 Service 建立起來,Initialize 成功後,Pod 就成功建立了:

gh

Sidecar

我們來實戰日誌收集的 Sidecar 模式。首先建立一個沒有 Sidecar 的 Deployment:

# 建立 Deployment
kubectl create deployment game2048-logging --image=naketa/2048game

# 建立 Service (因為我是 KinD 所以沒特別設定要用 Node Port)
kubectl expose deployment game2048-logging --port=80

# Port Forward (雲端防火牆開放 Port 30123)
kubectl port-forward --address 0.0.0.0 services/game2048-logging 30123:80

2048 遊戲成功運行:

gh
gh

使用 kubectl logs 查看時,發現沒有任何輸出,因為這個 Image 沒有做標準輸出的導出:

kubectl logs game2048-logging-764746bb59-w7qgr

gh

但進入 Container 可以看到日誌檔案確實存在:

kubectl exec -it game2048-logging-764746bb59-w7qgr -- /bin/sh -c "tail /var/log/nginx/access.log"

gh

現在加上 Sidecar Container,使用共享 Volume 來收集日誌:

kubectl create deployment game2048-logging --image=naketa/2048game --dry-run=client -o yaml > game2048-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: game2048-logging
  name: game2048-logging
spec:
  replicas: 1
  selector:
    matchLabels:
      app: game2048-logging
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: game2048-logging
    spec:
      containers:
      - image: naketa/2048game
        name: 2048game-logging
        imagePullPolicy: Always
        volumeMounts:
        - mountPath: /var/log/nginx
          name: shared-log
      - image: busybox:1.28
        name: busybox-logging
        args:
        - /bin/sh
        - -c
        - tail -fn+1 /var/log/nginx/access.log
        imagePullPolicy: Always
        volumeMounts:
        - mountPath: /var/log/nginx
          name: shared-log
      volumes:
      - emptyDir: {}
        name: shared-log
status: {}

部署後可以看到 Pod 裡面現在有兩個 Container Ready:

gh

現在可以透過 Sidecar Container 查看日誌了 (使用 -c 指定容器名稱):

kubectl logs game2048-logging-77c447b96c-vw25q -c busybox-logging

gh

Adapter

接下來實戰 Adapter 模式,將 Nginx 的狀態透過 Adapter Container 轉換成 Prometheus 格式的 Metrics:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx-adapter
  name: nginx-adapter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-adapter
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx-adapter
    spec:
      containers:
      - image: nigelpoulton/nginxadapter:1.0
        name: nginxadapter
      - name: transformer
        image: nginx/nginx-prometheus-exporter
        args: ["-nginx.scrape-uri","http://localhost/nginx_status"]
        ports:
        - containerPort: 9113
status: {}

建立 Service 並 Port Forward:

# 將 Deployment 的 9113 Port 對應 Service 的 80 Port
kubectl expose deployment nginx-adapter --port 80 --target-port 9113

# Port Forward (雲端防火牆開放 Port 30123)
kubectl port-forward --address 0.0.0.0 services/nginx-adapter 30123:80

成功將 Nginx 的 Metrics 轉換成 Prometheus 格式:

gh
gh

如果沒有使用 Adapter Container

  • Nginx 原生只提供簡單的 /nginx_status 頁面,輸出的是純文字格式的狀態資訊
  • Prometheus 無法直接理解這種格式,需要額外的 exporter 來轉換
  • 若要讓 Prometheus 能夠監控,就必須修改 Nginx 的 Image,安裝 exporter 模組,或是在外部另外部署一個轉換服務
  • 這樣會讓架構變得複雜,且違反了單一職責原則

透過 Adapter Container,我們完全不需要修改原本的 Nginx Image,就能優雅地提供 Prometheus 相容的監控端點。

Ambassador

最後實戰 Ambassador 模式,建立一個主 Container 和 Ambassador Container 來處理外部 API 連線:

apiVersion: v1
kind: Pod
metadata:
  name: themoviedb
  labels:
    app: themoviedb
spec:
  containers:
    - name: main
      image: nginx
    - name: ambassador
      image: startkubernetes/ambassador:0.1.0
      env:
        - name: API_KEY
          valueFrom:
            secretKeyRef:
              name: themoviedb
              key: apikey
      ports:
        - name: http
          containerPort: 8080

建立 Secret 來存放 API Key(需要自行申請 TMDB API Key):

apiVersion: v1
kind: Secret
metadata:
  name: themoviedb
data:
  apikey: <base64-encoded-api-key>  # 需要將你的 API key 進行 base64 編碼

gh

建立完成後,主 Container 可以透過 localhost 來存取外部 API:

kubectl exec -it themoviedb -- curl localhost:8080/movies | jq

成功取得電影資料:

gh

如果沒有使用 Ambassador Container

  • 主應用需要自己處理所有的 API 連線邏輯,包括 API Key 管理、請求重試、錯誤處理等
  • 每次環境變更(開發、測試、生產)都需要修改主應用的配置或程式碼
  • 如果 API 端點改變或需要切換到不同的服務提供者,就必須重新部署主應用
  • API Key 等敏感資訊可能會硬編碼在應用程式中,造成安全風險

透過 Ambassador Container,主應用只需要知道 localhost:8080,所有複雜的外部連線邏輯都被封裝在 Ambassador 中,實現了完美的解耦。

總結

今天我們深入探討了 Kubernetes Pod 的進階設計模式,從 Init ContainerMulti-Container 的三大模式(Sidecar、Adapter、Ambassador),並透過實戰來理解每種模式的應用場景:

  • Init Container 解決了啟動前的依賴問題,確保主應用程式在正確的環境下啟動
  • Sidecar 模式透過共享 Volume,優雅地解決了日誌收集問題
  • Adapter 模式充當轉換器,讓不同格式的監控指標能被標準化
  • Ambassador 模式作為代理,簡化了主應用與外部服務的溝通複雜度

這些設計模式都體現了 Kubernetes 的核心理念,透過將不同職責分配給不同的容器,我們能夠建構出更靈活、可維護的應用架構。然而,隨著應用架構變得越來越複雜,手動編寫和管理這些 YAML 檔案也變得越來越困難。想像一下,如果每個環境(開發、測試、生產)都需要不同的配置,或是需要管理數十個相互依賴的服務,純手工維護 YAML 檔案將會是一場噩夢。

這就是為什麼我們需要 Helm —— Kubernetes 的套件管理工具。明天我們將深入研究 Helm,看看它如何透過 Chart,將複雜的 Kubernetes 應用打包成可重複使用的套件,並提供版本控制、依賴管理和配置管理等強大功能。

下一篇文章:Kubernetes 的套件管理器:Helm 與 Chart 實戰


上一篇
【Day22】Ingress 實戰:從 NodePort 到反向代理
系列文
30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言