Terraform 是一款 IaC 工具,所謂 IaC 就是「基礎設施即代碼」,將我們的基礎設施以宣告式的代碼寫出來,隨後使用terraform apply即可完成基礎設施的部署,同一份配置,部署出來的一定是一模一樣的基礎設施(Nix OS 用戶狂喜)。
傳統的基礎設施管理絕大部分基於人力和各種雲服務商的 Dashboard,這就帶來了下面這些痛點:
| 痛點 | 説明 |
|---|
| 難以重現 | 透過 Dashboard 點點點進行配置,容易改錯或者改漏,而且難以復現 |
| 環境漂移 | 手動操作導致生產和測試環境配置逐漸偏離,導致極端情況下測試沒問題生產直接崩 |
| 難以擴充套件 | 新增一套環境需要重複大量手工操作,耗時且容易出錯 |
| 難以審計 | 沒有變更記錄,出了事不好甩鍋 |
| 難以協作 | 基礎設施由少數「懂的人」掌控,誰想要改都得去找這些人,效率低 |
為了解決這些問題,「基礎設施即代碼」的概念被提了出來,Terraform 就是其中一個著名的解決方案。
假設一個現實的使用場景,例如你每年都會購買新的 GCP 的 $300 試用金賬號,雖然便宜,但是每年都要重新跑到 GCP 的面板裏開機器。而有了 Terraform,想要部署同樣配置的機器,只要在每次更換賬號之後,將 API Token 替換成新的,隨後terraform apply,只需要幾分鐘就能開出和你上個賬號一模一樣的機器、VPC、S3、防火牆配置等等。
再比如 Cloudflare DNS 和 Tunnel 的管理和遷移,也只需要將原 Tunnel 的 Ingress Rule 複製到新 Tunnel 的 Ingress Rule。即使將來要遷移到 AliDNS、Route 53 等其他服務商,我們也可以原封不動的將資料複製過去。
尤其是到了多人協作的時候,配合 Git,每次更改都有跡可循,更不需要擔心合併衝突,PR 可以自動生成更改預覽,出現問題可以立刻回滾到上一個版本,這些都是傳統依賴人力去直接操作服務提供商 Dashboard 的做法完全無法做到的。
Terraform 是用 Go 寫的,編譯出來的產物自然也是單個可執行檔案,所以安裝起來也非常容易,Windows 下可以直接使用 Winget 安裝:
1
| winget install Hashicorp.Terraform
|
如果不想使用 Winget,也可以使用 Scoop 等其他包管理器,或者將預編譯的二進位制檔案放入$PATH,即可完成安裝。
如果你在 Linux 環境下,則使用對應的包管理器安裝即可,如果使用將二進位制檔案放入$PATH的安裝方法,則要記得sudo chmod +x terraform賦予檔案的執行許可權。
Provider(s)
我們前面已經提到,Terraform 是一個款礎設施即代碼工具,作為一個工具本身,他並不繫結某個平台,而是透過 Provider(s) 與各個平台對接,要檢視有哪些 Provider(s),可以瀏覽 Terraform 的 Registry。

狀態管理
Terraform 將每次執行基礎設施變更操作時的狀態資訊儲存在一個狀態檔案中,預設情況下會儲存在當前工作目錄下的terraform.tfstate檔案裏,我們也可以修改配置檔案自行指定其他的儲存後端,例如 S3、Postgres。每次我們執行terraform apply時,Terraform 都會將目前配置檔案宣告的狀態同現有的狀態檔案進行比對,從而計算變動的部分,自動確定調整順序之後操作 Provider 落實狀態變動。
現有資源的匯入一直是 Terraform 為人詬病的難題,Hashi Corp. 似乎一直在堅持着一個固執且愚蠢的觀點:你所有的基礎設施從一開始就應當是用我們的 Terraform 建立的,所以也不存在什麼資源匯入的問題。
| 時間 | 版本 | 進展 | 問題 |
|---|
| 2014–2022 | v0.x–v1.4 | 只有 terraform import,一次一條,而且完全不生成配置 | 匯入完還得自己手寫 HCL |
| 2023.06 | v1.5 | 引入 import 塊和 -generate-config-out引數,可以生成配置了 | 然而還是得一條一條寫,而且還得自己提供現有資源的 ID |
| 2024.01 | v1.7 | import 塊支援 for_each | 批次了,但 ID 還得自己搞 |
| 2024 下半年 | v1.12 | 引入了 terraform query 和 list 塊,終於實現了自動發現資源的功能 | 然而這個功能要 Provider 要自己實現,大量 Provider 根本沒跟上 |
這麼多年的時間,社羣一直在罵,然而,「我有一堆現有資源,怎麼匯入 Terraform」這個問題一直沒有被認真對待。Terraform 作為「基礎設施即代碼」工具,設計哲學就是宣告式和冪等性,Hashi Corp. 他們篤信「一切以代碼宣告的狀態為準,就應該用 Terraform 從零開始建立資源」。但現實是絕大多數公司都有大量歷史存量資源,先有資源、後有代碼才是常態。這個矛盾 Hashi Corp. 承認得很晚,v1.12 的 terraform query 才算是官方第一次認真面對這個問題——但 Provider 生態的跟進嘛……真的是一言難盡。
Terraform 的檔案結構很簡單,其主程式執行時會無腦讀取工作目錄下所有的.tf檔案,只要資訊齊全,想給檔案取什麼名字都可以,比如我的檔案結構就長這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ❯ tree -a -I .git
.
├── .editorconfig
├── .github
│ ├── dependabot.yml
│ └── workflows
│ ├── terraform-apply.yml
│ ├── terraform-plan.yml
│ └── your-fork.yml
├── .gitignore
├── .terraform.lock.hcl
├── cf_dns_zones.tf
├── cf_tunnel.tf
├── dns_example_com.tf
├── dns_example_net.tf
├── dns_example_top.tf
├── dns_example_cn.tf
├── main.tf # 基礎配置(terraform 塊)
├── moved.tf # 移動過的資源
├── provider.tf # 每個 Provider 的配置
├── README.md
├── rename_resources.ps1
└── variables.tf # 自定義的變數
|
main.tf用於存放基礎配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
| terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
tencentcloud = {
source = "tencentcloudstack/tencentcloud"
version = ">= 1.81.43"
}
}
}
|
provider.tf用於存放每個 Provider 的配置:
1
2
| provider "cloudflare" {}
provider "tencentcloud" {}
|
這裏留空是因為我們可以透過環境變數來傳遞 Credentials,而不是直接將 Credentials 寫在配置檔案中,例如 Cloudflare 的 Provider 就接受CLOUDFLARE_API_TOKEN作為api_token這個變數的替代。
variables.tf用於宣告自定義的變數:
1
2
3
4
5
6
7
8
9
| variable "cloudflare_zone_example_com" {
description = "Cloudflare zone ID for example.com"
type = string
}
variable "cloudflare_zone_example_top" {
description = "Cloudflare zone ID for example.top"
type = string
}
|
這些變數可以透過字首為TF_VAR_的環境變數傳入,Terraform 也會自動讀取terraform.tfvars檔案中的變數,變數的主要用途是在別的配置檔案中呼叫,例如:
1
2
3
4
5
6
7
8
9
10
11
12
| resource "cloudflare_dns_record" "example_cname" {
content = "${cloudflare_zero_trust_tunnel_cloudflared.Production_Tunnel.id}.cfargotunnel.com"
name = "example.example.com"
proxied = true
tags = []
ttl = 1
type = "CNAME"
zone_id = var.cloudflare_zone_id_example_com #這裏呼叫了cloudflare_zone_example_com這個變數
settings = {
flatten_cname = false
}
}
|
有了這些基礎配置,我們就可以執行terraform init初始化 Terraform 環境並鎖定依賴版本,剩下的就都是我們資源的宣告瞭。
前文提到過 Terraform 的狀態管理這個概念,狀態管理雖好,但在我們初始化時也帶來了一個問題——在預設狀態下,Terraform 的狀態檔案顯然是空的。
此時我們需要做的是將雲服務提供商上現有資源的狀態匯入 Terraform,這樣 Terraform 才能無縫接手並管理我們的基礎設施。相信大家也都看到了前文對 Terraform 資源匯入問題的吐槽。以匯入 Cloudflare 上託管的 DNS 記錄為例,前文提到過,如果 Provider 支援,可以使用 Terraform 在 1.12 版本之後引入的terraform query,然而大多數情況下,Providers 都是沒有跟進這個新功能的,Cloudflare 就屬於不支援的那一類。
好在雖然 Hashi Corp. 不認真解決問題,各路高強度使用 Terraform 管理基礎設施的公司就各顯神通。Cloudflare 就維護了名為 cf-terraforming 的匯入工具,免去了很多手動匯入的麻煩。首先安裝cf-terraforming,這個工具是用 Go 語言編寫的,需要在有 Go 語言環境的情況下安裝,或者將官方編譯好的二進位制檔案其放入$PATH並賦予其執行許可權。
1
| go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest
|
這個工具的用法還是比較簡單的,首先你需要以下環境變數:
1
2
3
4
5
6
7
8
9
| # 如果你使用 API Token
export CLOUDFLARE_API_TOKEN='Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j'
# 如果你使用 API Key
export CLOUDFLARE_EMAIL='user@example.com'
export CLOUDFLARE_API_KEY='1150bed3f45247b99f7db9696fffa17cbx9'
# 指定需要匯入的域名的區域 ID,如果匯入的是賬户資源(例如 Cloudflare Tunnel)則不需要
export CLOUDFLARE_ZONE_ID='81b06ss3228f488fh84e5e993c2dc17'
|
此處的命令假設你正在使用 Bash,如果使用的是與 Bash 語法不相容的 Shell,則需要做出調整,例如對於 Windows 上的 PowerShell,匯入環境變數的語法如下:
1
| $env:CLOUDFLARE_API_TOKEN='Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j'
|
通常我們只需要設定CLOUDFLARE_API_TOKEN和CLOUDFLARE_ZONE_ID就可以了,在控制枱建立 API Token 時,記得賦予這個 Token 必要的許可權,本次我們只是匯入 DNS 記錄,所以只賦予編輯區域 DNS 的許可權即可。

好了,準備工作全部完成,現在可以開始生成配置檔案:
首先匯入賬户中域名的配置,也就是cloudflare_zone:
1
2
3
| cf-terraforming generate \
--key $CLOUDFLARE_API_KEY \
--resource-type "cloudflare_zone" > zone.tf
|
這一步會在當前目錄下生成一個名為zone.tf的檔案,裏面會有如下格式的內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| resource "cloudflare_zone" "REDACTED" {
name = "REDACTED"
paused = false
type = "full"
vanity_name_servers = []
account = {
id = "REDACTED"
name = "REDACTED"
}
}
resource "cloudflare_zone" "REDACTED" {
name = "REDACTED"
paused = false
type = "full"
vanity_name_servers = []
account = {
id = "REDACTED"
name = "REDACTED"
}
}
|
現在域名資源已經匯入了,但是裏面的配置並沒有。接下來,匯入域名下的 DNS 記錄:
1
2
3
4
| cf-terraforming generate \
--zone $CLOUDFLARE_ZONE_ID \
--key $CLOUDFLARE_API_KEY \
--resource-type "cloudflare_dns_record" >> dns.tf
|
這一步會在當前目錄下生成一個名為dns.tf的配置檔案,裏面會有如下格式的內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| resource "cloudflare_dns_record" "terraform_managed_resource_5deb14xxxxxb629bf123xxxxxxxc8f_0" {
content = "67.24.33.108"
name = "example.example.com"
proxied = true
tags = []
ttl = 1
type = "A"
zone_id = "81c7f2de8dfxxxxxx52629xxxxxxfc"
settings = {}
}
resource "cloudflare_dns_record" "terraform_managed_resource_89xxxxx0bf9cxxxxxx9a_1" {
content = "35.27.108.33"
name = "terraform.example.com"
proxied = true
tags = []
ttl = 1
type = "A"
zone_id = "8xxxxxx7644e428526xxxxxx"
settings = {}
}
|
如果有多個域名需要匯入,則多次分別設定環境變數CLOUDFLARE_ZONE_ID再重複執行命令即可。
這裏生成的配置檔案可以直接使用,也就是我們之後需要的 Terraform 配置檔案。然而,此時我們僅僅只是生成了配置檔案,但是目前 Terraform 的狀態依然是空的,這時要是直接terraform apply,Terraform 會不管三七二十一將我們剛剛匯入的宣告一律視為新增資源,然後甩出一大堆「Alredy Exists」報錯。所以接下來,我們需要將生成的配置檔案匯入 Terraform 的terraform.tfstate狀態。
Terraform 在 1.5 版本引入了import塊,相比以往一行一行輸入命令的方式更加現代。其匯入流程是先生成一個包含import塊的.tf檔案,下次進行terraform apply時,Terraform 就會自動為我們執行匯入操作。
生成cloudflare_zone的import塊:
1
2
3
4
5
| cf-terraforming import \
--resource-type "cloudflare_zone" \
--modern-import-block \
--key $CLOUDFLARE_API_KEY \
--zone $CLOUDFLARE_ZONE_ID >> import.tf
|
生成cloudflare_dns_record的import塊:
1
2
3
4
5
| cf-terraforming import \
--resource-type "cloudflare_dns_record" \
--modern-import-block \
--key $CLOUDFLARE_API_KEY \
--zone $CLOUDFLARE_ZONE_ID >> import.tf
|
這一步會在當前目錄下生成import.tf,它包含了所需要的匯入資訊,作用就是告訴 Terraform 上一步生成的每個resource塊到底對應的是哪一個雲服務提供商的資源 ID。這個 ID 是雲服務提供商內部標記資源的代碼,平常是不會在控制面板上顯示的,只有在用 API 特別請求時才會知道。Terraform 在匯入過程中需要用到這個 ID 以確認本地的定義對應的雲端資源,以實現嚴格的冪等性。
好了,現在我們執行terraform plan:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| $ terraform plan
cloudflare_dns_record.minio_a: Refreshing state... [id=xxxxxxxxxxx53]
cloudflare_zero_trust_tunnel_cloudflared_config.raspberrypi: Refreshing state...
......
Terraform will perform the following actions:
# cloudflare_dns_record.terraform_managed_resource_0 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_0" {
content = "67.24.33.108"
created_on = "2026-04-08T10:18:12Z"
id = "5deb14c21xxxxxxx20f1c8f"
meta = jsonencode({})
modified_on = "2026-04-08T10:18:12Z"
name = "example.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
# cloudflare_dns_record.terraform_managed_resource_1 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_89c149exxxxxxxxxxxba13xxxxxa_1" {
content = "35.27.108.33"
created_on = "2026-04-08T10:17:54Z"
id = "89cxxxxxxxxxxxxxxxxxx09a"
meta = jsonencode({})
modified_on = "2026-04-08T10:17:54Z"
name = "terraform.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "81xxxxxxxxxxxxxxxxxxxxxfc"
}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly
these actions if you run "terraform apply" now.
|
如果需要 Add、Change 和 Destroy 的資源數量都是 0,説明我們的匯入操作沒有問題,直接terraform apply --auto-approve,資源就匯入完成了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| $ terraform apply --auto-approve
cloudflare_dns_record.push_a: Refreshing state... [id=REDACTED]
Terraform will perform the following actions:
# cloudflare_dns_record.terraform_managed_resource_REDACTED_0 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_0" {
content = "67.24.33.108"
created_on = "2026-04-08T10:18:12Z"
id = "REDACTED"
meta = jsonencode({})
modified_on = "2026-04-08T10:18:12Z"
name = "example.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
# cloudflare_dns_record.terraform_managed_resource_REDACTED_1 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_1" {
content = "35.27.108.33"
created_on = "2026-04-08T10:17:54Z"
id = "REDACTED"
meta = jsonencode({})
modified_on = "2026-04-08T10:17:54Z"
name = "terraform.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Importing... [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Import complete [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Importing... [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Import complete [id=REDACTED/REDACTED]
Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.
|
狀態儲存和持續整合
IaC 的核心價值之一就在於可以輕易實現基於 Git 的多人協作,以及 CI 的持續整合,但是在此之前,又有一個新的問題需要解決——terraform.tfstate到底放哪裏:沒人想要換一次環境辛辛苦苦匯入的狀態就丟一次。
Terraform 目前支援以下幾種儲存狀態的後端:
- local
- remote
- azurerm
- consul
- cos
- gcs
- http
- Kubernetes
- oci
- oss
- pg
- s3
沒有特殊需求可以像我一樣選擇s3,畢竟 Cloudflare R2 有免費額度,不用白不用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| terraform {
backend "s3" {
bucket = "terraform"
key = "terraform.tfstate"
region = "auto"
endpoints = {
s3 = "https://REDACTED.r2.cloudflarestorage.com"
}
# R2 不需要這些 AWS 的驗證
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
use_path_style = true
}
}
|
對於 S3 的後端,建議用AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY兩個環境變數來儲存 Credentials:
1
2
| export AWS_ACCESS_KEY_ID='REDACTED'
export AWS_SECRET_ACCESS_KEY='REDACTED'
|
配置完成,執行terraform init -migrate-state,配置就成功儲存到雲上了,以後不論在何處修改配置、執行terraform apply都無須擔心 Terraform 的 State 不同步的問題。
接下來就是 Github CI 的配置,其實很簡單,無非就是每次git push時觸發一次terraform init、terraform fmt和terraform apply,以下是我的.github/workflows/apply.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| name: "Terraform Apply"
on:
push:
branches:
- main
env:
TF_IN_AUTOMATION: "true"
CLOUDFLARE_API_TOKEN: "${{ secrets.CLOUDFLARE_API_TOKEN }}"
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
TF_VAR_cloudflare_zone_id_example_com: "${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}"
TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}
jobs:
terraform:
name: "Terraform Apply"
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: terraform-apply
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Terraform
uses: hashicorp/setup-terraform@v4
- name: Terraform Init
run: terraform init -input=false
- name: Terraform Apply
run: terraform apply -input=false -auto-approve
|
對於 PR 則應當讓 CI 自動為每次 PR 附上terraform plan的輸出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
| name: Terraform Plan
on:
pull_request:
paths:
- "**/*.tf"
- ".github/workflows/terraform-plan.yml"
permissions:
contents: read
pull-requests: write
env:
TF_IN_AUTOMATION: "true"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_VAR_cloudflare_zone_id_example_com: "${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}"
TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
- name: Terraform fmt
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init -input=false
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -input=false -no-color
continue-on-error: true
- name: Post Plan to PR
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('<!-- terraform-plan -->')
);
const planOutput = `${{ steps.plan.outputs.stdout }}`.substring(0, 65000);
const body = `<!-- terraform-plan -->
#### Terraform Plan
| Step | Result |
| -------- | --------------------------------- |
| fmt | \`${{ steps.fmt.outcome }}\` |
| init | \`${{ steps.init.outcome }}\` |
| validate | \`${{ steps.validate.outcome }}\` |
| plan | \`${{ steps.plan.outcome }}\` |
<details><summary>展開 Plan 詳情</summary>
\`\`\`terraform
${planOutput}
\`\`\`
</details>`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
}
- name: Fail if plan failed
if: steps.plan.outcome == 'failure'
run: exit 1
|

後續的工作流程
到這一步,Terraform 的「接管初始化」已經結束了,後面就進入了日常維護階段。這個階段其實就三件事:
- 建立新資源
- 修改現有的資源
- 刪除不再需要的資源
常用的操作就兩個:terraform plan和terraform apply,如果是個人使用,小的改動直接提交就算了;如果是團隊協作,每次修改則應當遵循能 PR 就不直接 Commit 的原則。
建立基礎設施
假設你現在要新增一條 DNS 記錄,或者新建一個 Tunnel、一個物件儲存桶,流程如下:
flowchart TD
C1[新建分支
如 feat/add-minio-record] --> C2[新增 resource 塊]
C2 --> C3[terraform fmt + validate]
C3 --> C4[terraform plan]
C4 --> C5{僅新增預期資源?}
C5 -- 否 --> C6[修正配置後重跑 plan]
C6 --> C4
C5 -- 是 --> C7[提交 PR]
C7 --> C8[合併後 CI apply]最理想的plan輸出是:
X to add0 to change0 to destroy
如果你只是想加東西,結果出現了to destroy,那就先別衝動,通常是引用寫錯、變數搞錯,或者不小心改了資源地址,仔細檢查是什麼地方出了問題。
修改與刪除基礎設施
修改流程和建立類似,但要多一步——評估變更是否會觸發重建。
因為很多 Provider 欄位是ForceNew,你以為只是改個欄位,Terraform 看完説:「好的,刪了重建。」這在我們修改 DNS 的這個場景並不是什麼很大的問題,但是到了雲例項這種資源,如果刪除重建勢必會造成損失。
建議按下面這個順序來:
flowchart TD
M1[修改 .tf] --> M2[terraform plan]
M2 --> M3{出現 replace/destroy?}
M3 -- 否 --> M7[確認影響範圍]
M7 --> M8[terraform apply]
M3 -- 是 --> M4[暫停並複核變更]
M4 --> M5[必要時加 lifecycle 保護]
M5 --> M6[安排變更視窗]
M6 --> M8對生產環境來説,建立、修改得慢一點並不是什麼很大的問題,最應當看重的是操作的正確性,慢一點,不要出錯。IaC 不是比手速,IaC 比的是可預期性。
如果是刪除資源(比如下線某個 DNS 記錄、清理廢棄 Tunnel),走下面這個流程:
flowchart TD
D1[確認資源已廢棄檢查業務/監控/指令碼依賴] --> D2[刪除 resource 塊或調整 count/for_each]
D2 --> D3[terraform plan]
D3 --> D4{to destroy 是否符合預期?}
D4 -- 否 --> D5[回滾修改並繼續排查依賴]
D5 --> D1
D4 -- 是 --> D6[準備回滾方案並選低峯視窗]
D6 --> D7[PR 稽核透過]
D7 --> D8[terraform apply]
D8 --> D9[刪除資源後的可用性檢查]日常協作建議
- 把 Credentials 放環境變數或 CI Secret,別寫進
.tf和倉庫 - 對關鍵資源開啓保護策略,防止誤刪
- 將目錄按資源型別拆分
- 定期執行
terraform plan做基礎設施漂移檢查和校正,避免在面板誤操作手動修改
雖然這套流程看起來很麻煩,但每一次變更都有記錄、可審計、可回滾,最重要的是可復現,這才是 IaC 最有價值的地方。
蔘考
評論已停用,直到您接受功能性 Cookie。