接手一個有半套 IaC 的環境,比接手全手動的環境更難處理。全手動環境的規則簡單:所有東西都在 Console,逐一盤點就好。半套 IaC 的環境則有兩套真相並存 — 有些資源由程式碼管理、有些是手動加的、有些曾經由程式碼管理但後來被手動改過。terraform plan 跑出來一長串 diff,哪些是該收進來的手動變更、哪些是該回退的設定漂移、哪些資源根本不在 state 裡,都要逐一判斷。在搞清楚這些之前,任何 apply 都可能覆蓋正在服務客戶的設定。

本篇的操作流程從盤點差距開始,經過 state 健康檢查、drift 收斂、文件重建,到最後排出收斂的優先序。每一步都在不影響線上服務的前提下進行。

state 與現實的差距盤點

盤點的第一步是跑 terraform plan 但不 apply — plan 的輸出就是程式碼描述的狀態與雲端現實之間的完整差距清單。

1terraform plan -no-color > plan-baseline-$(date +%Y%m%d).txt

把這份輸出存進 repo,它是接手時的基線快照。之後每一次收斂動作的效果都用「跟這份基線比少了幾項 diff」來衡量。

三類 diff 的判讀

plan 輸出的每一項 diff 歸屬三類,各自的風險等級與處理方式不同:

diff 類型plan 標記含義風險處理方式
要改~ (update in-place)資源存在於 state 與雲端,但屬性不一致逐項判斷是採納手動變更還是回退
要建+ (create)資源在程式碼裡但雲端不存在通常是前人寫了但沒 apply、或曾 destroy
要刪- (destroy)資源在 state 裡但雲端不存在、或雲端有但程式碼想移除絕對不要盲目 apply — 先確認資源是否仍在使用

「要刪」是最危險的一類。常見成因是:前人在 Console 手動刪了某個資源但沒同步從程式碼移除(state 裡還有紀錄),或者前人在程式碼裡移除了某段 HCL 但沒跑 apply(雲端資源還在、state 記得它)。兩種情況都需要先確認該資源在雲端是否存在、是否仍被服務依賴,再決定是從 state 移除(terraform state rm)還是補回 HCL。

另一個需要留意的標記是 -/+(forces replacement)— 它代表 Terraform 判定這個屬性的變更無法原地更新,必須先刪除再重建。對 stateful 資源(RDS、EBS volume)來說這等於資料遺失,在接手階段看到這個標記要先暫停、查清楚是哪個屬性觸發了 replacement。

哪些資源在 state 裡、哪些不在

terraform state list 列出所有被 IaC 管理的資源。配合 terraform show -json 可以取得更結構化的 managed resource 摘要:

1# state 裡有什麼(清單)
2terraform state list > managed-resources.txt
3
4# state 裡有什麼(結構化摘要:type + name + provider)
5terraform show -json | jq '.values.root_module.resources[] | {type, name, provider}' > managed-summary.json

但 state 只是一份已知的清單 — 雲端上可能還有大量不在這份清單裡的資源。用 CLI 列舉雲端資源跟 state 做比對:

1
2# 雲端上有什麼(以 EC2 + RDS + SG 為例)
3aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text > cloud-ec2.txt
4aws rds describe-db-instances --query 'DBInstances[].DBInstanceIdentifier' --output text > cloud-rds.txt
5aws ec2 describe-security-groups --query 'SecurityGroups[].GroupId' --output text > cloud-sg.txt

用這兩份清單做比對,分成三類:

類別定義下一步
已管理state 裡有、雲端也有處理 drift(上一節的 diff)
未管理雲端有、state 裡沒有評估是否需要 import
孤兒state 裡有、雲端沒有terraform state rm 清除過時紀錄

未管理的資源需要逐一判斷:這個資源是前人刻意排除在 IaC 外的(例如一個還在實驗的測試機),還是應該納管但漏了?判斷依據是它的角色 — security group、IAM role、VPC 這類地基資源應該優先 import;一台跑完就該關的測試 EC2 可以暫時留在手動。

手動比對 state list 與 CLI 輸出的效率有限,driftctl(現由 Snyk 維護、開源)可以自動掃描雲端資源與 Terraform state 的差異,一次列出所有 unmanaged resource。它跟 terraform plan 的差別在於 plan 只看已管理資源的 drift,driftctl 同時涵蓋根本不在 state 裡的資源。兩者互補:先用 driftctl 產出完整的 unmanaged 清單,再用 plan 處理已管理資源的 drift。

state 的健康檢查

state 本身的存放方式決定了後續所有操作的安全性。接手後第一件事是確認 state 的健康狀態。

存放位置

1# 查看 backend 設定
2grep -A 10 'backend' *.tf

如果 backend 是 local(或沒有 backend 設定),state 檔只存在某台機器的磁碟上。這代表如果有第二個人從自己的機器跑 apply,兩人會用不同版本的 state 互相覆蓋。把 state 搬到 remote backend(S3 + DynamoDB lock)是接手後的第一優先事項,做法見IaC 工具選型與 state 地基

加密與版本控制

如果 state 已經在 S3,確認三件事:

1# bucket 有沒有 versioning
2aws s3api get-bucket-versioning --bucket <state-bucket>
3
4# bucket 有沒有加密
5aws s3api get-bucket-encryption --bucket <state-bucket>
6
7# 有沒有 lock table
8aws dynamodb describe-table --table-name <lock-table> 2>/dev/null

versioning 沒開的話,一次壞掉的 apply 寫壞 state 就回不去了。加密沒開的話,state 裡的敏感值(資料庫密碼、private key 輸出)以明文存在 S3。

state 裡的敏感值

state 檔經常包含不該暴露的值。確認 state 有沒有在 Git 歷史裡:

1git log --all --diff-filter=A -- '*.tfstate' '*.tfstate.backup'

如果命中,代表 state 曾經被推進 repo。此時 Git 歷史裡的敏感值已經無法徹底清除(git filter-branchgit filter-repo 可以嘗試,但無法保證所有 clone 都更新)。務實的處理是:列出 state 裡的敏感值,全部輪替。

1# 用 jq 從 state JSON 撈敏感值候選
2terraform show -json | jq -r '
3  [.. | objects | to_entries[] |
4   select(.key | test("password|secret|key|token"; "i"))] |
5  unique_by(.key) | .[] | "\(.key): \(.value)"
6' 2>/dev/null

這個 jq 查詢會遞迴掃描 state JSON 裡所有欄位名稱含 password / secret / key / token 的值。命中的每一筆都要確認是否為真實密鑰、是否需要輪替。

drift 收斂策略

盤點完差距、確認 state 健康之後,逐項收斂 drift。對 plan 輸出的每一項 diff 做一個二選一的決定:採納手動變更(改 HCL 去符合現實),或回退到程式碼版本(讓下一次 apply 把現實改回來)。

採納 vs 回退的判斷

多數 drift 應該採納。前人在 Console 手動改設定通常有一個操作理由(即使沒有記錄下來)— 加了一條 security group 規則可能是為了讓某個新服務連進來,改了 RDS 的 max_connections 可能是為了解決連線數不足。在沒有充分理解這些改動的背景之前,回退它們等於撤銷一個可能正在支撐服務運作的設定。

回退適用的情境是:drift 明顯是誤操作(例如 0.0.0.0/0 打開了不該打開的埠)、或 drift 的屬性是有標準答案的(例如 S3 的 block_public_access 被關掉了)。

操作步驟

1# 1. 刷新 state 到最新雲端狀態(不改資源、只更新 state 的快照)
2terraform apply -refresh-only
3
4# 2. 再跑一次 plan — 刷新後 diff 會減少(純 state 過期的 diff 消失)
5terraform plan -no-color > plan-after-refresh.txt
6
7# 3. 對剩餘的 diff 逐項處理
8#    採納:改 HCL 讓程式碼跟現實一致 → plan 確認該項 diff 消失
9#    回退:不改 HCL、讓 apply 把現實改回程式碼版本 → 先確認影響

-refresh-only 是安全的操作 — 它只更新 state 裡的屬性快照,不會改動任何雲端資源。但它會把手動變更「記進」state,讓後續 plan 的 diff 只剩程式碼與 state 的差異(而非程式碼與雲端的差異)。刷新後 plan 的 diff 更精確、更少、更容易逐項處理。

import 未管理的資源

對未管理的資源,用 import 區塊一次處理一個,每次 import 後都跑 plan 確認零新增 diff:

1import {
2  to = aws_security_group.legacy_app
3  id = "sg-0abc123def456"
4}
1# 生成對應的 HCL
2terraform plan -generate-config-out=generated_legacy_app.tf
3
4# 確認生成的 HCL 跟現實一致
5terraform plan
6# 預期:只有 import 動作、沒有 change/destroy

生成的 HCL 需要人工確認 — 有些屬性是雲端自動設的預設值,Terraform 會把它們全部列出來,造成 HCL 冗長。移除純預設值的屬性、只保留有意義的設定,讓 HCL 反映設計意圖而非雲端預設。

對於大量未管理資源需要一次性反推 HCL 的情境,Former2 可以從現有 AWS 資源批量生成 Terraform code。它掃描帳號裡的資源、產出對應的 HCL,品質不完美(命名會用資源 ID 而非有意義的名稱、屬性可能包含大量預設值),但作為起點比從零手寫每個資源快得多。產出後仍需逐檔清理命名與移除預設值。

文件重建

接手的環境通常沒有文件、或者文件已經過時到比沒有更糟(記載的是兩個版本前的架構)。文件重建的目標是讓下一個接手者不需要重複同樣的盤點過程,而非追求一份完美的架構文件。

來源

能重建的資訊來源有限,但每個都有價值:

來源能找到什麼
Git logcommit 訊息裡可能有「為什麼這樣改」的線索
PR 歷史review 討論裡可能有決策脈絡
HCL 程式碼變數命名、module 結構反映架構意圖
CloudTrail過去 90 天的 API 呼叫紀錄
帳單哪些服務在花錢、量級多大
terraform-docs從 HCL 自動產出 module 文件(inputs/outputs)
Inframap從 state 產出依賴關係視覺化圖

terraform-docs 用一條指令就能從現有 HCL 產出每個 module 的 inputs、outputs 和 resources 清單,省去手動整理 module 介面的時間。Inframap 從 state 或 HCL 產出依賴關係圖,比 terraform graph | dot 好用的地方在於它自動過濾掉 provider 和 data source 的噪音,大型 state 也能產出可讀的圖。

最小可行文件

寫一份 INFRA-STATE.md 放在 repo 根目錄,包含:

  • 管理範圍:哪些資源由 IaC 管理、哪些是手動的、為什麼手動的沒有 import(例:還在實驗、不穩定、計畫廢棄)
  • 已知 drift:目前 plan 輸出裡還有哪些未處理的 diff、每個 diff 的處理方向(採納/回退/待調查)
  • state 存放位置:backend 設定、bucket 名稱、lock table 名稱
  • credential 狀態:有幾把 access key、哪些還在用、上次輪替時間
  • 接手日期與盤點結果:盤點時的資源數量、覆蓋率(managed / total)

這份文件不需要精美,需要的是準確且持續更新。每次收斂一項 drift 或 import 一個資源,就更新對應的段落。前任團隊的知識已經不在了,這份文件取代它成為環境的記憶。

收斂到完整 IaC 的優先序

把整個收斂過程排成四個階段,每個階段都能獨立交付價值:

階段目標交付物預估時間
1state 健康remote backend + 加密 + versioning + lock1-2 天
2地基 importsecurity group、IAM role、VPC 納管1-2 週
3drift 收斂已管理資源的 plan 歸零1-2 週
4覆蓋率提升應用層資源逐批 import持續

每個階段的驗證方式相同:terraform plan 的輸出是否比上一階段乾淨。階段一完成後,plan 的可信度才成立;階段二和三是把 plan 的 diff 清到零;階段四是擴大 plan 的管轄範圍。

每一步操作之前都先備份 state:

1# 手動備份 state(不論 bucket 有沒有 versioning 都先拉一份)
2terraform state pull > state-backup-$(date +%Y%m%d).json

state 操作失敗時的回退路徑是 terraform state push state-backup.json 從備份還原 — 資源本身不受影響,只是工具對現實的記憶回到上一個正確的版本。state push 是覆寫操作,只在確認備份版本正確時使用。

需要搬移資源在 state 裡的位址時(例如重構 module 結構),優先用 moved {} 區塊而非 terraform state mvmoved 是宣告式的、寫在 HCL 裡、可以被 PR review、plan 時會顯示搬移動作。state mv 是指令式的、直接改 state、沒有 review 機制、操作紀錄只在 CLI 歷史裡。

1moved {
2  from = aws_security_group.old_name
3  to   = module.network.aws_security_group.app
4}

跨分類引用