infra 的責任邊界、成熟度階梯與 day 1 鐵律
基礎設施(infrastructure,簡稱 infra)是承載應用程式的那層資源與規則:運算、網路、身分、儲存、可觀測性,以及定義它們如何被建立、變更、回收的治理機制。它的責任是讓應用程式有一個可被信任、可被重建、可被審計的執行環境。本篇建立的責任邊界、成熟度階梯與 day 1 鐵律,是後續所有 infra 模組共用的心智模型,其他章節會直接引用這裡定義的詞彙。
infra 的責任邊界
infra 承擔的是「應用程式之下、作業系統之上」那層共享資源的供應與治理。把責任拆成五個面向比較好對齊:每一面都有自己的失效模式,混在一起談會讓判斷失焦。
運算(compute)
運算負責「程式跑在哪、用多少資源、怎麼擴縮」。它的衡量點是容量與彈性:流量尖峰時能不能長出更多實例、閒置時能不能縮回去省錢。一台手動開的 VM 也是運算資源,差別只在它是否被納入可重建的描述。
運算涵蓋的光譜從 VM(EC2 instance)到容器(ECS task、Kubernetes pod)到 serverless function(Lambda)。抽象層級越高,infra 需要直接管理的細節越少——VM 要管 OS 更新與磁碟擴容,容器只需管映像與編排,serverless 幾乎只管程式碼與觸發條件。但抽象層級不改變運算的基本問題:它跑在什麼網路裡、用什麼身分存取其他資源、出了問題怎麼查。這些「接線」正是 infra 其他四個面向的職責。
運算層常見的失效模式有兩類。第一類是容量不足:流量上來了但 auto-scaling 沒設或設錯,新實例來不及啟動就超時,表現為使用者端的 502 或延遲飆高。這類事故的排查路徑是先看 scaling policy 的觸發條件與 cooldown 是否跟真實流量匹配,再看運算節點的啟動時間是否在可接受的範圍內。第二類是殭屍資源:跑完的測試機器沒關,停掉的開發環境仍掛著 EBS volume,閒置著燒錢卻沒人發現。殭屍資源的判讀訊號是 CPU 使用率長期趨近於零且沒有對外連線——靠定期盤點加上 tag 過濾最能系統性地收斂,詳見模組八:治理好習慣。
網路(network)
誰能連到誰、流量走哪條路?這兩個問題的答案在網路層。VPC 切分、子網路、route table、security group 把可達性變成明確規則,而非預設全通。邊界沒畫清楚時,一個被入侵的服務就能橫向打穿整個環境。
網路的失效模式分兩極。過度開放的代價是安全事故:一條 security group 入站規則寫成 0.0.0.0/0 允許任何來源連到資料庫埠(5432、3306),等於把密碼驗證當作唯一防線,而暴力嘗試的掃描流量在公網上是持續的。意外隔離的代價是服務中斷:有人改了一條 route table 的預設路由,導致 private subnet 的服務失去出站能力——拉不到外部套件、連不上第三方 API,服務看起來在跑但功能全部退化。兩者在平時都不被注意,事故發生時才現形。排查網路問題的第一步通常是「這個封包走的那條路上,每一層有沒有放行」——route table → NACL → security group,逐層確認。網路地基的系統性設計在模組三:網路地基展開。
身分與憑證(identity)
即使網路邊界畫得完美,一把權限過大的 access key 外洩了,攻擊者可以用 API 繞過所有網路規則直接操作資源——身分與憑證是五個面向中失守代價最高的一層。它的職責是讓人、服務、CI pipeline 各拿剛好夠用的權限(最小權限),並確保憑證有明確的生命週期。
身分層的失效模式有兩類常見形態。權限擴散指的是一個 role 隨時間累積了遠超本職所需的權限——每次需求都加一條新的 action,卻從來沒人收斂已經用不到的舊權限。典型場景是一個 CI role 一開始只需要讀 S3、後來加了建 ECR image、再後來加了改 RDS parameter group,半年後這個 role 的 policy 有三十幾行 action,其中只有不到一半還在使用。憑證散落則指同一把 access key 被複製到越來越多地方——CI 環境變數、開發者筆電的 ~/.aws/credentials、某段部署腳本裡的 hardcode。每多一個副本就多一個外洩點,而外洩後的回退要找出所有副本同步輪替,這在手動環境裡幾乎做不到。這兩者的完整處理在模組二:身分與憑證地基。
儲存(storage)
運算可以隨時重建,資料一旦遺失通常無法重來——這條分界線劃出了儲存層的職責。備份策略、版本保留、刪除保護構成儲存的三道防線,每一道都要在出事前就驗證過,而非事後才發現沒開。
儲存涵蓋從物件儲存(S3)到區塊儲存(EBS)到受管資料庫(RDS)的底層磁碟。這些資源的共同特性是它們承載狀態,而狀態的失效模式跟運算不同——運算節點掛了重開一台就好,資料刪了就是刪了。具體的失效場景包括:一台 RDS 沒開刪除保護(deletion protection),有人清理開發資源時誤刪了 production 的資料庫;一個 S3 bucket 沒開 versioning,一段錯誤的腳本把整批物件覆寫成空內容,回不去了;一份 EBS snapshot 只保留了 3 天,周五出事、周一上班才發現,快照已經被自動清除。把刪除保護、備份保留天數、版本控制這些防線寫進 IaC,讓保護策略本身成為可審查、可追蹤的程式碼,是模組五:核心服務上 IaC 的重點之一。
可觀測性(observability)
可觀測性負責「系統現在發生什麼、出事後查得到嗎」。它把 log、metric、trace 變成可查詢的事實來源。這層常被當成事後再補的附加品,但它和被它觀測的服務應該同生命週期一起建立。
後補的可觀測性有一個結構性缺陷:出事之前沒有監控,代表出事當下最關鍵的那段資料不存在——知道服務「現在壞了」,但看不到「壞之前發生了什麼」。CPU 從什麼時候開始上升、錯誤率從哪個部署開始出現、某個 API 的延遲從什麼時候劣化——這些問題的答案需要連續的歷史資料,而歷史資料只能在事前就開始收集。另一個常見失效是 alarm 設了但通知沒有接到人:alarm 綁到一個 SNS topic,topic 的 subscription 是某個已停用的 email,值班工程師從頭到尾沒收到通知,直到使用者自己回報。可觀測性的 IaC 描述在模組六:可觀測性與 log。
五面的共同根源
這五面的共同點是:它們都不是應用功能,使用者看不到,但任何一面崩了,上面的功能全部跟著崩。這正是地基隱形的根源——它的價值只在缺席時被感知。
地基為什麼隱形
infra 的特性是「運作正常時完全不被感知,失效時才一次現形」。地基鋪得好的環境,工程師每天部署、擴縮、改設定,卻幾乎不會意識到底下有一層在支撐,因為它安靜地做對了每件事。這種隱形讓 infra 在資源排序上長期吃虧:看得見的功能有人催,看不見的地基沒人提。
現形的時刻通常是環境失效的時刻,而且會在不同規模的團隊裡反覆出現——差別只在影響範圍。
沒有描述檔的資源在需要重建時,必須從 Console 逐頁反推它的設定——屬於哪個 VPC、掛了哪些 security group、用了什麼 IAM role。這些資訊散落在不同頁面,拼湊一個資源的完整設定要半天,而且每個找到的設定都帶著「不確定是不是還有漏掉的」疑慮。
一次安全稽核要求列出所有對外開放的連接埠,才發現 security group 散落在三個帳號、沒人說得清哪條規則還有用。有些規則是兩年前為了某個已經下線的服務開的,但沒人敢刪——萬一那條規則還被某個看不到的服務依賴呢?稽核結果是「我們列出了 37 條規則,其中 12 條無法確認是否仍在使用」。
一台資料庫磁碟滿了要擴容,才發現它從來沒進過任何納管流程。改它的 instance class 或磁碟大小,在 Console 上操作意味著可能觸發重啟,而這台資料庫是 production 唯一的寫入端點。操作時無法預測影響範圍,因為沒有可對照的描述檔;不操作則等著服務因為磁碟寫不進去而停擺。
這些場景有一個共同的累積模式:每一次「這次先手動救」的決定本身是合理的——救火當下沒有時間走流程。問題在於這些決定的殘留會堆疊。手動改了一條 security group 但沒記錄,下一個月又手動改了另一條,半年後沒人說得清哪些規則是原始設計、哪些是臨時補丁。每一次救火都在增加下一次排查的成本,而這個成本在平時完全隱形,只在下一次事故裡一次性浮現。
隱形債務的徵兆很直接:當團隊開始用這些語言描述某項資源,債就已經在累積——「不敢動那台機器」代表依賴關係不可見;「只有某某知道怎麼改」代表知識沒有沉澱在程式碼裡;「上次碰它好像出過事」代表變更缺乏 review 與回退機制;「那個先別管,能跑就好」代表技術債被刻意延後、沒有 tripwire。
地基的價值無法在平順時被看見,只能在它缺席的代價裡被回推,所以它需要一條和功能不同的論證路徑——這條路徑怎麼用商業語言講給上層聽,是模組九:怎麼把 infra 推動起來的主題。
day 1 鋪地基與事後補的成本差
在資源剛開始長出來時就用程式碼描述它,和等環境長大後再回頭納管,兩者的成本差距是非線性的。早期鋪地基的成本接近固定:寫一份描述檔、建一個 state、設一條 pipeline,環境只有三五個資源時這些都很輕。事後補的成本則隨資源數量、相互依賴與「不確定能不能動」的恐懼一起放大。
事後納管的痛具體長這樣:一個手動建出來的資源要納入 IaC,得先把它當前的真實狀態完整反推成程式碼(import)。這個過程要逐欄比對 Console 上的設定——一個 RDS instance 的 parameter group、backup retention、storage type、multi-AZ 設定,Console 上看到什麼 HCL 裡就得寫什麼,漏一個欄位下次 apply 就可能把線上設定改掉。資源彼此有依賴時,納管順序也得排——一個 security group 引用另一個 security group 作為 source,兩個都還沒進 IaC 時,要決定哪個先 import、程式碼怎麼暫時處理另一個的引用。當這些手動資源還是線上服務正在用的,整個納管過程等於在開著的引擎上換零件。
import 之後的第一次 plan 是真正的考驗。如果 HCL 跟雲端現實有任何落差——哪怕只是一個 tag 的大小寫不同、或某個欄位在 Console 上有預設值但 HCL 裡沒寫——plan 會把那些落差列為需要修改的變更。在 stateless 資源上這只是小修正,在 production 的 RDS 上如果 plan 判定需要 replace(先刪後建),那就是一個會造成資料遺失的操作,必須在 apply 之前被攔截。手動環境累積的資源越多,這類 plan 裡的「驚喜」越多,整理每一個驚喜都要時間和注意力。這就是事後補的成本隨時間複利的具體機制。
務實的判準不是「day 1 就把所有東西寫成完美的 IaC」,而是「day 1 就讓新長出來的資源預設走可重建的路徑」。多數早期環境合理的選擇是讓地基類資源(網路、身分、state 本身)從一開始就在程式碼裡,而把還在高速試錯的應用層資源留一點手動彈性,等形狀穩定再納管。
哪些資源屬於「地基類」的判斷依據是回頭改的代價。VPC 的 CIDR 一旦確定、裡面的 subnet 都分配出去了,要改地址範圍幾乎等於重建整個網路。IAM 的 role 和 policy 一旦被多個服務引用,改動任一條的影響範圍是整個授權模型。state 後端的 bucket 和 lock table 如果第一天沒設好、用了本地 state,後續要搬到 remote backend 要處理 state migration——而 state 搬遷失敗可能讓工具失去對所有資源的記憶。這類地基的回頭成本是階梯式的(一旦長歪就很貴)。應用層資源的回頭成本是線性到多項式的(越晚越貴但不至於一步跳崖)。差別在於:前者的回頭成本固定,後者隨時間複利。模組一:最小可行 IaC 會示範這條最小路徑怎麼落地。
成熟度階梯
infra 的成熟度可以排成一條從「全手動」到「全程式碼治理」的階梯,每一階用「資源怎麼被建立與變更」來定義。這條階梯是全系列共用的座標:後續模組描述某個能力時,會說它對應到哪一階,所以這裡先把刻度釘清楚。
| 階段 | 名稱 | 資源怎麼被建立 | 真實狀態的來源 | 對應模組 |
|---|---|---|---|---|
| 0 | Console 手動 | 在網頁介面點選建立 | 只存在於雲端,無描述 | 模組負一 |
| 1 | 腳本化 | 用 CLI 或腳本建立 | 腳本,但無狀態追蹤 | — |
| 2 | 宣告式 IaC | 寫描述檔、由工具 apply | state 檔記錄已建資源 | 模組一 |
| 3 | 環境分離 | 同一份模組套用多環境 | 各環境獨立 state | 模組四 |
| 4 | PR 流程治理 | 變更走 PR、CI 自動 plan | state + 版控歷史 + 審查紀錄 | 模組七 |
第 0 階:Console 手動
所有環境的起點,也是該優先離開的一階。特徵是真實狀態只存在雲端,沒有任何離線描述,所以無法 review、無法重建、無法回答「這個環境長什麼樣」。它不是錯誤的起點,是還沒鋪地基的起點。
問自己兩個問題:「我們的 VPC 長什麼樣」能不能不打開 Console 就回答?「上一次 security group 什麼時候改過」能不能不翻 CloudTrail 就查到?兩題都要靠手動查,就還在第零階。停在這一階的環境怎麼盡量做好,見模組負一:還沒有 infra 的手動環境。
第 1 階:腳本化
把建立動作寫成 CLI 或 shell 腳本,比手動可重複,但腳本只描述「怎麼建」,不追蹤「現在有什麼」。重跑同一支腳本可能重複建立或報錯,因為它不知道資源已經存在。
這一階的常見陷阱是誤以為「有腳本就等於有 IaC」。差別在狀態這塊地基——一份 setup.sh 能把環境從零建起來,但它回答不了「跑完後環境裡有哪些資源」「哪些資源是這個腳本建的、哪些是之前手動建的」「如果腳本裡的設定改了,下次重跑會不會把現有資源改壞」。這些都是 state 要解的問題。辨認自己在哪一階的方式是試一次:刪掉某個資源後重跑腳本,能自動把它補回來而不影響其他資源,那就已經在接近第 2 階的行為;重跑會報「already exists」錯誤或重複建立,就還在第 1 階。
第 2 階:宣告式 IaC
地基真正成形的一階:用 Terraform / OpenTofu 這類工具寫下「環境應該長什麼樣」,工具負責比對現況與描述、算出差異再套用。state 檔在這裡誕生,成為「目前納管了哪些資源」的事實來源。
怎麼知道自己在第 2 階
試回答一個問題:能不能從程式碼把整個環境在另一個帳號重建出來?「可以,apply 一次就好」代表 IaC 覆蓋率足夠。「大部分可以,但有些東西還是要手動補」——那些手動補的部分就是下一批該 import 的資源。另一個觀察角度:跑 terraform plan 時如果出現大量 drift(state 與現實不符),代表有人繞過 IaC 直接在 Console 改東西,Console 唯讀紀律在鬆動。工具選型與 state 管理的具體做法在模組一:最小可行 IaC。
第 3 階:環境分離
把同一份描述模組化,套用到 dev / staging / production 等多個環境,各自獨立 state。它解決的問題是「在 staging 驗證過的變更,能用同一套描述安全地推到 production」。
判讀訊號:dev 和 prod 的設定差異是否全部表達在參數裡、還是散落在不同的 code 分支中。如果 prod 目錄裡有一段 dev 目錄沒有的 code,那段 code 就是從來沒在低環境驗證過的生產設定——這是漂移的起點。另一個訊號:如果部署到 staging 和部署到 production 走的是兩條不同的 pipeline 或手動流程,代表環境分離只做了一半。完整切法在模組四:環境分離與模組化。
第 4 階:PR 流程治理
把 infra 變更接上和應用程式碼相同的協作流程:變更走 pull request,CI 自動跑 plan 把預期差異貼上來,人審查後才 apply。到這一階,infra 的每次變更都有提案、審查、歷史與回退點。
用兩個問題定位:任意一次 infra 變更,能不能在 git log 裡找到對應的 PR、看到 plan 輸出、知道誰 review 的?如果某些變更是直接在 main 上 push 的、或是某人在本地 apply 的,代表流程有漏洞。更進一步:主要負責 infra 的人請假時,其他人能不能只靠讀 repo 就理解現狀並安全地改一個小設定?完整的治理護欄在模組七:infra 走 PR 流程。
階梯不是單向命令
這條階梯是一把對齊現況的尺,用來判斷某項資源該停在哪一階,不是越高越好的單向命令。停在哪一階的依據是務實節奏——一個只有三個人、五個資源的早期團隊,強上第四階的 PR 流程,review 成本可能超過它擋下的風險。反過來,一個已經有二十個人在改 infra 的團隊,停在第二階不走 PR,就是在賭每次 apply 都不會出錯。
早期新創的務實節奏
早期團隊的合理目標是「地基類資源先上到階梯第 2 階,應用層資源容許暫時留在低階」,而不是一步衝到第 4 階。資源有限、需求還在劇烈變動的階段,把全部資源都套上完整治理流程,收益正的機率不高——治理的固定成本會壓到本來就稀缺的開發頻寬。
判斷節奏的依據是「這項資源的形狀穩不穩、動它的代價高不高」:
| 資源類型 | 形狀穩定度 | 改錯代價 | 判準 |
|---|---|---|---|
| VPC / subnet | 高 | 極高 | day 1 進 IaC |
| IAM role / policy | 高 | 極高 | day 1 進 IaC |
| state backend | 高 | 極高 | day 1 進 IaC |
| RDS(已穩定的) | 中高 | 極高 | 形狀確定後立刻進 |
| 對外 LB | 中 | 高 | 開始有流量就進 |
| 應用層 EC2 / ECS | 低到中 | 中 | 開始被依賴或第二人要改時進 |
| 測試用臨時資源 | 低 | 低 | 可以留在手動,設 tag 方便清理 |
day 1 鐵律
網路拓撲、身分權限、state 後端這三類地基資源,一旦長歪回頭改的代價極高,值得 day 1 就進 IaC——這是少數接近「該照做」的硬判準,因為它牽涉安全邊界:
- VPC / subnet:CIDR 一旦確定、subnet 分配出去,改地址範圍幾乎等於重建整個網路(見模組三)
- IAM role / policy:權限模型被多個服務引用後,改動任一條的影響範圍是整個授權體系(見模組二)
- state backend:state 的存放位置與鎖機制如果第一天沒設好,後續 state migration 失敗可能讓工具失去對所有資源的記憶(見模組一)
反過來,一個還在每週改三次規格的功能用的運算資源,過早凍進嚴格流程反而拖慢試錯。這時容許它手動,但設一條 tripwire:當它開始被線上流量依賴、或開始有第二個人需要改它時,就是把它納管的時機。
tripwire 的操作方式是在建立資源時就決定「觸發納管的條件」,而非等到某天靈感來了才想到要 import。例如:一台跑開發用途的 EC2,建立時在內部文件標記「當這台開始接 staging 或 production 流量時納管」;一個 S3 bucket 正在測試用,標記「當開始存正式用戶上傳的檔案時納管」。tripwire 讓「什麼時候該進 IaC」變成一個可追蹤的條件,而非一個持續被拖延的意願。
兩個反向誤判
過度設計和放任手動是這個階段的兩個反向誤判。
過度設計的訊號:環境只有五個資源,卻已經有多層抽象模組和還用不到的多環境結構,維護抽象的時間比省下的時間多。常見的觸發是照搬最佳實踐文章的全部教條——三層 module 嵌套、Terragrunt 全家桶、每個資源都有 for_each——結果團隊裡只有一個人看得懂這套結構。對這類過度設計的自測是:「如果今天不做這個抽象,三個月後補的成本是多少?」如果答案是花一小時就能補,那就三個月後再說。
放任手動的訊號:每次有人問「這個怎麼建的」都只能去翻某個人的記憶,地基債務在無聲累積。放任手動的常見藉口是「我們還在早期、先把功能做出來再說」——這句話在創業前三個月合理,但如果三個月後還在這麼說、而環境已經有二十個資源、三個人在改,債就開始複利了。
務實節奏就是在這兩者之間,讓地基先穩、讓應用層保留試錯彈性,再隨著形狀固定逐項往階梯上推。
跨分類引用
- → 模組負一:還沒有 infra 的手動環境:階梯第 0 階的環境怎麼盡量做好
- → 模組一:最小可行 IaC:地基資源跨上成熟度階梯第 2 階的最小路徑
- → 模組二:身分與憑證地基:身分層的權限收斂與憑證生命週期
- → 模組三:網路地基:網路層的隔離、路由與 security group 設計
- → 模組四:環境分離與模組化:成熟度階梯第 3 階的切法
- → 模組五:核心服務上 IaC:運算與儲存資源的 IaC 描述
- → 模組六:可觀測性與 log:可觀測性同生命週期管理
- → 模組七:infra 走 PR 流程:成熟度階梯第 4 階的治理護欄
- → 模組八:治理好習慣:殭屍資源盤點與 tagging 規範
- → 模組九:怎麼把 infra 推動起來:地基的價值怎麼用商業語言講給上層聽