從單一 Terraform Root 到 Modules + Live: 分階段遷移、Import 與 State Surgery
這篇記錄一件我現在很常回頭看的事: 如果一開始 Terraform 只有一個 root, 後來才想補 dev / prod、補 shared 層、補 CI/CD 邊界, 到底該怎麼遷移才不會影響既有環境。
我最後的結論很簡單:
- 不要一次全部重寫
- 不要先切 CI/CD
- 在新 roots 還沒收斂前, 不要先刪掉舊 roots
真正可用的做法是分階段。
遷移前的問題長什麼樣
一開始很常見的 Terraform layout 是這種:
|
|
這種 layout 在早期階段很好用, 因為快。
但後面需求一多, 問題就會同時冒出來:
- shared resources and env resources live in the same root
- service root depends on platform root assumptions
- secrets are not environment-safe
- production domain logic leaks into dev
- CI/CD is coupled to legacy roots
這時候最危險的念頭就是: 直接把舊 root 刪掉, 換成新結構。
這樣通常不會成功。
我的目標結構
目標不是單純換資料夾名稱, 而是換成擁有權模型。
|
|
這個目標結構的重點是:
modules/*負責定義可重用的 infra 契約live/*負責真正的 state 與環境落地
遷移順序比最終結構更重要
我最後認為最重要的不是終態長什麼樣, 而是遷移順序。
我會把順序寫成這樣:
- 建立新的 modules 和 live roots
- 遷移期間保留 legacy roots
- 把資源 move 或 import 進新的 live roots
- 讓新 roots 收斂到乾淨狀態
- 再把 CI/CD 切到新路徑
- 最後才刪掉 legacy roots
只要把 5 和 6 提前, 很容易出事。
什麼用 import, 什麼用 state mv
這是遷移時最實際的一題。
我會這樣分:
適合用 terraform import 的情況
例如這些資源本來就活在 cloud provider 裡, 只是新的 root 還沒接手:
- Cloud Run service
- Cloud SQL instance
- service account
- IAM member
- Artifact Registry repository
這類資源用 import 最自然:
|
|
適合用 terraform state mv 的情況
最典型的是 random_password。
這種資源在遠端系統裡, 並不會直接保留 state 本身代表的值。你如果重新 import 或重建, 很可能不是「接手既有值」, 而是「產生新值」。
這在 DB password 之類的東西上非常危險。
|
|
這種情況真正要保護的不是 resource name, 是那個 value 本身。
遷移時最容易漏掉的坑: first deploy exception
理論上我們都希望新 root 一套上去就 No changes。
實際上第一次遷移通常會有例外, 尤其是這幾種:
- existing custom domain mapping
- manually populated secrets
- 從來沒有寫進 tfvars 的執行階段值
- old resources with names that no longer match the new contract
這些東西通常不會剛好落在理想化的自動化流程裡。
所以我現在會把 migration 分成兩種步驟:
| Type | Description |
|---|---|
| structural migration | modules, roots, state ownership |
| first deploy exception handling | import-only resources, secret copy, runtime value capture |
如果把這兩件事混成一條直線, runbook 很容易寫得太樂觀。
切 CI/CD 的時機
我後來很確定一件事:
CI/CD 要最後再切換。
原因很簡單。只要新的 live roots 還沒通過這幾條:
live/sharedplan is cleanlive/dev/platformplan is cleanlive/dev/serviceplan is clean
那 CI/CD 就不該先改成完全依賴新 roots。
不然一旦自動 deploy 跑起來, 你會同時遇到:
- code path changed
- workflow changed
- state ownership changed
- runtime contract changed
這時候根本很難 debug。
比較穩的方式是:
- make the new roots valid
- import or move state
- reconcile to zero drift
- only then switch workflows over
什麼叫 migration done
我自己現在會用很嚴格的標準。
不是 terraform apply 成功就算 done。
我會看這幾條:
- new roots can plan with no changes
- old roots no longer own migrated resources
- 現行 workflows 只指向新 roots
- 執行階段行為仍正常
- production-only 資源仍只留在 production 路徑
少一條都不算真的完成。
一個很實用的 runbook 思路
如果未來我自己再做一次類似遷移, 我會直接沿用這個 checklist:
|
|
這樣寫的好處是, 你可以很清楚知道現在卡在哪一個 phase, 而不是把整個 migration 當成一個模糊的大任務。
常用 gcloud 指令
這類遷移文最容易忘的通常不是 Terraform, 而是要去哪裡把現在的執行階段值抓回來。下面這幾條我自己最常回頭查。
查目前 Cloud Run service 的 image
|
|
查目前 Cloud Run service 的 URL
|
|
查目前 service 內的 BASE_URL
|
|
看目前 service 的 env 陣列
|
|
複製既有 secret value 到新的 env-specific secret
|
|
查某個 secret 的最新值
|
|
結論
Terraform migration 最怕的不是多寫幾份 HCL, 而是把擁有權變更、workflow 切換、執行階段變更 一次綁在一起。
真正穩的做法是:
- modules first
- live roots next
- state migration after that
- CI/CD 切換放在後面
- legacy deletion last
只要順序對了, modules + live 這種重構其實可以做得很穩, 不需要 big bang rewrite。