🌐 This page hasn't been translated yet — showing the original Chinese. Translated posts

Stack 與環境:staging 不該是另一份 code,是同一份的另一個 instance

· tech

#iac#devops#book-notes

📑 目錄

進入書的第二部分:stack。前面講切割時一直在說「一個 stack」怎樣怎樣,這篇先把這個詞站穩,再處理它最日常的應用場景——同一包基礎設施,要生出 dev、staging、prod 好幾份。這件事幾乎每個團隊都在做,而且大多數是用錯的方式做的。

Stack:基礎設施的變更單位

書給 stack 的定義很乾脆:一組被當成同一個單位來定義、建立、變更的基礎設施資源——用白話說,就是「一起 plan、一起 apply、一起冒險」的那包東西。Terraform 的一份 state、CloudFormation 的一個 stack、Pulumi 的一個 project,都是這個概念的實體。

三個常被混用的詞在這裡值得掰開:repo 是放 code 的地方、module 是可重用的程式庫、stack 是變更的單位——repo 裡可以有多個 stack,一個 stack 可以用很多 module,但「改了之後會一起上線的範圍」只由 stack 決定。上一篇的爆炸半徑,量的就是 stack 的邊界。

環境問題:一份定義,多個 instance

需要多個環境時,直覺會走向兩條歧路,而正解是第三條:

複製貼上 一包全裝 可重用 stack code A code A' code A'' dev staging prod ✗ 三份 code 各自漂移 改三次,忘一次就失真 一份 code 同一個 stack(同一份 state) dev stg prod ✗ 環境共用爆炸半徑 改 dev 的失誤可能炸到 prod 一份定義 dev staging prod 各自的 state,差異只在參數檔 ✓ 測過的那份定義 就是上線的那份
左、中是兩條歧路:複製貼上讓環境各自演化成不同的東西;全裝一包讓「改測試環境」揹著正式環境的風險。正解是右邊:環境=同一份定義的不同 instance
  • 複製貼上環境:每個環境一個資料夾、一份 code,「反正只是先複製一下」。三個月後三份各長各的——修 bug 要改三次,總有一次忘掉,於是 staging 驗證的再度不是 prod 會跑的東西(上一篇的環境失真,根源常常就在這)。
  • 一包全裝:dev、staging、prod 全定義在同一個 stack、同一份 state。看似 DRY,實際上是把爆炸半徑橫跨到環境之間——你只想動 dev,但 plan 掃過 prod 的資源,一個 drift、一個手滑,炸的就是正式環境。環境隔離的意義被 state 一鍋端掉了。
  • 可重用 stack(正解):一份定義,per 環境各起一個 instance、各持一份 state,環境差異全部收進小小的參數檔。這才同時拿到兩個世界:定義同源(staging 驗的就是 prod 要跑的),風險隔離(每個 instance 自己的爆炸半徑)。

順帶把一個常見誤會打掉:環境不是 git branch。用 branch 區分環境,本質上就是複製貼上的變形——merge 漏了就是漂移,而且你永遠說不清楚 prod branch 上「多出來的那幾個 commit」是什麼。環境差異屬於參數,不屬於版本歷史。

參數化的甜蜜點:參數是介面,不是垃圾抽屜

一份定義伺候多個環境,靠的是參數——而參數的紀律,決定這個模式撐不撐得久:

全寫死 只好每個環境複製一份 少量參數 名字、規模、少數旗標 過度參數化 config 長出 if/else 甜蜜點 回到複製貼上的漂移 環境差異一眼看得完 變成另一種程式語言 參數每多一個,要測的組合就乘一次
參數化跟宣告式語言一樣有混種地帶:當參數檔開始控制「邏輯走哪條路」而不只是「值是多少」,你就是在用 config 寫程式了

書裡給的參數傳遞手段從命令列、環境變數、per 環境參數檔到 parameter registry 都有,但工具選擇是小事,紀律才是大事:環境之間的差異應該小到「一個參數檔一眼看得完」——名字前綴、機器規格、instance 數量、少數功能旗標,就這樣。一旦定義裡出現 if env == "prod" 這種分岔,警鈴就該響:dev 和 prod 走的已經是不同的邏輯路徑,「測過的就是上線的」再次被打破——這正是宣告式混種地帶的參數版。

反思

我判斷環境健康的一行指令:diff 兩個環境的參數檔

這章給了我一個很省事的健檢法:把 dev 和 prod 的差異攤開來,應該只剩兩個小參數檔的 diff。做得到,代表定義同源、環境只是 instance;做不到——要 diff 整個資料夾、要人腦記得「prod 還有改過哪些」——就是已經在複製貼上的路上了。這個檢查殘酷的地方在於它沒有中間地帶:同源就是同源,「大部分一樣」在驗證的意義上等於不一樣,因為你不知道剩下那部分藏著什麼。

便宜環境是可重用 stack 送的紅利,而且比想像中值錢

一份定義能生任意多個 instance 之後,「環境」突然變得便宜:每個 feature branch 起一套完整環境跑測試、用完銷毀;要重現三個月前的事故,起一個當時版本的 instance 慢慢驗。這在複製貼上的世界裡想都不敢想——多一個環境就多一份要維護的 code。回頭看,這其實是可拋棄原則的環境級版本:環境從「稀缺的、要排隊共用的資產」變成「用完就丟的消耗品」,而搶 staging 排隊這件事,消耗的團隊時間遠比大家願意承認的多。

參數的准入審查:說得出「哪兩個 instance 要不同值」才准進

參數會自然增生——每次有人想留個彈性,就多一個參數,「以防之後要改」。但參數是這份 stack 的公開介面:多一個參數,使用它的人就多一個要理解的概念,測試要覆蓋的組合就乘一次,而「以防萬一」的參數十個有九個從來沒被改過第二個值。所以我現在對新參數只問一個問題:現在、具體地,哪兩個 instance 需要不同的值? 答不出來就寫死,等真的需要再開——這跟 API 設計「先窄後寬好過先寬後窄」是同一個判斷:收回一個參數是 breaking change,加一個永遠來得及。