動手前的前提

以下步驟是寫第一行 IaC 之前需要就位的前置條件。如果已經備妥可以跳過。如果是第一次接觸雲端帳號,先讀拿到雲端帳號的第一天做安全底線和帳號現況判讀。

雲端帳號。需要一個 AWS 帳號(或 GCP / Azure,本模組以 AWS 為主要範例)。註冊完成後立刻對 root 帳號啟用 MFA(Multi-Factor Authentication)——root 帳號是整個雲端環境的最高權限,沒有 MFA 等於大門沒鎖。啟用路徑:AWS Console → 右上角帳號名稱 → Security credentials → Multi-factor authentication (MFA) → Assign MFA device。日常操作用 IAM user 或 IAM Identity Center 登入,root 帳號只在需要 root-only 操作時使用。

本機工具。安裝 IaC CLI(Terraform 或 OpenTofu)和雲端 CLI(AWS CLI):

1# macOS
2brew install opentofu awscli
3
4# Arch Linux(opentofu 和 aws-cli-v2 在 AUR,需要 AUR helper)
5yay -S opentofu-bin aws-cli-v2
6
7# 驗證安裝
8tofu --version
9aws --version

雲端認證。本機需要能對雲端 API 認證。最直接的方式是用 AWS CLI 設定 credentials:

1aws configure
2# 輸入 Access Key ID、Secret Access Key、預設 region(如 ap-northeast-1)

這組 access key 來自 IAM user。如果帳號裡還沒有 IAM user,到 AWS Console → IAM → Users 建立一個、附加 AdministratorAccess policy、在 Security credentials 分頁建立 access key。正式環境應該用 SSO 或 short-lived credentials 取代長期 key(模組二會展開),但起步階段一組 IAM user key 足以讓 tofu apply 跑起來。

Git repo。IaC 程式碼從 day 1 就應該在版本控制裡——這是模組零「可重建路徑」的落地前提。建一個 Git repo,後續所有 .tf 檔都放在這裡:

1mkdir infra && cd infra
2git init
3echo '.terraform/' > .gitignore
4echo '*.tfstate'  >> .gitignore
5echo '*.tfstate.*' >> .gitignore
6git add .gitignore && git commit -m "init: gitignore for terraform"

.gitignore 排除 .terraform/(provider 快取)和 *.tfstate(state 檔含敏感值,存放策略見下方 remote state 段落)。


踏上成熟度階梯(從全手動到全程式碼治理的五階分級)第二階(宣告式 IaC,也就是 state 檔誕生那一階)的最小路徑,從兩件事開始:選對工具、把 state 管好。工具決定用什麼語言描述基礎設施,state 則是工具對雲端現實的唯一記憶。這份記憶存在哪、怎麼保護、怎麼防止並行寫壞,是整套 IaC 能不能站穩的地基。

IaC 工具選型:宣告式狀態管理 vs 程式語言抽象

IaC 工具的核心職責是把「我要的基礎設施長什麼樣」描述成可版本控制的程式碼,再由工具負責算出現況與目標的差異並收斂。市場上的工具分成兩條路線,差別落在「用什麼語言描述」與「狀態由誰持有」這兩個軸上,而非功能多寡。

宣告式 DSL 路線

第一條路線的代表是 Terraform 與其開源分支 OpenTofu。寫的是 HCL(HashiCorp Configuration Language),描述的是資源的最終樣貌,工具自己維護一份 state 來追蹤每個資源的真實 ID 與屬性。

 1resource "aws_s3_bucket" "artifacts" {
 2  bucket = "acme-deploy-artifacts"
 3}
 4
 5resource "aws_s3_bucket_versioning" "artifacts" {
 6  bucket = aws_s3_bucket.artifacts.id
 7  versioning_configuration {
 8    status = "Enabled"
 9  }
10}

這段 HCL 描述的是「一個開了 versioning 的 S3 bucket 應該存在」。第一次 apply 時工具建立它,之後每次 apply 時工具比對 state 與雲端現況,只做差異收斂。讀的人看 HCL 就知道最終結果長什麼樣,不需要在腦中追蹤執行順序。

這條路線適合團隊成員背景混雜、需要讓非專職後端的人也能讀懂 infra 定義的情境 — HCL 的閱讀門檻低,diff 直觀,review 時看得出「這個 PR 會新增一個 RDS、改掉一條 security group」。缺點是 HCL 的表達力有限:遇到需要大量條件分支或動態生成的場景時,語法會變得笨拙,countfor_eachdynamic 區塊很快就堆出難以閱讀的嵌套。

程式語言路線

第二條路線的代表是 AWS CDK 與 Pulumi。寫的是 TypeScript、Python、Go 這類語言,靠迴圈、函式、類別來生成資源。這條路線適合 infra 邏輯本身複雜、需要大量條件分支與抽象複用的團隊,例如要根據環境清單動態生成數十組對稱資源。

代價是 review 難度上升。一段 for 迴圈展開後到底建了哪些東西,得在腦中執行程式才看得出來,diff 不再等於變更本身。一個抽象類別改了一行建構子參數,展開後可能影響所有繼承它的資源,而 PR diff 上只看到那一行。對跨職能 review(PM、SRE、安全團隊都要看的變更)來說,這是可感知的閱讀成本。

CDK vs Pulumi:狀態由誰持有

CDK 與 Pulumi 同屬程式語言路線,但「狀態由誰持有」這個軸把它們再分開。

CDK 把程式碼 synth 成 CloudFormation 模板,再交給 CloudFormation 服務端執行與追蹤。state 由 AWS 代管 — 沒有一份 tfstate 檔要自己存放、加密、回捲,也不需要額外的鎖表來防並行。這份「狀態維運外包給雲端」正是 CDK 在 AWS 生態內的賣點之一。代價是綁定 CloudFormation 與單一雲 — CloudFormation 的更新速度、resource coverage、錯誤訊息品質都由 AWS 控制,團隊的 debug 能力受限於 CloudFormation 的回報粒度。

Pulumi 走另一邊:它維護一份自己的 state,預設交給 Pulumi Cloud 託管,也能改用 S3 之類的後端自管。形態上更接近 Terraform 的 state 模型,state 的存放、保護與並行控制重回團隊手上。同一條程式語言路線,選 CDK 等於把 state 責任讓給雲端,選 Pulumi 則保留對 state 落點的掌控。

選型判準

選型看的是團隊組成與變更的審查需求,可以用一張決策表歸納:

判準宣告式 DSL(Terraform / OpenTofu)程式語言(CDK / Pulumi)
diff 可讀性HCL diff 即是資源變更程式碼 diff,要展開才知道結果
跨職能 review適合需要讀者熟悉程式語言
抽象複用有限,靠 module + for_each完整程式語言能力
state 管理自管或託管皆可CDK 交 AWS;Pulumi 自管或託管
跨雲provider 生態支援多雲CDK 限 AWS;Pulumi 支援多雲
學習曲線HCL 語法簡單,概念模型需適應語言本身熟悉,IaC 概念需適應

若多數變更要跨職能 review、希望 diff 一眼可讀,宣告式 DSL 較划算;若 infra 由專職平台團隊維護、抽象複用的收益大於審查透明度的損失,程式語言路線較划算。

Terraform 與 OpenTofu 之間,OpenTofu 是授權變更後社群分叉出的相容實作,HCL 與 provider 生態幾乎共用;選擇主要看對授權條款與治理模式的偏好,技術判準在這一階沒有實質差異。本模組後續一律以 HCL 示意,換成任一宣告式工具判準仍成立。

上述兩條路線之外,還有兩類工具走不同的運作模型。Kubernetes-native 路線(代表是 Crossplane)用 CRD 描述雲資源、由 controller 持續收斂,state 由 Kubernetes 的 etcd 持有,適合已經重度投入 Kubernetes 的團隊。Serverless-first 框架(代表是 SST)把部署與 IaC 合一,適合全 serverless 架構。這兩條路線的 state 模型與 CLI 驅動的 plan/apply 流程不同,本系列不展開。

state 是工具對現實的唯一記憶

state 是 IaC 工具用來記錄「上一次 apply 之後,每個資源在雲端真實長什麼樣」的快照。它的作用是讓工具能算出「現況」與「目標」之間的最小差異。沒有 state,工具每次都得把所有資源重新查一遍才知道該不該動,而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。

一份 state 的實際內容大致長這樣(簡化版):

 1{
 2  "resources": [
 3    {
 4      "type": "aws_s3_bucket",
 5      "name": "artifacts",
 6      "instances": [
 7        {
 8          "attributes": {
 9            "id": "acme-deploy-artifacts",
10            "arn": "arn:aws:s3:::acme-deploy-artifacts",
11            "bucket": "acme-deploy-artifacts",
12            "tags": { "env": "prod", "owner": "platform" }
13          }
14        }
15      ]
16    }
17  ]
18}

state 裡通常含有資源的真實 ID、相依關係,以及部分敏感屬性 — 例如資料庫的初始密碼、private key 的輸出值、加密金鑰的 ARN。這帶來兩條硬邊界,違反任一條都會在未來製造代價高昂的事故。

state 絕不能進 git

state 含明文敏感值,一旦推進版控就等於把密碼寫進每個 clone 的歷史裡。事後 rotate 密碼也清不掉 git 歷史 — 因為 git 是 append-only 的,舊版本的 state 永遠留在 commit 裡,除非用 git filter-branchgit filter-repo 重寫整條歷史(這本身是一個破壞性操作,會影響所有已經 clone 的副本)。

.gitignore 裡搜尋 *.tfstate*.tfstate.backup——如果這兩行不在,state 有進版控的風險。在 repo 根目錄執行一次搜索確認:

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

如果有任何結果,代表 state 曾經被 commit 過,那些 commit 裡的敏感值已經暴露。

state 不能只放本地

本地 state 的失敗模式是它把整份基礎設施的記憶綁在一台筆電上 — 換人接手、換台機器、或多人同時 apply 時,記憶就分裂了。

具體場景:工程師 A 在自己的筆電 apply 了一次,state 記住「已經建了 3 個 security group」。工程師 B 在另一台筆電上拉了同一份 code,但她的本地沒有 state(或有一份過時的 state),apply 時工具以為那 3 個 security group 不存在,又建了 3 個重複的。更糟的場景是 B 的 state 比 A 舊,工具對比後認為 A 後來新增的 security group「不在記憶裡、是多餘的」,於是 apply 時把它們刪掉 — 而 A 還以為那些規則還在保護服務。

這兩條邊界共同指向同一個結論:state 需要一個團隊共享、有版本、有存取控制、且能防止同時寫入的存放處。這就是 remote state backend 要解的問題。

remote state backend:自管 vs 託管

remote state backend 是把 state 從本地移到團隊共享儲存的機制,它要同時滿足三件事:持久保存、防止並行寫入衝突、以及保護敏感內容。達成方式分成自管儲存與託管服務兩種,差別在維運責任落在誰身上。

自管 backend

自管路線以雲端物件儲存加鎖機制為典型組合。以 AWS 為例,state 檔放 S3、用一張 DynamoDB 鎖表防止兩個人同時 apply:

1terraform {
2  backend "s3" {
3    bucket         = "acme-tf-state"
4    key            = "prod/network/terraform.tfstate"
5    region         = "ap-northeast-1"
6    encrypt        = true
7    dynamodb_table = "acme-tf-lock"
8  }
9}

這段設定的每一項都對應前一節的一條邊界:

encrypt = true 讓 state 在 S3 落地時加密,回應「state 含敏感值」的風險。加密用的是 S3 的 server-side encryption,搭配 KMS key 可以進一步控制誰能解密。

bucket versioning 是這段設定裡沒有出現、但在建立 bucket 時就該開的屬性。apply 寫壞或誤刪 state 時,versioning 是把記憶回捲到上一個正確版本的唯一退路。沒開的話一次壞寫就讓工具失去對現實的記憶,而回復的唯一方式是從雲端逐個資源重新 import。建立 state bucket 的 HCL 應該同時開 versioning 與刪除保護:

 1resource "aws_s3_bucket_versioning" "state" {
 2  bucket = aws_s3_bucket.tf_state.id
 3  versioning_configuration {
 4    status = "Enabled"
 5  }
 6}
 7
 8resource "aws_s3_bucket_lifecycle_configuration" "state" {
 9  bucket = aws_s3_bucket.tf_state.id
10
11  rule {
12    id     = "retain-old-versions"
13    status = "Enabled"
14
15    noncurrent_version_expiration {
16      noncurrent_days = 90
17    }
18  }
19}

舊版本的保留天數是成本與安全的取捨。90 天足以涵蓋大多數「發現 state 壞了再回去找正確版本」的時間差 — 超過 90 天才發現的 state 問題通常已經被後續 apply 覆蓋,回捲到更早的版本反而引入更大的落差。

dynamodb_table 指向一張鎖表。apply 開始時寫入一筆鎖、結束才釋放,第二個人同時跑就會被擋下並提示鎖被誰持有。這正是本地 state 無法提供、卻是多人協作底線的並行保護。鎖表本身的建立只需要幾行 HCL:

 1resource "aws_dynamodb_table" "tf_lock" {
 2  name         = "acme-tf-lock"
 3  billing_mode = "PAY_PER_REQUEST"
 4  hash_key     = "LockID"
 5
 6  attribute {
 7    name = "LockID"
 8    type = "S"
 9  }
10}

鎖表用 PAY_PER_REQUEST 模式足夠,因為它的讀寫頻率很低(只在 apply 開始和結束時各一次)。鎖卡住時(apply 中途失敗、沒有正常釋放鎖),用 terraform force-unlock <lock-id> 手動釋放,但前提是確認沒有其他 apply 正在執行。

key 是 state 在 bucket 內的路徑,這裡先用 prod/network 之類的分層命名。實際怎麼依環境切分 state 留待模組四:環境分離與模組化展開。

自管 backend 的雞生蛋問題

自管 backend 有一個啟動悖論:state bucket 和 lock table 本身也是雲端資源,它們該由誰來管理?用 Terraform 管理 Terraform 的 backend?

務實的做法是接受這個循環:用一份獨立的、最小化的 Terraform code 來建立 state bucket 和 lock table,這份 code 用 local state(因為它只在啟動那一次跑)。建立完成後,所有後續的 Terraform code 都指向這個 remote backend。這份啟動 code 的 local state 可以 commit 進 repo(它不含敏感值,只有 bucket 和 DynamoDB table 的 ID),或直接在跑完後丟棄 — 因為這些資源如果需要重建,幾行 CLI 就能做到。

 1# bootstrap/main.tf — 只用一次,建立 state 基礎設施
 2terraform {
 3  # 刻意用 local state,因為 remote backend 還不存在
 4}
 5
 6resource "aws_s3_bucket" "tf_state" {
 7  bucket = "acme-tf-state"
 8}
 9
10# ... versioning, encryption, lock table

託管 backend

託管路線把上述維運細節包起來,由 Terraform Cloud、Spacelift、env0 這類平台代管 state、鎖與加密,附帶 web UI 與 audit log。

判讀訊號是團隊規模與維運餘裕。自管 backend 的成本是要自己把 bucket versioning、加密、鎖表、IAM 權限配對,配錯任何一項都可能讓 state 失去保護 — 例如忘了開 versioning,一次壞寫就回不去。託管服務用月費換掉這份配置與維運負擔,代價是 state 託付給第三方、且進階治理功能常綁在付費級距。

小團隊起步、不想第一週就花在配 backend 上,託管較划算。對 state 存放位置有合規或主權要求、或希望基礎設施盡量自持的團隊,自管較划算。託管服務(Terraform Cloud / Spacelift)的免費方案涵蓋基本功能,付費級距約 $20-70/user/月;自管 backend 的成本是初次配置半天到一天的工程師時間,加上持續的 IAM 權限與 versioning 維護。

導入時程參考:最小可行 IaC(state backend + 第一批地基資源)的導入約需 2-3 天工程師時間。第一個可見里程碑是「一條指令能在新帳號重建整個地基環境」。之後每批服務的納管約 1-2 天/批,依資源複雜度而定。

State 地基設好後,下一步是立 Console 唯讀鐵律、並用最小可行資源集合驗證整條鏈路,見Console 唯讀鐵律與最小可行資源集合

跨分類引用