iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
AI Engineering

Agent 開發, 從本地到雲端: AI 工程師的 K8s 筆記系列 第 5

Day 05|K8s 三個心智模型(Pod / Deployment / Service),用 bot 艦隊講一遍

  • 分享至 

  • xImage
  •  

cover

POV:手上那份 20 行的 docker compose,第一次要「翻譯」成 K8s,卻不知道 Pod、Deployment、Service 哪一個對應哪一行。

第一週最後一篇。Day 03 貼過 LINE bot 那份真的在跑的 compose(bot 加 cloudflared tunnel,20 行);今天直接把那 20 行翻成 K8s YAML,三個名詞自然會對上位置。

先講結論:三個模型各回答一個問題。Pod 回答「什麼東西要一起跑」,Deployment 回答「要維持幾份、掛了怎麼辦」,Service 回答「別人怎麼找到它」。 下面每節都照這個順序:回答問題、對應 compose、給 YAML、給指令。

Pod:什麼東西要一起跑

它回答什麼問題。 哪幾個 container 要排在同一台機器、共用網路、當成一個單位。

對應 compose 哪幾行。 lineoa-coursecloudflared 這兩個 service:compose 裡是兩個獨立容器,靠 depends_on 管啟動順序;但 tunnel 只服務這隻 bot 的 webhook,離開 bot 就沒意義——這種關係在 K8s 裡就是同一個 Pod 裡的兩個 container,tunnel 當 bot 的 sidecar

其他對應整理在文末表格;這裡多補一個 compose 沒有對應欄位的 allowPrivilegeEscalation: false,防止行程取得更高權限。

最小 YAML。 只放 containers 段,bot 的 volumes 先略過(config.toml 與 codex-home 的去向,最後一節講)。tunnel 的設定檔得先掛進去,不然 cloudflared 一起來就找不到 config.yml 退出;這裡只示範掛法,credentials 的 Secret 省略,照抄還跑不起來。

apiVersion: v1
kind: Pod
metadata:
  name: lineoa-course
  labels: { app: lineoa-course }
spec:
  restartPolicy: Always
  containers:
    - name: bot
      image: ghcr.io/openabdev/openab:0.9.0-beta.10-codex
      ports:
        - containerPort: 8080
      envFrom:
        - secretRef: { name: lineoa-course-env }
      livenessProbe:
        exec: { command: ["pgrep", "-x", "openab"] }
        periodSeconds: 30
      securityContext:
        capabilities: { drop: ["ALL"] }
        allowPrivilegeEscalation: false
    - name: tunnel
      image: cloudflare/cloudflared:latest
      args: ["tunnel", "--config", "/etc/cloudflared/config.yml", "run"]
      volumeMounts:
        - name: tunnel-config
          mountPath: /etc/cloudflared
          readOnly: true
  volumes:
    - name: tunnel-config
      configMap: { name: lineoa-course-tunnel }  # credentials 另掛 Secret,此處省略

metadata.labels 那行現在看起來多餘,Service 那節會用到:Service 靠 label 找 Pod,沒有這行,後面的 selector 就選不到它。

容易寫錯的地方:restartPolicy 是 Pod 層級欄位,重啟卻是各個 container 自己來:tunnel 掛了只重啟 tunnel,不會連帶重啟 bot,反過來也一樣。「一起排程、共享網路」是真的,「一起重啟」不是。

一個指令看它。 kubectl get pod -o wide 看排在哪個節點、拿到什麼 IP;kubectl describe pod lineoa-course 看兩個 container 的重啟次數與 probe 結果。

Deployment:要維持幾份、掛了怎麼辦

它回答什麼問題。 上面那個 Pod 若整個消失,誰補一個新的?沒有人。單獨建的 Pod 沒了就沒了,跟直接 docker run 一個容器沒兩樣。(Deployment 取代上一節那個獨立 Pod,不是疊加;兩段 YAML 別一起套用,否則會留下一個 Deployment 不管、但同樣符合 selector 的獨立 Pod,Service 可能把流量分給兩邊。)

對應 compose 哪幾行。 還是 restart: unless-stopped,這次對應的是它的另一半語意。9/15 早上 dev-box 重開機,13 個 OpenAB 容器在 22 秒內自己回來,靠 Docker daemon 開機後照 restart policy 拉起容器。K8s 把這件事交給 controller:replicas: 1 之下,controller 不停比對「宣告要一個」和「實際有幾個」,差多少補多少,補不起來就一直試。

最小 YAML。 概念上是把 Pod 的 spec 塞進 template;下面這塊本身不能直接套用——篇幅所限只列了 bot,要真的取代前面的 Pod,得把 tunnel、probe、securityContext 從上一段整段搬進 containers 才算完整。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: lineoa-course
spec:
  replicas: 1
  selector:
    matchLabels: { app: lineoa-course }
  template:
    metadata:
      labels: { app: lineoa-course }
    spec:
      restartPolicy: Always
      containers:
        - name: bot
          image: ghcr.io/openabdev/openab:0.9.0-beta.10-codex
          ports:
            - containerPort: 8080
        # tunnel、livenessProbe、securityContext 照上一段 Pod 的寫法補上

replicas 這隻 bot 維持 1,不是不能開兩份,是我猜 codex 狀態放在具名 volume,兩份可能搶同一份——這只是假設,還沒實測過並行行為。

換版時 RollingUpdate 預設會短暫讓兩個 Pod 並存,replicas: 1 管的是穩態,兩者不衝突;真的不能同時碰狀態要改用 strategy.type: Recreate(還沒實測過)。

一個指令看它。 kubectl get deploy 看 READY 是不是 1/1kubectl rollout status deploy/lineoa-course 看換版有沒有卡住。

Service:別人怎麼找到它

它回答什麼問題。 Pod 會被重建,IP 常變;誰要連它,該連哪裡?

對應 compose 哪幾行。 ports: "127.0.0.1:8092:8080" 加上 tunnel,就是這隻 bot「怎麼被找到」的全部設定:容器內聽 8080,host 只綁 loopback 8092,外面的 LINE webhook 走 tunnel 進來。

另一個例子是 devstack:rag-engine:4001 是它自己的服務位址(compose 內部 DNS,同機同專案內有效)。K8s 的 Service 把這種名字升級成叢集範圍:固定名字加內部 IP,後面掛著 Ready 的 Pod(沒設 readinessProbe 就預設 Ready;設了才由它決定收不收流量),像 rag-engine.default.svc,換節點也解析得到。

最小 YAML。

apiVersion: v1
kind: Service
metadata:
  name: lineoa-course
spec:
  selector:
    app: lineoa-course
  ports:
    - port: 8092
      targetPort: 8080

老實說,這隻 bot 可能不需要 Service:tunnel 跟 bot 同 Pod,直接連 localhost:8080 就好,且是主動連出去,沒有 Pod 要找它。Service 真正派上用場的是 devstack 那種互相查找的關係,這隻 bot 要不要留,Day 13 再決定。

一個指令看它。 kubectl get svc 看名字和 ClusterIP;kubectl port-forward svc/lineoa-course 8092:8092 拉到自己電腦,效果跟 compose 綁 loopback 類似。

對照表:compose 欄位 → K8s

compose 欄位 K8s 物件/欄位 備註
restart: unless-stopped Pod spec.restartPolicy: Always + Deployment replicas 不是完全對應:compose 手動 stop 後不會自動起來,Always 是容器退出就一直重啟;replicas 管的是 Pod 消失後補新的
healthcheck livenessProbe 另有 readinessProbe 決定給不給流量
env_file Secret + envFrom.secretRef 非機密的設定可改用 ConfigMap
ports: "127.0.0.1:8092:8080" containerPort + Service port/targetPort 沒有 loopback-only 對應;ClusterIP 預設只在叢集內可達
depends_on 沒有直接對應 compose 是「等容器起來再啟動」;同 Pod 的 container 同時啟動、沒有這種等待,順序得用 init container;跨 Pod 靠應用程式自己重試,readinessProbe 只決定給不給流量
volumesconfig.toml:ro ConfigMap 掛成檔案
volumes(具名 codex-home PersistentVolumeClaim
cap_drop: [ALL] securityContext.capabilities.drop: ["ALL"] 寫在 container 層級

三個模型沒說的事

  • 外面流量怎麼進來。 Service 只是叢集內門牌,外部進來還要 Ingress 或 Gateway;這隻 bot 靠 tunnel 繞過,其他服務不行。
  • 資源 requests 與 limits。 三塊 YAML 都沒寫;bot 在 compose 裡本來沒設 mem_limit,搬過來若也不寫,排程器不知道它要多少,Day 09 專門講。
  • 多節點。 三個模型在單機 k3s 上一樣有用;真正只有多機器才會冒出來的問題,像 Pod 排到哪台、Service 怎麼跨節點找到它,這篇沒碰。

下週開始講資源:AI 工作負載跟一般 web app 差別很大;明天先把我的服務分成三種工作負載,再決定哪一種該用哪個 controller。

今日一句話

Pod 是「什麼東西要一起跑」,Deployment 是「要維持幾份」,Service 是「怎麼被找到」;compose 20 行大部分能對到這三個之一,對不到的(像 depends_on)就是 K8s 多出來要你決定的事。

延伸閱讀


上一篇
Day 04|為什麼 docker compose 還是不夠: 當環境 swap 見底的時候...
下一篇
Day 06|Agent 系統的三種工作負載:long-running bot、burst build、GPU 推論
系列文
Agent 開發, 從本地到雲端: AI 工程師的 K8s 筆記6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言