邊開火邊移動
經驗具現化 — EP03:Skill 上路,然後不夠用了
我有一些過去整理的影片和學習記錄放在 AWS S3 上。每次一打開來,不忍直視,又立馬關了起來。檔名亂取、目錄結構早就對不上當初的邏輯、有些東西重複了好幾份。我已經毫無心力去整理它了。也許你會說,叫 AI 整理吧?!我怎麼可以放棄用 AI 寫寫小廢專案的機會呢!人類果真是不會好好遵守規則的(謎之音:只有你,就是你!不要把其他人拖下水啊!)。
嗯,還是先關起來,眼不見為淨。我們在新的地方開始吧,希望這次能有好一點的都市計劃。
技術選型:老熟人優先
家裡沒有 Mac mini 或什麼小伺服器可以跑這種東西。所以我過去做 side project 都是選 serverless 架構。不用管機器、不用管 OS 更新、能自動關閉資源,沒煩惱的存在。簡單定義一下「老熟人」,都有我的信用卡綁定了。以不新綁卡為原則。
AWS Lambda 是老朋友了。作為成熟的投資者兼開發者,自動化更新自己的持股與成本是必要的。靠永豐金證券的 SinoTrade API,放在 AWS Lambda 上面加 CloudWatch Events(在我寫作時查手冊才發現,它改名為 EventBridge 了)排程跑的。這個機制穩穩地跑了好多年,穩定到常讓人忘了它的存在。只有壞掉時才會想起來。忘了更新券商的 Python package 所以不能動了,或是 API token 到期而無法工作。
當然我知道現在 vibe coding 很紅,不是說這些極高便利性的 serverless 平台跟著一起冒出來,而是它們也試著去抓住 vibing programmer 的注意,用免費額度吸引他們使用。有些開發者會追逐各家平台的免費額度,反正 vibing 都不用自己動手改,搬家的成本看起來很低,所以索性這麼做了。但我不想逐免費額度而居。通常不是平台改方案,而是額度用完了,搬去其他還有免費的地方。可是時間成本才是最大的成本,這些不必要的摩擦,我不想承受。
三大雲也有免費額度,但我一點都不在意,我只關心我對平台熟不熟、對成本結構是否能明確掌握。
Cloudflare 是一個強而有力的候選。它的 Workers 生態很成熟,edge computing 的方向也很對。但我的部署習慣是依賴 container,而 Cloudflare 的 container 服務去年中才開始 public beta,差不多接近秋天才比較像認真的產品線。對我來說還是太新了一點。
所以,配合剛做好的 deploy skill,這次選了 GCP Cloud Run。未來如果要支援其他平台,skill 再做 AWS 或 Cloudflare 的版本就好。反正 skill 的本質就是 SOP,換個平台就是換一份 SOP。
談談 Skill 的首戰 Side Project:Media Archive
我需要一個新的地方來收集影片、素材。不是什麼公開的影音平台,就是一個私人的 media archive。做筆記的時候錄的東西、流程記錄、各種學習素材。這些東西只需要保持在我自己的 Object Storage 桶裡就好。
需求就是:一組 API 能接受 request,幫我把指定的影片、素材收集回來,存到 Media Archive。有需要的時候,有個簡單的網頁可以瀏覽。平時沒用到不花錢,Cloud Run 的 min-instances=0 在《EP01:懶人需要一個部署方案》就設好了。Object Storage 儲存成本是已知的,並在可以接受的範圍內。
由實戰推進需求:到了該支援多 Component 部署的時候了
第一步很簡單。一個 Cloud Run Service,跑一個 HTTP API,接受 request。project root 放一個 Dockerfile,跟上一篇帶著 agent 走一遍部署流程時的 hello world 一樣的起手式,只是這次是真的要用的東西。
上一篇做好的 deploy skill 跑一次,部署上去,拿到 URL,確認能動。沒問題。
到這裡為止,都在 skill 能負擔的範圍內。接著事情就開始變了。
這個 API 需要支援影片的下載,下載完存到 Object Storage。問題來了:Cloud Run 的 HTTP service 每次 request 有時間限制。你沒辦法在一個 request 裡面下載完所有必要的素材,有些影片很大,有些需要分段下載再合併。
這時候就必須要有一個能在背景做這件事的服務。Cloud Run Job 就是為了這個:你觸發它,它跑完就結束,不需要一直掛著等 request。
整個流程大概長這樣:
User API Service Cloud Run Job Object Storage
| | | |
|-- submit -------->| | |
| |-- parse & store ------>| |
| | metadata to DB | |
| | | |
|-- trigger ------->| | |
| download |-- split into batches | |
| | | |
| |-- dispatch batch 1 --->| |
| |-- dispatch batch 2 --->| (concurrent) |
| |-- dispatch batch N --->| |
| | | |
| | |-- download & verify -->|
| | | |
| |<-- report done --------| |
| | | |
|-- browse -------->|-- signed URL --------------------------------->|
API Service 負責接收請求和調度。它把大量的下載工作切成批次,每批派一個 Cloud Run Job execution 去跑。多個 Job 同時執行,各自下載完後回報 API。需要瀏覽的時候,API 產生臨時的存取連結(pre-signed URL),讓你直接從 Object Storage 讀取。
這個模式讓你可以在短時間內把巨大的檔案完成保存。一個 Job 下載一批,三十個 Job 同時跑,就是三十倍的下載速度。API 不需要自己做下載,它只負責調度和追蹤進度。
這樣的需求聽下來,我們開始遇到了原本部署工具所不支援的型態。現在一個 repo 裡有兩個東西要部署了。Service 負責接 request 和調度,Job 負責實際下載。原本的 skill 只會處理 project root 的一個 Dockerfile,還無法支援新的部署需求。
那陣子我總是開著兩組 Claude。一個在 twjug-lite-infra 改 skill,另一個在做 side project 本身。兩邊互相驅動,side project 碰到 skill 不夠用的地方,就切到另一邊去改。這就是「邊開火邊移動」的真實節奏。
這裡順便講一下「改 skill」到底是什麼意思。Claude Code 的 skill 是透過 plugin 發佈的,plugin 有版號,安裝後就是你 terminal 裡可以直接用的能力。所以「改 skill」不只是改一個 markdown 檔案,而是一個完整的循環:
Developer Claude Code (infra) Marketplace Local Plugin
| | | |
|-- modify SKILL.md ->| | |
| |-- validate format | |
| | (skill-creator) | |
| | | |
|-- "release it" ---->| | |
| |-- bump version | |
| | (plugin.json) | |
| | | |
| |-- git push ---------->| |
| | | |
| |-- marketplace update ->| |
| | | |
| |-- plugin update -----------------------> |
| | | |
| |<-- done | |
修改 SKILL.md 的內容,用 skill-creator 這個 skill 來確保格式對、觸發條件對、frontmatter 沒漏。改完之後更新 plugin.json 的版號:加 skill 或拿掉 skill 就升 minor,改現有 skill 的行為就升 patch。然後 push 到 remote,跑 claude plugin marketplace update 把新版推上 marketplace,再跑 claude plugin update 把本機的 plugin 更新到最新版。
聽起來步驟很多,但其實就是 modify → bump version → push → publish → update。而且這些步驟本身也可以寫成 SOP。事實上 CLAUDE.md 裡就有這麼一段:
當使用者說「release」「release it」「發布」時,指的就是以下流程:
- 更新 plugin.json 版號(新增 skill 升 minor,改行為升 patch)
- push 到 remote
claude plugin marketplace update推上 marketplaceclaude plugin update更新本機
寫在 CLAUDE.md 裡,agent 就知道了。所以在 skill 那邊的 Claude,我說一聲「release it」,它就會自己把這整串跑完。
兩個戰場,各自有各自的 agent 在跑。我在中間切來切去,像個傳話的人。
隨需求演化而來的 Configuration File
一個 repo 要部署多個東西,那就需要一個地方描述「這個 repo 裡有什麼、各自怎麼部署」。
於是長出了 mini-deployment.yaml。第一版很簡單:
components:
- name: hello-run-api
type: service
dockerfile: api/Dockerfile
enabled: true
- name: segment-downloader
type: job
dockerfile: segment-downloader/Dockerfile
enabled: true
一個清單,每個 component 有名字、類型(service 或 job)、Dockerfile 的位置、以及一個 enabled 開關讓你不用刪設定就能暫停某個 component。
deploy skill 讀到這個檔案,就平行觸發對應的 workflow。多個 component 同時部署,不需要排隊等。
夠用了。暫時。
邊跑邊改的節奏
回頭看這段過程,最有意思的不是 mini-deployment.yaml 最後長什麼樣子,而是它怎麼長出來的。
我沒有事先規劃「我需要一個多服務部署的格式」。我只是在做 side project,做到不夠用了,才回頭改工具。改完繼續做,做到又不夠用了,再改。
這跟《EP02:第一次成功部署》講的起手式是一樣的邏輯:先跑通,遇到問題再改。差別在於那次是帶著 agent 走一遍就完事了,這次是帶著它走了好幾遍,每一遍都撞到一個新的牆,然後一起把牆拆掉。
在這過程中,新版的 deploy skill 也在這些來回碰撞裡整理出來了。skill 的 release process 也跟著成形,包含 review、bump version、push、publish 到 marketplace。這些步驟本身也變成了可以一句「release it」就跑完的 SOP。工具在長大,管理工具的流程也在長大。
到這裡為止,這個 infra 專案已經不單純是 GCP deploy 工具了。部署的 skill 在長大,管理 skill 的流程也在長大。但做事的過程中,我發現自己花了不少時間在「管理做事的節奏」上,每天開工回想進度、釐清方向、討論完之後剩下的執行。這些也是一種摩擦,只是不在技術上,而在協作的流程裡。
下一篇,來聊這些「不是部署但也很煩」的事,怎麼被具現化成另一組 skill。