
POV:你把 LLM 的 API key 貼進 Dockerfile 的 ENV,image 推上去、服務起來了,然後才想到這個 image 誰都拉得到。
前兩天講的是「哪種 workload 適合 Cloud Run」,今天直接動手。第五、六週我會用同一個 workload 依序走完 Cloud Run(今天)、GKE Autopilot(明天)、AKS(Day 27),所以先把它定義清楚。
agent-api一個很小的 FastAPI 服務,聽 PORT(預設 8080),兩個端點:
GET /healthz:回 {"ok": true}。POST /ask:收 {"q": "..."},呼叫 LLM API 後回 {"answer": "..."};API key 從環境變數 LLM_API_KEY 讀。無狀態、request-driven,就是 Day 21 說的那種 Cloud Run 最喜歡的形狀。假設 image 已經 build 好推到 Artifact Registry:asia-east1-docker.pkg.dev/PROJECT_ID/agents/agent-api:v1(PROJECT_ID 換成你的)。
先老實說:下面的指令我是對著 gcloud run deploy --help 與官方文件逐個旗標核過的,但寫這篇的當下沒有為了這篇重新部署一次,所以 front matter 標 verified: false。
開頭那個 POV 是最省事、也最危險的做法。比較對的做法是 key 進 Secret Manager,部署時用 --set-secrets 注入成環境變數,程式碼一行都不用改。注意 --set-secrets 只是「指向」那個 secret,Cloud Run 的執行 service account 還要拿到 Secret Manager Secret Accessor 角色才讀得到,這步官方文件有寫,很容易漏:
# 1. key 進 Secret Manager(從 stdin 讀,不留在 shell history)
printf '%s' "$LLM_API_KEY" | gcloud secrets create llm-api-key \
--data-file=- --replication-policy=automatic
# 2. 讓執行 service account 讀得到它(沒指定 --service-account 時,
# Cloud Run 用的是 Compute Engine 預設 SA)
PROJECT_NUMBER=$(gcloud projects describe PROJECT_ID --format='value(projectNumber)')
gcloud secrets add-iam-policy-binding llm-api-key \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role=roles/secretmanager.secretAccessor
# 3. 部署
gcloud run deploy agent-api \
--image=asia-east1-docker.pkg.dev/PROJECT_ID/agents/agent-api:v1 \
--region=asia-east1 \
--port=8080 \
--cpu=1 --memory=512Mi \
--min-instances=0 --max-instances=3 \
--concurrency=8 \
--set-secrets=LLM_API_KEY=llm-api-key:latest \
--no-allow-unauthenticated
部署完驗證。--no-allow-unauthenticated 代表要帶 identity token 才打得到,而且光有 token 不夠,呼叫者本身還要有 roles/run.invoker:自己部署的帳號通常已經有,要讓別的帳號或 service account 打,得先用 gcloud run services add-iam-policy-binding 綁給它,不然 token 是對的也會拿到 403:
URL=$(gcloud run services describe agent-api --region=asia-east1 \
--format='value(status.url)')
TOKEN=$(gcloud auth print-identity-token)
curl -s -H "Authorization: Bearer $TOKEN" "$URL/healthz"
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"q":"ping"}' "$URL/ask"
第一個 curl 如果明顯等了一下才回,很可能是冷啟動,也就是 --min-instances=0 的代價;不過網路或 handler 本身慢也會有同樣現象,要確認得看 log 裡有沒有新 instance 起來。
部署完,其他旗標大多是「照抄就好」,真正會影響行為與帳單的是三個擴縮/並發旋鈕:
--min-instances:0 就是 scale-to-zero,沒人用就不付錢,代價是冷啟動;設 1 以上就是花錢買掉冷啟動。這就是 Day 21 那個矛盾的旋鈕本體。
--max-instances:instance 數的上限,不是請求數的上限。同時能處理的請求大約是 max-instances × concurrency(這份設定是 3 × 8 = 24),而一個請求可能對 LLM 發不只一次呼叫,所以它只是「一波流量最多能放大到多少」的粗略護欄;真正的天花板是 LLM 供應商的 rate limit,要一起算。
--concurrency:一個 instance 同時處理幾個請求。agent-api 大部分時間在等 LLM 回來(I/O bound),可以往上調;如果你的 agent 是在本機跑模型推論(CPU / GPU bound),就要往下調。這個值沒有標準答案,取決於 workload 的性格,這也是 Day 06 為什麼要先分類。
--cpu / --memory 是示範值,它們是每個 instance 拿到的資源配置與上限,不是 Day 09 那種給排程器參考的 requests,Cloud Run 沒有排程這一層。它們跟 --concurrency 是一組的:一個 instance 撐得住多少並發,取決於你給它多少 CPU 與記憶體,要用實際壓測回頭調,不是憑感覺填。價格不寫死,以官方定價頁為準。
Day 17 講過我評估 liaostudio 搬 Cloud Run 後決定暫不搬。回頭看,卡住的正好就是今天這幾個旗標:
--min-instances=1 --max-instances=1 鎖死,scale-to-zero 與水平擴展都沒了。--no-cpu-throttling,計費模型就從「按請求」變得比較像「固定跑一台小機器」。agent-api 之所以能用今天這組旗標直接上,是因為它無狀態、每個請求自己收尾。今天的指令不難,難的是你的服務有沒有長成能吃這組旗標的形狀。
--port 不一致,或一啟動就去連 LLM 但 key 還沒注入。先用 /healthz 確認容器活著,再測 /ask。--min-instances 不小心設成 1 以上。回想 Day 21 的問題:大部分時間沒人用的服務,該不該一直開著?明天用同一個 image、同一組環境變數,搬到 GKE Autopilot,看這幾個旗標在 YAML 裡各自變成什麼。
Cloud Run 上部署 agent API 真正要想的是三個擴縮/並發旋鈕:min-instances 決定你要不要吞冷啟動,max-instances 配上 concurrency 是流量放大的粗略護欄,concurrency 本身看你的 agent 是在等 LLM 還是在算;cpu / memory 則是資源與帳單,要跟並發一起量。
gcloud run deploy 旗標完整參考:gcloud run deploy