這篇涵蓋兩個主題: Terraform 的 module 化設計與 GCS remote state, 以及 Ansible 的核心結構與冪等性驗證。兩者在角色上互補 — Terraform 負責建立基礎設施, Ansible 負責設定伺服器內部。
為什麼要 Module
Terraform 沒有 module 時, 同樣的 VPC 邏輯在 dev 和 prod 各寫一份。改一個地方, 另一個忘了改, 就出問題。Module 的作用和函式一樣: 定義一次, 傳入不同參數使用。
目標結構:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
terraform/
├── modules/
│ └── vpc/
│ ├── variables.tf # inputs
│ ├── main.tf # resource definitions
│ └── outputs.tf # outputs
└── environments/
├── dev/
│ ├── backend.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── main.tf
└── prod/
└── ...
modules/ 定義可重用的 infra 單元, environments/ 負責真正的落地, 傳入不同的變數值。
variables.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
variable "project_id" {
type = string
}
variable "region" {
type = string
default = "us-east1"
}
variable "vpc_name" {
type = string
}
variable "subnet_name" {
type = string
}
variable "subnet_cidr" {
type = string
}
main.tf
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
resource "google_compute_network" "vpc" {
name = var . vpc_name
auto_create_subnetworks = false
project = var . project_id
}
resource "google_compute_subnetwork" "subnet" {
name = var . subnet_name
network = google_compute_network . vpc . id
region = var . region
ip_cidr_range = var . subnet_cidr
project = var . project_id
}
resource "google_compute_firewall" "allow_internal" {
name = "${var.vpc_name}-allow-internal"
network = google_compute_network . vpc . name
project = var . project_id
allow {
protocol = "tcp"
}
allow {
protocol = "udp"
}
allow {
protocol = "icmp"
}
source_ranges = [ var . subnet_cidr ]
}
resource "google_compute_firewall" "allow_ssh" {
name = "${var.vpc_name}-allow-ssh"
network = google_compute_network . vpc . name
project = var . project_id
allow {
protocol = "tcp"
ports = [ "22" ]
}
source_ranges = [ "0.0.0.0/0" ]
}
outputs.tf
1
2
3
4
5
6
7
8
9
10
11
output "vpc_id" {
value = google_compute_network . vpc . id
}
output "vpc_name" {
value = google_compute_network . vpc . name
}
output "subnet_id" {
value = google_compute_subnetwork . subnet . id
}
environments/dev/main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
provider "google" {
project = var . project_id
region = var . region
}
module "vpc" {
source = "../../modules/vpc"
project_id = var . project_id
region = var . region
vpc_name = "devops-vpc"
subnet_name = "devops-subnet"
subnet_cidr = "10.0.1.0/24"
}
1
2
project_id = "devops-lab-lou-2026"
region = "us-east1"
prod 環境呼叫同一個 module, 只是傳入不同的 vpc_name 和 subnet_cidr。module 本身不改動。
Remote State 與 State Locking
為什麼需要 Remote State
Terraform 的 state file 記錄「現在 GCP 上長什麼樣子」。沒有 remote state, state 只存在本機:
本機不見 = infra 資訊全部丟失
多人協作 = 每個人各自有一份 state, 互相衝突
GCS backend 把 state 集中存在 GCS bucket, 所有人共用同一份。
為什麼需要 State Locking
如果兩個人同時跑 terraform apply, 兩個人都讀到相同的 state, 各自計算 diff, 各自套用變更 — 結果不可預期。
State locking 讓第一個 apply 開始後就鎖住, 第二個人必須等待鎖釋放才能繼續。這對應的是 CAP Theorem 的 Consistency: 分散式系統裡, 不能讓兩個操作同時修改同一個東西。
1
2
3
4
5
6
7
# environments/dev/backend.tf
terraform {
backend "gcs" {
bucket = "devops-lab-lou-2026-tfstate"
prefix = "environments/dev"
}
}
GCS backend 原生支援 locking, 用 GCS object 的 generation 機制實作。不需要額外設定。
初始化:
1
2
3
terraform init
terraform plan
terraform apply
plan 和 apply 要分開的原因: plan 讓你看到「即將發生什麼」, apply 才真正執行。Production 環境裡, plan 的結果通常要先審核才能 apply。
State 操作指令
1
2
3
4
5
6
7
8
9
10
11
# list all resources tracked in state
terraform state list
# show detail of a specific resource
terraform state show module.vpc.google_compute_network.vpc
# move resource to a different address (e.g. refactor module path)
terraform state mv old_address new_address
# remove resource from state without destroying it
terraform state rm module.vpc.google_compute_network.vpc
state rm 不會刪除 GCP 上的資源, 只是讓 Terraform 不再追蹤它。下次 apply 時, Terraform 以為那個資源不存在, 可能嘗試重建。所以 state rm 通常搭配 terraform import 使用, 先移除再重新 import 到新的 address。
Ansible
Terraform
Ansible
用途
建基礎設施
設定伺服器內部
語言
HCL (declarative)
YAML playbook
狀態
state file
無 state, 每次重新檢查
連線方式
API call
SSH (agentless)
冪等性
天生冪等
需要正確寫 playbook
Ansible agentless 的意思是: 目標機器不需要安裝任何 Ansible agent, 只需要有 SSH 和 Python。Ansible 在控制機 (你的 laptop 或 CI runner) 上執行, 透過 SSH 連到目標機器執行操作。
現代 K8s 環境裡 Ansible 的角色變小了, 因為 K8s YAML 取代了很多設定工作。但 VM 環境和 on-premise 環境仍然很常見。
核心結構
Inventory : 你要管理哪些機器, 分組管理。
1
2
3
4
5
6
7
8
9
[local]
localhost ansible_connection = local
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com ansible_user = ubuntu
Module : Ansible 內建的操作單元, 例如 file, copy, apt, service。Module 是冪等的基礎 — 每個 module 在執行前先檢查目標狀態, 狀態已符合就不做任何事。
Task : 一個步驟, 呼叫一個 module。
Playbook : 一個或多個 task 的組合, 定義在哪些 host 上執行什麼。
Role : 多個 task 的集合, 可重用, 類似 Terraform 的 module。適合封裝 nginx 安裝、prometheus 設定這類可重複使用的邏輯。
基本 Playbook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- name : localhost practice
hosts : local
tasks :
- name : create practice directory
file :
path : /tmp/ansible-practice
state : directory
mode : "0755"
- name : write config file
copy :
dest : /tmp/ansible-practice/config.txt
content : |
env=dev
version=1.0
- name : show file content
command : cat /tmp/ansible-practice/config.txt
register : file_content
- name : print result
debug :
msg : "{{ file_content.stdout }}"
1
2
3
4
5
# check connectivity
ansible local -i inventory.ini -m ping
# run playbook
ansible-playbook -i inventory.ini playbook.yml
冪等性驗證
第一次執行:
1
2
3
4
TASK [create practice directory] ... changed
TASK [write config file] ... changed
TASK [show file content] ... changed
TASK [print result] ... ok
第二次執行 (不改任何東西):
1
2
3
4
TASK [create practice directory] ... ok # already exists, no action
TASK [write config file] ... ok # content unchanged, no action
TASK [show file content] ... changed # command module always runs
TASK [print result] ... ok
file 和 copy module 是冪等的, 第二次執行顯示 ok 代表沒有做任何變更。command module 不是冪等的, 每次都會執行 — 這是設計上需要注意的地方。需要冪等的指令操作要用 creates 或 when 條件控制。
References