阻止你寫點小東西的,是那個摩擦力
經驗具現化 — EP01:懶人需要一個部署方案
我常常有一些新的想法想試。但一想到試完之後要把它部署出去,就覺得好麻煩。
我手上有一個小專案,FastAPI 寫的,功能很簡單,就是想丟到一個公開的 URL 讓人試玩。Dockerfile 早就寫好了,本機跑得好好的。接下來呢?
開 GCP Console,建 project。開 Artifact Registry。設 IAM。裝 gcloud CLI。跑 gcloud auth configure-docker。Build image,push image。然後 gcloud run deploy,參數一串,打錯一個 flag 就要重來,然後你又得再研究一下,又被摩擦了一次。
全部搞完,兩三個小時過去了。程式本身可能只花了一個下午,但部署的前置作業比寫程式還久。到底要怎麼辦呢?
就算有了 AI agent,還是需要克服那個「好麻煩啊」的念頭。克服的方法就是不給自己時間想,讓它自動做起來,連猶豫的機會都沒有。
先看結果
最後做出來的東西長這樣。你有一個 repo,裡面有 Dockerfile,然後在 terminal 打:
gh workflow run deploy.yml \
-f type=service \
-f name=my-demo \
-f repo_url=https://github.com/user/my-app
等 GitHub Actions 跑完,大概兩三分鐘,你會拿到一個 Cloud Run 的 URL。瀏覽器打開,東西在上面。
本機只需要 gh CLI,不需要 gcloud,不需要任何 GCP 金鑰。這點對我來說特別重要,身上揹了太多 GCP account,不同角色不同用途,想分清楚又怕誤用。大半的時間,啟用中的都只是一個無關緊要的帳號。如果部署還要我切帳號、選 project,那個摩擦力又來了。
但剛開始不是這樣的。讓我說這個東西是怎麼長出來的。
三角信任:為什麼開發者不需要碰 GCP
整個架構有三個角色,各做各的事:
你的電腦只負責「按按鈕」。透過 gh workflow run 送一個 HTTP 請求給 GitHub,附帶幾個參數:要部署哪個 repo、叫什麼名字、什麼 branch。就這樣,你的電腦做的事到這裡結束。
GitHub Actions 做所有苦工。收到請求後,它 clone 你的 repo、build Docker image、push 到 GCP 的 Artifact Registry、然後部署到 Cloud Run。所有跟 GCP 打交道的事都在這裡發生。
但問題來了:GitHub Actions 要操作 GCP,就需要認證。最直覺的做法是產一把 Service Account key,存到 GitHub Secrets。這樣做能動,但那把 key 是長期的,會過期、會外洩、要輪替。對一個 PoC 平台來說夠用,但總覺得可以更乾淨。
Workload Identity Federation 是更優雅的做法。原理是這樣的:GitHub Actions 跑的時候,GitHub 簽一個 OIDC token 說「我是 twjug-lite-infra 這個 repo 的 workflow」。GCP 收到後驗證 token,確認來源可信,就發一個短期 access token 回去。這個 token 用完就過期,不需要存放,不需要輪替。
沒有長期金鑰。金鑰不存在,就不會洩漏。
你的電腦 GitHub Actions GCP
| | |
|-- gh workflow run ------->| |
| |-- OIDC token ---------->|
| |<-- 短期 access token ---|
| | |
| |-- clone, build, push -->|
| |-- gcloud run deploy --->|
| | → Service URL |
說穿了,我不想同時當開發又當 DevOps。寫功能的時候就專心寫功能,不要一邊還在想權限怎麼設、image 推到哪。這個架構做的事情就是把維運那一半的參與降到最低。開發者只管寫程式和 Dockerfile,GCP 權限集中在一個地方管理,部署操作透過 CI 代理執行。你需要的只有三樣東西:一個有 Dockerfile 的 repo、gh CLI 已登入、對 infra repo 有觸發 workflow 的權限。
專案初期:先做有的沒的
這個專案一開始就是跟 Claude Code 一起做的。但初期做的事可能跟你想像的不一樣,不是叫它寫 deploy script。這就是放假沒事、想克服懶散的 side project,所以當然不會一開始就積極地朝目標衝。先做點有的沒的,看看網路上講的 LLM 或 Claude 的 best practice,然後就開始東摸西摸了。
起手第一件事是寫 CLAUDE.md。這是 Claude Code 的專案規則檔,你可以在裡面告訴它在別的地方學不到的事,像是你的偏好、你的地雷、這個專案的脈絡。我忘了在哪看到的,有人說 CLAUDE.md 最有價值的部分是那些「只有你知道」的東西,而且它應該跟著你一起演化,不是寫一次就放著。所以我從另一個專案借了幾條規則:沒有明確指示時只查不改、學到新東西就記日記。日記這件事後來變得很重要,它讓我跟 AI 可以一起反省過去、改善未來。
然後我想搞懂 Workload Identity Federation(WIF)到底怎麼運作。官方文件不好讀,那就叫 AI 寫個極簡版本,看不懂就一直逼它再簡化。來回幾次之後生出了一份 how-it-works.md,裡面有循序圖和說明文字。我本來就偏好這種圖文對照的格式,結果 AI 自己就往這個方向生了,我可沒特別跟它提過。
因為是閒暇之餘的 side project,做到一半常常會順便試一些之前想試但沒認真弄的東西。那陣子在想怎麼整理過去散落在 ~/.claude 裡的 slash command 和 skill,想了一下,都已經在用 superpowers 了,應該讓 Claude 好好模仿它的格式,幫我整理出自己的 plugin。於是就把 skill 從手動打包改成 marketplace plugin 格式,安裝變成一行指令。所以這個專案裡面不只有 GitHub Actions 執行的各種 gcloud wrapper 來包裝迷你專案的部署,還包含了 Claude plugin 裡的 skill,讓你在 terminal 裡就能直接操作 Cloud Run 的 deploy 和 undeploy。
回到部署工具本身。一開始只做了 service 的支援,所有 script 都圍繞著 service 在轉。一直到覺得有必要讓它能 dispatch job 出來,才開始建 job 的流程。
跑整合測試的時候,出了一個有意思的 bug。我請 AI 用 undeploy skill 刪除一個 Job,它卻去呼叫了 Service 版本的 undeploy.sh,結果當然失敗。
原因不是 AI 判斷錯誤,是我的 script 命名不一致。因為最早只有 service,undeploy 那邊就叫 undeploy.sh,沒有加 -service 後綴。後來加了 job 支援,deploy 那邊很對稱地叫 deploy-service.sh 和 deploy-job.sh,但 undeploy 忘了改。一部分的 script 有 service suffix,一部分只有單純的 action verb,AI 在這之間不知道該呼叫哪一個,最後選了錯的。
修復方式很簡單:把 undeploy.sh 改名為 undeploy-service.sh,讓 deploy 和 undeploy 兩邊都有一致的 -service、-job 後綴。改了命名,問題就消失了。不過這只是最初的版本,後來覺得讓 AI 一直寫 bash script 不是辦法,每次改邏輯都在跟 shell 的各種 edge case 搏鬥,還是叫它寫個完整的 Python script 來用比較實在。
跟 AI 協作,你的命名就是你的 API。如果 API 有歧義,呼叫方一定會踩到。不管呼叫方是人還是 AI。
計費安全
別人做 side project 可能很在意 best practice、coding style、各種軟體工程實踐。這些對我來說都不是最重要的。我最在意的是預算控管,不想哪天收到一張非預期的帳單。所以我每天都會去看帳單,看過去兩天有沒有什麼錯誤的決策導致用量暴增,或是之前覺得可以省錢的寫法,是不是真的把費用降下來了。
這次做的是輔助實作 PoC 的小工具,架設需求不是長駐服務,是有人用才需要打開的。所以 Cloud Run service 可以設計成 request-based billing,只有收到 HTTP request 才計費。部署時強制帶三個參數:
-
--min-instances=0:沒流量時縮到零,不計費 -
--max-instances=1:限制最大實例數,防止爆量 -
--cpu-throttling:只在處理請求時才計算 CPU 費用
沒有流量的時候,成本趨近於零。
下集預告
到這邊,部署流程是跑通了,但每次都是我帶著 agent 一步一步走。它還不會自己來。
一個 repo 可以部署成一個 service 或一個 job。夠用了嗎?
我是個悠閒的單人開發團隊,且看且走,沒用到的東西就不開發,有需要再說,反正 Claude 最終會承受所有的一切。所以一直到某天真的想在同一個 repo 裡放三個 service 加一個 batch job,才發現不夠用。那時候才開始設計一個 YAML 格式來描述多容器部署,結果 Claude 幫我重寫了三次。因為有開發過 Kubernetes 服務的經驗,我知道需要類似 Pod 那樣描述多容器的東西,但不用那麼完整,也不想直接搬 Knative YAML 出來,還沒有到那麼複雜的情況。
至於每天看帳單這件事,後來也做了一個 billing skill,直接用 BigQuery 查 GCP export 出來的帳單資料,不用再自己開 Console 翻。能簡化的事就不要手動做。