
這篇涵蓋兩個主題: Terraform 的 module 化設計與 GCS remote state, 以及 Ansible 的核心結構與冪等性驗證。兩者在角色上互補 — Terraform 負責建立基礎設施, Ansible 負責設定伺服器內部。

## Terraform Module 化

### 為什麼要 Module

Terraform 沒有 module 時, 同樣的 VPC 邏輯在 dev 和 prod 各寫一份。改一個地方, 另一個忘了改, 就出問題。Module 的作用和函式一樣: 定義一次, 傳入不同參數使用。

目標結構:

```text
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

```hcl
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

```hcl
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

```hcl
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

```hcl
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"
}
```

### terraform.tfvars

```hcl
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: 分散式系統裡, 不能讓兩個操作同時修改同一個東西。

```hcl
# environments/dev/backend.tf
terraform {
  backend "gcs" {
    bucket = "devops-lab-lou-2026-tfstate"
    prefix = "environments/dev"
  }
}
```

GCS backend 原生支援 locking, 用 GCS object 的 `generation` 機制實作。不需要額外設定。

初始化:

```sh
terraform init
terraform plan
terraform apply
```

`plan` 和 `apply` 要分開的原因: `plan` 讓你看到「即將發生什麼」, `apply` 才真正執行。Production 環境裡, plan 的結果通常要先審核才能 apply。

### State 操作指令

```sh
# 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 vs 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**: 你要管理哪些機器, 分組管理。

```ini
[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

```yaml
---
- 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 }}"
```

```sh
# check connectivity
ansible local -i inventory.ini -m ping

# run playbook
ansible-playbook -i inventory.ini playbook.yml
```

### 冪等性驗證

第一次執行:

```text
TASK [create practice directory] ... changed
TASK [write config file]         ... changed
TASK [show file content]         ... changed
TASK [print result]              ... ok
```

第二次執行 (不改任何東西):

```text
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

- [Terraform Modules documentation](https://developer.hashicorp.com/terraform/language/modules)
- [Terraform GCS Backend](https://developer.hashicorp.com/terraform/language/backend/gcs)
- [Terraform State Commands](https://developer.hashicorp.com/terraform/cli/commands/state)
- [CAP Theorem - Wikipedia](https://en.wikipedia.org/wiki/CAP_theorem)
- [Ansible Getting Started](https://docs.ansible.com/ansible/latest/getting_started/index.html)
- [Ansible Module Index](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html)
- [Ansible Idempotency](https://docs.ansible.com/ansible/latest/reference_appendices/glossary.html#term-Idempotency)
