iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
AI Engineering

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

Day 24|同一個 workload 上 GKE Autopilot:Deployment / Service / Ingress 最小集,兩邊 YAML 對照

  • 分享至 

  • xImage
  •  

cover

POV:昨天一行 gcloud run deploy 搞定的東西,今天變成三個 YAML 物件,你盯著 Ingress 那段想:我到底多做了什麼?

昨天 agent-api 上了 Cloud Run。今天用同一個 image、同一個環境變數,改放到 GKE Autopilot。目的不是比誰好,而是看清楚:同一組需求從「幾個 CLI 旗標」展開成「幾個 K8s 物件」時,多出來的每一行各自在解決什麼。跟昨天一樣,YAML 是對著官方文件整理的,寫這篇時沒有為它重開一個叢集,verified: false。

前置:叢集、憑證、Secret

Autopilot 建叢集只要指定 region,節點的事不用管(Day 22 講過為什麼)。接著把 API key 建成 K8s 原生 Secret,再套 YAML:

gcloud container clusters create-auto agent-cluster --region=asia-east1
gcloud container clusters get-credentials agent-cluster --region=asia-east1

kubectl create namespace agents
kubectl -n agents create secret generic llm-api-key \
  --from-literal=LLM_API_KEY="$LLM_API_KEY"
kubectl -n agents apply -f agent-api.yaml   # 先把 YAML 裡的 PROJECT_ID 換掉再 apply
kubectl -n agents get pods,svc,ingress

原生 Secret 只是 base64,不是加密;正式環境會串 Secret Manager,但那是另一篇的事,今天先用最小集把對照做完。注意這裡跟昨天的差別:昨天 key 放在 Secret Manager,Cloud Run 幫你注入;今天 key 進了叢集,誰有權限讀 Secret 物件(kubectl get secret -o yaml 拿到 base64 就能解回來)誰就看得到,權限邊界從平台移到了叢集裡。

跟錢有關:這組指令會建一個真的會計費的 Autopilot 叢集,實驗做完記得 gcloud container clusters delete agent-cluster --region=asia-east1,只刪 Pod 叢集還是在。這行別貼進部署流程,做完再手動跑。

最小集:Deployment + Service + Ingress

apiVersion: apps/v1
kind: Deployment
metadata: {name: agent-api, namespace: agents}
spec:
  selector: {matchLabels: {app: agent-api}}
  template:
    metadata: {labels: {app: agent-api}}
    spec:
      containers:
      - name: agent-api
        image: asia-east1-docker.pkg.dev/PROJECT_ID/agents/agent-api:v1
        ports: [{containerPort: 8080}]
        envFrom: [{secretRef: {name: llm-api-key}}]
        resources: {requests: {cpu: 250m, memory: 512Mi}, limits: {cpu: 250m, memory: 512Mi}}
        readinessProbe: {httpGet: {path: /healthz, port: 8080}}
---
apiVersion: v1
kind: Service
metadata: {name: agent-api, namespace: agents}
spec: {selector: {app: agent-api}, ports: [{port: 80, targetPort: 8080}]}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: agent-api, namespace: agents}
spec: {defaultBackend: {service: {name: agent-api, port: {number: 80}}}}

先講兩個 apply 完就會炸的事:K8s 不會替 YAML 做變數插值,PROJECT_ID 要先換成你的 project、v1 要是 registry 裡真的存在的 tag,不然它就照字面去拉。另外 containerPort 只是宣告,K8s 不會像 Cloud Run 那樣塞 PORT 環境變數進來;agent-api 自己預設聽 8080 所以沒事,你的程式如果只認 PORT,要自己在 env 補一個。

幾個沒寫出來的預設值:replicas 沒寫就是 1;Service 沒寫 type 就是 ClusterIP。readinessProbe 那行是 Day 05 講過的事:Service 只把流量導給 ready 的 Pod,/healthz 回 2xx 或 3xx 之前,這個 Pod 在 Service 眼裡不存在。

Ingress 沒指定 class,GKE 會用內建的 controller 替你開一個外部 Application Load Balancer;kubectl get ingress 等到 ADDRESS 欄出現 IP,就能從外面打 /healthz,要等一陣子,不是 apply 完就有。Service 留 ClusterIP 就好,不用改 NodePort:官方 Ingress 文件寫明,叢集是 VPC-native、沒用 GKE Network Policy、沒關 HttpLoadBalancing add-on 時,GKE 會自動啟用 container-native load balancing,替 Service 加上 NEG annotation,LB 直接打 Pod IP。用預設值建的 Autopilot 叢集照文件應該符合這些條件,但這篇我沒實跑,實跑時如果 backend 一直 UNHEALTHY,先確認 readinessProbe 真的回 2xx,再回頭對這幾個條件。

對照表:昨天的旗標,今天落在哪一行

Cloud Run(Day 23) K8s(今天) 差在哪
--image containers[].image 一模一樣
--port containerPort + Service targetPort 多一層:Service 對外 80、轉給容器的 8080;程式聽哪個埠仍是程式自己的事
--set-secrets envFrom.secretRef 從「平台幫你注入」變成「自己宣告一個 Secret 物件」
--cpu / --memory resources.requests / limits 概念相近;Autopilot 依 requests 計費
--min-instances replicas,或 HPA 的 minReplicas replicas: 0 是合法的手動縮容;缺的是「沒流量就自動縮到 0」,要靠額外元件
--max-instances HPA 的 maxReplicas 沒設 HPA 就是固定 replicas
--concurrency 沒有對應 K8s 不管一個 Pod 同時吃幾個請求,那是程式自己的事
--no-allow-unauthenticated 沒有對應 沒特別設定的 Ingress 開出去就是公開的,認證要自己掛

看出差別了嗎?Cloud Run 先替你決定了執行模型與網路長什麼樣;K8s 把這兩件事拆成物件,交還給你。 少一個 --concurrency,是因為 K8s 不碰請求層;少一個 --no-allow-unauthenticated,是因為認證在 K8s 裡也不是內建的;多出 Service 與 Ingress,是因為「服務怎麼被找到」在 K8s 裡是你的責任。最後一列尤其要注意:昨天 Cloud Run 預設關著,今天這份沒加任何設定的 Ingress 一開就是全世界都打得到。

--min-instances 那列也值得多看一眼。你可以手動把 replicas 設成 0,但原生 K8s 沒有「沒人用就自動縮到 0、有請求再拉起來」的控制器,這要另外裝。對 agent-api 這種 request-driven 服務,那個一直開著的 Pod 是多付的閒置成本;但對 Day 21 講的 always-on bot,這反而是它想要的:一直有一個 Pod 在,不必每次醒來都冷啟動。同一個特性,換一種 workload 就從缺點變優點。

Autopilot 下最該盯的一行

resources.requests。Day 22 講過 Autopilot 是按 Pod 宣告的資源計費,所以 cpu: 250m, memory: 512Mi 不是參考值,是帳單本身;價格以官方定價頁為準。填太大是浪費;填太小的後果要分開講:requests 本身只影響排程與帳單,真正會 OOMKilled 的是容器用量超過 limits.memory(Day 09),而這篇把 limits 對齊 requests,所以 requests 填太小就等於 limit 也太小。另外 Autopilot 有最低 requests 門檻,宣告得比門檻低會被自動拉高,看到帳單跟 YAML 對不上時先想到這件事。

這裡我要拿自己的 dev-box 出來對照:2026-09-16 用 docker inspect 核過,13 隻 OpenAB bot 容器全部沒設 mem_limit(HostConfig.Memory = 0),只有 devstack 那幾個有。在 docker compose 上就這樣一直跑著,因為沒人逼我填。搬到 Autopilot 不一樣:你不填 requests,它就套預設值,然後照預設值跟你收錢。這篇 YAML 我把 limits 的 CPU 與 memory 都直接對齊 requests(官方文件:requests 等於 limits 的 Pod 走 Guaranteed QoS;limits 不寫則看叢集支不支援 bursting),就是想先把「宣告多少、付多少、最多用多少」三件事對齊,再用 Day 15 的 kubectl top 看實際用量回頭調。

明天談 GPU:同樣是 GCP,Cloud Run GPU 跟 GKE GPU node pool 我是怎麼選的。

今日一句話

Cloud Run 的擴縮與資源旗標在 K8s 都找得到落腳處,concurrency 與認證這兩件事則要自己扛;多出來的 Service 與 Ingress 是你為「服務怎麼被找到」扛起的責任,而 Autopilot 上那行 requests 就是帳單。

延伸閱讀


上一篇
Day 23|把一個 agent API 部署到 Cloud Run:secret、min instances、並發設定
下一篇
Day 25|GPU on GCP:Cloud Run GPU(L4)vs GKE GPU node pool,Whisper backend 的選擇脈絡
系列文
Agent 開發, 從本地到雲端: AI 工程師的 K8s 筆記 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言