多數服務的起點是一台運算實例加一台資料庫,部署方式是 SSH 進去拉 code 再重啟。這個結構在單人、單環境、低變更頻率的條件下運作正常,但它的隱性前提是:所有設定只有一份,且只有一個人在操作。機器的配置存在操作者的記憶裡,資料庫參數存在 Console 頁面上,security group 規則是建立時隨手設的。這些設定沒有被記錄在任何能回溯或重建的地方。

這個結構的操作極限會在兩個時間點浮現:第一次需要在正式環境以外的地方驗證變更時,以及第二個人開始操作同一組資源時。以下依序說明每個階段的操作現實與對應的 infra 需求。

資料庫變更需要驗證環境

應用新增功能時經常需要改資料庫的表結構 — 加欄位、改索引、拆表。這類操作(database migration)如果語法有誤或邏輯有缺,可能導致服務中斷或資料不一致。正常做法是先在非正式環境驗證通過,再推到 production 執行。

單一環境的情況下沒有驗證的場所。三種應對方式各有不同的風險邊界:

直接在 production 執行。成本最低,風險最高。migration 腳本跑下去的那一刻,正在使用服務的使用者直接承受後果 — 一個鎖住整張大表的 ALTER TABLE 會讓所有查詢卡住,一個 DROP COLUMN 刪錯欄位會造成不可逆的資料遺失。服務規模小、使用者少時代價尚可承受;一旦服務開始承載營收或外部依賴,這個做法的風險代價就超過了它省下的時間。

手動複製一套環境。到 Console 上照 production 的設定重新建一台 EC2、開一台 RDS、配一組 security group,得到一套「看起來一樣」的 staging。migration 先在 staging 驗證再推 production。這解決了驗證場所的問題,但引入了漂移問題 — 下一節說明。

用程式碼描述環境,讓工具複製。把 production 的設定寫成描述檔,用 Terraform 或 OpenTofu 根據同一份描述建出 staging。初始成本比手動複製高(要學工具、寫描述檔),但它保證了手動複製保證不了的一件事:staging 和 production 的結構來自同一份描述,差異只存在於刻意不同的參數(機器規格、備份天數)。這就是 Infrastructure as Code(IaC)的起點。

手動複製的環境會漂移

手動複製的 staging 在建立當天跟 production 一致。一個月後通常不再一致。

漂移的來源是日常操作中的局部調整:staging 的 security group 多了一條規則(某次除錯時加的,事後忘了刪)、production 的 RDS 參數被調過(線上出現慢查詢,DBA 改了 work_mem 但沒同步 staging)、staging 的 IAM role 多了一條 policy(測試新功能時加的,測完沒拿掉)。每一筆差異都很小,小到不值得專門同步,但它們會累積。

漂移引爆的時機跟產生的時機通常隔很遠。一個 migration 在 staging 通過、推到 production 失敗,排查半天後發現是一個月前的參數調整造成的 — staging 的 work_mem 跟 production 不同,剛好影響了這次 migration 的執行計畫。這種因果關係跨越時間的錯誤,排查成本遠高於錯誤本身。

漂移的根源是「兩套環境各自獨立維護」。只要兩份設定各自存在,同步就完全依賴操作者的記憶與紀律,而記憶會衰退、紀律會在壓力下鬆懈。結構性的解法是讓兩套環境共用同一份設定,差異只存在於刻意控制的參數。

同一份描述、不同的參數

IaC 工具消除漂移的方式,是把環境的結構寫成一份 module,用不同的參數值建出不同環境。程式碼只有一份,結構保證相同;差異全部收斂在參數裡,每一處「故意不同」都是明確且可審查的。

一個描述資料庫的 module:

 1variable "instance_class" {
 2  type = string
 3}
 4
 5variable "backup_retention_days" {
 6  type    = number
 7  default = 7
 8}
 9
10resource "aws_db_instance" "main" {
11  engine                  = "postgres"
12  instance_class          = var.instance_class
13  backup_retention_period = var.backup_retention_days
14}

Production 傳入大機器和長備份,staging 傳入小機器和短備份:

 1# production
 2module "database" {
 3  source                = "./modules/database"
 4  instance_class        = "db.r6g.large"
 5  backup_retention_days = 14
 6}
 7
 8# staging
 9module "database" {
10  source                = "./modules/database"
11  instance_class        = "db.t3.small"
12  backup_retention_days = 3
13}

兩個環境跑的是同一段 module 程式碼。引擎版本、連線方式、安全設定完全相同(寫在 module 裡、不是參數),差異只有機器規格和備份天數(刻意透過參數控制)。改動 module 一次、兩個環境同時生效,漂移的空間被結構性消除。

IaC 工具會維護一份 state 記錄,追蹤每個環境裡實際建了哪些資源和它們的屬性。改了程式碼後跑 terraform plan,工具會比對新的程式碼和 state 的差異,列出「會新增 / 修改 / 刪除什麼」。確認差異符合預期後才執行 apply。state 的角色與安全存放方式在模組一:最小可行 IaC 展開,環境的目錄結構與 module 設計在模組四:環境分離與模組化 展開。

環境分離牽出的後續關注點

環境分離解決了「在哪裡驗證」和「為什麼 staging 跟 production 不同」的問題。但多環境運行後,一組後續的操作需求會依序浮現,每一個對應 infra 的一個能力層:

身分與權限隔離。三個環境代表三組資源。如果所有人對所有環境都有完整操作權限,一次誤操作就可能改壞 production。production 的修改權限應該比 staging 嚴格、操作身分應該分開。這是模組二:身分與憑證地基的範圍。

變更審查流程。多人同時操作 infra 時,沒有經過 review 的變更會互相覆蓋。把 infra 變更接上跟應用程式碼相同的 PR 流程 — 開分支、自動跑 plan、review 通過才 apply — 讓每一次改動都有提案、審查和歷史。這是模組七:infra 走 PR 流程的範圍。

機密值管理。資料庫密碼、API key 這些機密值在有版本控制之前可能直接寫在 .env 或 CI 變數裡。一旦有了 IaC 和 git,這些值如果跟著程式碼進了版本歷史,就會隨著每一次 clone 擴散。機密值要存在專用的密鑰管理服務裡,程式碼只持有指向它的參照。這是模組八:治理好習慣的範圍。

可觀測性。三個環境各自需要 log、metric 和告警,這些監控要跟環境本身一起建立,而非等服務中斷後才發現沒有可查的資料。這是模組六:可觀測性與 log 的範圍。

網路邊界。三個環境如果共用同一個網段和防火牆規則,staging 的某個被入侵的服務可能橫向觸及 production 的資料庫。每個環境需要有自己的網路邊界。這是模組三:網路地基的範圍。

這些關注點的共同根源是同一件事:當服務從單人單環境長成多人多環境,原本藏在記憶和手動操作裡的決策,必須變成可描述、可審查、可重建的規則。整套教材的地圖在模組零:infra 是什麼,每個模組各自處理一個能力層。

跨分類引用