在單一 GCP Project 裡切 Dev / Prod: State、Secrets、IAM 與 Workflow 邊界
這篇記錄我把一套 Cloud Run + Cloud SQL 應用留在同一個 GCP project 裡, 但仍然把 dev 和 prod 切乾淨的做法。
重點不是 branch 命名, 也不是多建兩個 GitHub workflow 而已。真正要切的是 state、資源命名、secrets、執行身分、部署身分, 還有 workflow 控管。這幾層如果只漏一層, 最後就還是會互撞。
為什麼很多團隊會想留在同一個 GCP project
最直接的原因通常不是技術, 而是成本和管理負擔。
如果一開始就做成 project-per-environment, 會比較接近 cloud best practice, 但實際上也會多出很多東西:
- more IAM setup
- more service accounts
- more budget tracking
- more APIs to enable
- more Terraform bootstrap work
- more operational overhead
如果產品還在早期階段, 或只是想先把部署路徑、執行階段隔離和 branch 控管建起來, 留在單一 project 其實是合理取捨。
問題是: 同一個 project 代表所有資源都活在同一個 namespace 裡。只要命名、state、或 secrets contract 沒切開, dev 和 prod 只是看起來分開。
真正要切的是哪些邊界
我最後整理成 6 個邊界。
| Boundary | What must be separated |
|---|---|
| Terraform state | Shared, dev platform, dev service, prod platform, prod service |
| Resource naming | DB instance, Cloud Run service, secrets, service accounts |
| Runtime secrets | DATABASE_URL, auth secrets, API keys |
| Runtime identity | Cloud Run runtime service account |
| Deploy identity | CI/CD deploy service account |
| Workflow governance | branch -> environment routing and approval boundary |
如果只做了其中 2-3 個, 後面一定還會踩坑。
我最後採用的 Terraform layout
核心做法是把 resource definition 和 environment instantiation 分開。
|
|
這個 layout 有兩個好處:
modules/*專心描述 reusable infra contractlive/*專心描述實際環境的 state、vars、target wiring
這樣 dev 和 prod 才是兩個真的 live roots, 不是同一份 root 加一個變數切換。
單一 project 裡最容易漏掉的第一個坑: state collision
很多人一開始會這樣寫:
|
|
然後再用 environment = "dev" 或 environment = "prod" 去切資源名稱。
這樣不夠。
因為 state 還是一份。只要這份 state 同時管 shared 資源和 env-specific 資源, 後面你就會碰到:
- prod apply touches dev resources
- shared resources and env resources drift together
- import and migration become messy
- destroy blast radius becomes unclear
我最後的做法是把 state 前綴直接切開:
|
|
這樣就算還在同一個 GCS bucket, ownership boundary 也已經清楚很多。
第二個坑: naming isolation 不完整
在單一 project 裡, naming 不是 cosmetic, 是 isolation 的一部分。
例如這幾個東西都不能共名:
- Cloud SQL instance
- Cloud Run service
- Secret Manager secret
- runtime service account
- deploy service account
我後來把命名規則收斂成:
|
|
這聽起來很基本, 但真正會踩坑的是 secret naming。
如果你只把 Cloud Run service 改成 app-dev / app-prod, 但 secret 還叫:
|
|
那 dev 和 prod 還是在共享執行階段契約。
第三個坑: secret name 分開了, value 不一定分開
這是我覺得最值得記的一點。
把 secret ID 切成這樣:
|
|
只代表 resource boundary 分開了。
不代表 value 一定不同。
這在 migration 階段特別容易發生。因為第一次 bring-up prod 時, 很常會先把舊值複製過去, 想說流程先跑通再說。
技術上可行, 但要很清楚這只是 transitional state, 不是完成狀態。
我會把 secret separation 拆成兩層看:
| Layer | Meaning |
|---|---|
| ID separation | different secret resource |
| value separation | different credential or token |
對真正的 environment isolation 來說, 兩層都要成立。
第四個坑: 執行身分和部署身分不能混用
Cloud Run runtime service account 跟 CI/CD deploy service account 是兩種完全不同的身分。
我一開始比較容易把它們想成同一類, 後來才發現這樣很危險。
比較乾淨的模型是:
|
|
對應的權限也不同:
- runtime SA needs runtime permissions
- deploy SA needs deployment permissions
這樣好處很直接:
- deploy credentials cannot silently become runtime credentials
- prod deploy cannot affect dev by accident
- IAM review becomes readable
第五個坑: GitHub environment vars 不是全域變數
GitHub Actions 這一段很容易讓人誤會。
很多人會以為只要 repo 裡有 environment-scoped vars, workflow 任何 job 都能讀到。不是。
job 必須真的宣告:
|
|
或:
|
|
對應 job 才會讀到那個 environment 的 vars。
所以 environment-scoped variables 和 workflow routing 是綁在一起的。
如果 job 沒有 environment, 你就算在 GitHub UI 裡把變數設好了, workflow 還是拿不到。
第六個坑: branch policy 不是 environment isolation 的替代品
dev branch -> dev
main -> prod
這個 policy 很重要, 但它不是 environment isolation 本身。
正確順序應該是:
- isolate the environments first
- 再加上 branch 控管
也就是先把:
- state
- names
- secrets
- identities
- workflow entrypoints
切好, 再來收 branch boundary。
不然就會出現一種假象:
- 看起來
main只會 deploy prod - 但底下其實還在共用 state 或 secrets
我最後採用的 workflow model
我最後收斂到這個模型:
|
|
這樣的好處是操作者看到 workflow list 就知道每條線在做什麼。
我後來刻意避免把 environment routing 做成一支過度聰明的大 workflow。因為那種寫法一開始看起來很 DRY, 但後面 debug、權限檢查、branch guard, 全部都會變難讀。
我會怎麼判斷這套 env split 是否真的成立
最後不是看 repo 長得多漂亮, 而是看這幾條能不能明確回答「是」:
- dev deploy 是否完全不會碰到 prod state
- prod deploy 是否完全不會碰到 dev state
- runtime secrets 是否在名稱和實際值兩層都隔離
- production domain 是否只屬於 prod
- CI identity 和 部署身分 是否能分開審查
- 操作者是否不看原始碼也知道該跑哪條 workflow
如果這幾條還有一條答不出來, 代表 env split 還沒真正完成。
結論
把 dev 和 prod 留在同一個 GCP project 不是不能做, 但不能只靠 branch 或表面上的命名去切。
真正有用的做法是把 isolation 拆成多層邊界:
- state
- names
- secrets
- 執行身分
- 部署身分
- workflow 控管
只要這幾層一起收乾淨, 單一 project 也能跑出一套可維護的 dev / prod 模型。之後真的要升級成 project-per-environment, 也比較像是擴張, 不是推倒重做。