
Infrastructure as Code:停止在 Console 上點點點
2017 年某家新創公司的 SRE 在凌晨三點被叫起來處理 Production 事故。他登入 AWS Console,在手忙腳亂中改了 Security Group 規則想開放某個 port 來除錯。問題解決後他忘記把規則改回去,那個對外開放的 port 就這樣暴露了三個月,直到被外部安全掃描發現。沒有人知道那條規則是誰加的、為什麼加的、什麼時候該移除。因為它從來沒有被記錄在任何地方——它只存在於 AWS Console 的某個頁面裡。
這是手動管理基礎設施最根本的問題:不可追蹤、不可重現、不可審計。當你的基礎設施是透過在 Console 上點點點建立的,你面對的是這些困境:(1)沒有人記得三個月前為什麼改了那個設定。(2)建立一個和 Production 一模一樣的 Staging 環境需要花三天手動操作,還不保證一致。(3)新同事加入團隊,唯一的知識移轉方式是「我操作一次你在旁邊看」。(4)出了事故想 rollback,沒有人確定「上一個正常的狀態」長什麼樣子。
Infrastructure as Code(IaC)的核心理念就是:用程式碼定義基礎設施,像管理應用程式碼一樣管理基礎設施。所有的基礎設施變更都寫成程式碼、提交到版本控制系統、經過 Code Review、透過自動化流程部署。這篇文章從 IaC 的基礎概念出發,深入 Terraform 的實務操作,並結合 GitOps 工作流程,建立一套可追蹤、可重現、可審計的基礎設施管理體系。
架構概覽
flowchart LR Code["編寫 HCL\nTerraform Code"] --> Plan["terraform plan\n預覽變更"] Plan --> Review["Code Review\nMR / PR 審查"] Review --> Apply["terraform apply\n套用變更"] Apply --> State["State Management\nRemote Backend"] State -->|狀態同步| Cloud["雲端資源\nAWS / GCP / Azure"] Cloud -->|drift detection| Plan
架構概覽
flowchart LR Dev[工程師] -->|編寫 HCL\n定義基礎設施| Code[Terraform Code\nmain.tf / variables.tf] Code -->|git push| Repo[Git Repository\n版本控管] Repo -->|Merge Request\nCode Review| Review[團隊審查\nPlan 輸出檢視] Review -->|terraform plan| Plan[Plan\n預覽變更差異] Plan -->|approve & merge| Apply[terraform apply\n執行變更] Apply -->|記錄資源狀態| State[State File\nterraform.tfstate] State -->|遠端儲存 + 鎖定| Backend[Remote Backend\nS3 + DynamoDB Lock] subgraph GitOps 流程 Repo Review Plan end subgraph 基礎設施變更 Apply State Backend end
工程師用 HCL(HashiCorp Configuration Language)撰寫基礎設施定義,push 到 Git Repository。團隊成員透過 Merge Request 審查變更,CI pipeline 自動執行 terraform plan 產生變更預覽。審查通過後 merge 觸發 terraform apply,實際建立或修改雲端資源。所有資源的當前狀態記錄在 State File 中,存放在遠端 Backend(如 S3)並透過 DynamoDB 實作鎖定機制防止同時操作。
核心概念
-
IaC 三大原則:Infrastructure as Code 不只是「把指令寫成腳本」,它有三個核心原則:(1)宣告式(Declarative)而非命令式(Imperative)——你描述「最終想要的狀態」,而不是「一步步怎麼做到」。宣告式說「我要一台 t3.medium 的 EC2,在 us-east-1」;命令式說「先呼叫 create-instance API,傳入 instance type 參數,然後呼叫 assign-security-group API…」。宣告式的好處是幂等性——跑第二次不會重複建立資源。(2)冪等性(Idempotent)——同一份程式碼執行一次和執行十次的結果一樣。如果資源已經存在且符合定義,Terraform 不會做任何事。這讓你可以安心地重複執行 apply 而不怕弄壞東西。(3)版本控管(Version Controlled)——基礎設施的每一次變更都是一個 commit。誰改了什麼、為什麼改、什麼時候改,全部有紀錄。需要 rollback 就 revert commit,重新 apply。
-
Terraform 基礎元素:Terraform 使用 HCL 語言定義基礎設施,核心概念包括:(1)Provider——告訴 Terraform 要跟哪個雲端平台互動。AWS、GCP、Azure、Cloudflare、GitHub 都有對應的 Provider。每個 Provider 提供一組可管理的資源類型。(2)Resource——你要建立的基礎設施元件。一台 EC2 instance、一個 S3 bucket、一條 DNS record 都是 resource。Resource 之間可以互相引用,Terraform 會自動計算依賴關係和建立順序。(3)Data Source——查詢已經存在的資源。例如你想引用一個手動建立的 VPC ID,可以用 data source 去查詢而不是硬寫 ID。(4)Variable——參數化你的設定。把 instance type、region、environment name 等可變的值抽成變數,不同環境傳入不同值。(5)Output——執行 apply 後輸出的值。例如建立完 EC2 後輸出它的 public IP,供其他模組或人員使用。
-
State 管理:Terraform State 是 Terraform 最重要也最容易出問題的機制。State file(
terraform.tfstate)記錄了 Terraform 管理的所有資源的當前狀態——每個資源的 ID、屬性、元資料都在裡面。Terraform 比較 state file 和你的程式碼,計算出需要做什麼變更。Local State 是預設行為,state file 存在本地磁碟。這只適合個人實驗,因為:(1)多人協作時 state file 會衝突。(2)筆電壞了 state file 就不見了。(3)無法防止兩個人同時執行 apply。Remote State 把 state file 存在共享的遠端儲存(如 AWS S3),搭配鎖定機制(如 DynamoDB)確保同一時間只有一個人能修改。這是團隊協作的唯一正確選擇。 -
Modules:可複用的基礎設施元件:隨著專案成長,你會發現很多基礎設施模式是重複的——每個微服務都需要一組 Load Balancer + Target Group + Security Group + Auto Scaling Group。Module 讓你把這些重複的模式封裝成可複用的元件,就像程式設計中的 function。你定義一次 VPC module,在 dev、staging、prod 三個環境各呼叫一次,傳入不同的 CIDR 和環境名稱。Module 可以放在本地目錄,也可以發佈到 Terraform Registry 供其他團隊使用。
-
Workspaces:多環境管理:Terraform Workspaces 提供了一種管理多環境的方式——同一份程式碼,不同的 workspace 維護不同的 state file。
terraform workspace new dev建立 dev workspace,terraform workspace select prod切換到 prod workspace。每個 workspace 有自己的 state,修改 dev 的資源不會影響 prod。不過實務上很多團隊偏好用目錄結構而非 workspace 來分環境(例如environments/dev/、environments/staging/、environments/prod/),因為目錄結構更直觀、diff 更容易看懂。 -
GitOps:透過 Pull Request 管理基礎設施變更:GitOps 的核心原則是「Git repository 是基礎設施狀態的唯一真實來源(single source of truth)」。所有基礎設施變更必須透過以下流程:(1)工程師建立分支,修改 Terraform 程式碼。(2)提交 Merge Request / Pull Request。(3)CI pipeline 自動執行
terraform plan,把變更預覽貼到 MR 的留言中。(4)團隊成員 review plan 輸出,確認變更符合預期。(5)merge 到 main 分支後,CD pipeline 自動執行terraform apply。這個流程的好處:每次變更都有人 review、有完整的審計記錄、可以隨時透過 git revert 回滾、新成員透過看 MR 歷史就能理解基礎設施的演變過程。 -
Terraform vs 替代方案:(1)Pulumi——用真正的程式語言(Python, TypeScript, Go)寫基礎設施,而非 HCL。優點是可以用語言原生的迴圈、條件判斷、型別系統;缺點是學習曲線更陡,且社群資源比 Terraform 少。(2)AWS CloudFormation——AWS 原生的 IaC 工具,用 YAML/JSON 定義資源。優點是和 AWS 服務整合最緊密;缺點是只能管理 AWS 資源,語法冗長,除錯體驗差。(3)Ansible——偏向命令式(procedural),用 playbook 定義一系列操作步驟。適合配置管理(裝軟體、改設定檔),但不適合作為基礎設施的主要管理工具,因為缺乏 state 機制,冪等性需要自己處理。Terraform 的優勢在於:多雲支援、龐大的社群和 Provider 生態系、成熟的 state 管理機制、以及宣告式語法帶來的可預測性。
使用情境
-
全新專案的基礎設施建置:用 Terraform 從零建立 VPC、Subnet、Security Group、RDS、ElastiCache、ECS/EKS Cluster。所有資源定義提交到 Git,團隊成員 review 後 apply。三個月後新同事加入,看 Terraform 程式碼就能理解整個基礎設施架構,不需要登入 Console 到處翻。
-
多環境複製:用同一套 Terraform module,透過不同的
terraform.tfvars建立 dev / staging / prod 三套完全一致的基礎設施。Staging 和 Production 的差異只在資源規格和環境變數,架構完全相同,大幅降低「Staging 測過但 Production 壞掉」的風險。詳見 環境拆分。 -
災難復原與快速重建:當某個 region 發生故障需要在另一個 region 重建整套基礎設施時,Terraform 程式碼就是你的復原計畫。改幾個 region 變數,
terraform apply,整套基礎設施在另一個 region 建起來。比手動在 Console 操作快幾十倍,且不會遺漏任何資源。 -
合規審計:金融或醫療行業需要追蹤基礎設施的每一次變更。Git commit history + MR review record + Terraform plan output 提供完整的審計軌跡。稽核人員可以清楚地看到誰在什麼時候為什麼修改了哪些基礎設施資源。
實作範例 / 設定範例
Terraform 專案結構
一個典型的 Terraform 專案目錄結構如下,按照職責清楚拆分檔案:
infra/
├── modules/
│ ├── vpc/
│ │ ├── main.tf # VPC、Subnet、Route Table 定義
│ │ ├── variables.tf # 模組輸入變數
│ │ └── outputs.tf # 模組輸出值
│ ├── ec2/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── rds/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf # 呼叫 modules,傳入 dev 參數
│ │ ├── terraform.tfvars # dev 環境的變數值
│ │ └── backend.tf # dev 的 remote state 設定
│ ├── staging/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── .gitlab-ci.yml # CI/CD pipeline 定義
每個環境(dev / staging / prod)有自己的目錄和 state,彼此完全隔離。共用邏輯封裝在 modules/ 中,各環境只需傳入不同的參數即可。
AWS VPC + EC2 完整範例
以下是一個完整的 Terraform 範例,建立 VPC、Public Subnet、Internet Gateway、Security Group 和一台 EC2 Instance:
# === main.tf ===
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
# --- VPC ---
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# --- Public Subnet ---
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-${count.index + 1}"
}
}
# --- Internet Gateway ---
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# --- Route Table ---
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-${var.environment}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# --- Security Group ---
resource "aws_security_group" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from trusted IPs only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.ssh_allowed_cidrs
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-sg"
}
lifecycle {
create_before_destroy = true
}
}
# --- EC2 Instance ---
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_pair_name
root_block_device {
volume_size = var.root_volume_size
volume_type = "gp3"
encrypted = true
}
tags = {
Name = "${var.project_name}-${var.environment}-web-01"
}
}
# === variables.tf ===
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
}
variable "project_name" {
description = "Project name for resource naming"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "List of public subnet CIDR blocks"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
variable "ssh_allowed_cidrs" {
description = "CIDR blocks allowed to SSH"
type = list(string)
default = [] # 預設不開放 SSH,需要時才傳入
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_pair_name" {
description = "Name of the SSH key pair"
type = string
}
variable "root_volume_size" {
description = "Root EBS volume size in GB"
type = number
default = 20
}
# === outputs.tf ===
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "web_instance_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "web_security_group_id" {
description = "Security Group ID for web servers"
value = aws_security_group.web.id
}variables.tf 定義了所有可配置的參數及其預設值。在不同環境的 terraform.tfvars 中覆蓋這些值即可。outputs.tf 輸出建立完成後需要的資訊,可以供其他模組或人員使用。注意 data.aws_ami.ubuntu 是一個 Data Source,它查詢最新的 Ubuntu 22.04 AMI ID 而不是硬寫。
Remote State 設定(S3 Backend + DynamoDB Lock)
# === backend.tf ===
# 每個環境各自一份,指向不同的 state 路徑
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/infrastructure/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# === 初始化 S3 bucket 和 DynamoDB table(只需執行一次)===
# 這些資源本身建議用 AWS CLI 或另一個簡單的 Terraform 專案建立
# 不要用被管理的 state backend 來管理 backend 本身(雞生蛋問題)
# s3 bucket 設定
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
tags = {
Name = "Terraform State"
ManagedBy = "terraform-bootstrap"
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled" # 開啟版本控制,誤刪或覆蓋可以復原
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms" # State file 包含敏感資訊,必須加密
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true # 禁止公開存取
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# DynamoDB table 用於 state locking
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform State Lock"
ManagedBy = "terraform-bootstrap"
}
}Remote State 的重點:(1)S3 bucket 開啟 versioning,萬一 state file 被覆蓋或損毀可以從歷史版本復原。(2)強制加密,因為 state file 裡可能包含資料庫密碼等敏感資訊。(3)DynamoDB table 實作 state locking,當有人正在執行 terraform apply 時,其他人執行 apply 會被擋住,避免同時修改造成 state corruption。
可複用的 VPC Module
# === modules/vpc/main.tf ===
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.name_prefix}-vpc"
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.name_prefix}-public-${count.index + 1}"
Tier = "public"
}
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.name_prefix}-private-${count.index + 1}"
Tier = "private"
}
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${var.name_prefix}-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = {
Name = "${var.name_prefix}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# === modules/vpc/variables.tf ===
variable "name_prefix" {
description = "Prefix for resource names"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
}
variable "public_subnet_cidrs" {
description = "List of public subnet CIDRs"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "List of private subnet CIDRs"
type = list(string)
default = []
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
# === modules/vpc/outputs.tf ===
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# === 在環境中呼叫 module ===
# environments/dev/main.tf
module "vpc" {
source = "../../modules/vpc"
name_prefix = "myapp-dev"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"]
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
name_prefix = "myapp-prod"
vpc_cidr = "10.1.0.0/16"
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
private_subnet_cidrs = ["10.1.10.0/24", "10.1.11.0/24", "10.1.12.0/24"]
availability_zones = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}同一個 VPC module 被 dev 和 prod 環境呼叫,但傳入不同的 CIDR、子網數量和命名前綴。prod 環境多了一個 AZ 提高可用性。如果 VPC 的架構需要調整(例如加 NAT Gateway),只需要改 module 一個地方,所有環境同步更新。
CI/CD Pipeline for Terraform(GitLab CI)
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_DIR: "environments/${CI_ENVIRONMENT_NAME}"
.terraform_base:
image: hashicorp/terraform:1.6
before_script:
- cd ${TF_DIR}
- terraform init -input=false
# ============ Validate ============
validate:
extends: .terraform_base
stage: validate
variables:
CI_ENVIRONMENT_NAME: dev
script:
- terraform validate
- terraform fmt -check -recursive ../../
rules:
- if: $CI_MERGE_REQUEST_IID # 只在 MR 時執行
# ============ Plan (MR 時自動執行) ============
plan:dev:
extends: .terraform_base
stage: plan
variables:
CI_ENVIRONMENT_NAME: dev
script:
- terraform plan -out=plan.tfplan -input=false
- terraform show -no-color plan.tfplan > plan.txt
artifacts:
paths:
- ${TF_DIR}/plan.tfplan
- ${TF_DIR}/plan.txt
expire_in: 7 days
rules:
- if: $CI_MERGE_REQUEST_IID
changes:
- environments/dev/**/*
- modules/**/*
plan:prod:
extends: .terraform_base
stage: plan
variables:
CI_ENVIRONMENT_NAME: prod
script:
- terraform plan -out=plan.tfplan -input=false
- terraform show -no-color plan.tfplan > plan.txt
artifacts:
paths:
- ${TF_DIR}/plan.tfplan
- ${TF_DIR}/plan.txt
expire_in: 7 days
rules:
- if: $CI_MERGE_REQUEST_IID
changes:
- environments/prod/**/*
- modules/**/*
# ============ Apply (merge 到 main 後執行) ============
apply:dev:
extends: .terraform_base
stage: apply
variables:
CI_ENVIRONMENT_NAME: dev
script:
- terraform apply -auto-approve -input=false
dependencies:
- plan:dev
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- environments/dev/**/*
- modules/**/*
environment:
name: dev
apply:prod:
extends: .terraform_base
stage: apply
variables:
CI_ENVIRONMENT_NAME: prod
script:
- terraform apply -auto-approve -input=false
dependencies:
- plan:prod
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- environments/prod/**/*
- modules/**/*
when: manual # Prod 需要手動觸發,多一層保護
environment:
name: production這個 CI/CD pipeline 的設計邏輯:(1)MR 階段——自動執行 terraform validate 確認語法正確,執行 terraform plan 產生變更預覽。reviewer 可以在 MR 中看到 plan 輸出,確認變更符合預期。(2)Merge 到 main 後——dev 環境自動 apply,prod 環境需要手動觸發。(3)只有相關檔案有變更時才執行——改了 dev 環境不會觸發 prod 的 pipeline,改了 module 則會觸發所有環境的 plan。詳見 CD Templates。
常見問題與風險
-
State Corruption(State 檔案損毀):State file 是 JSON 格式,如果被手動編輯或在 apply 過程中中斷,可能導致 state 與實際資源不一致。預防措施:(1)永遠使用 remote state + locking。(2)S3 bucket 開啟 versioning,損毀時可以回復前一個版本。(3)避免手動執行
terraform state相關指令,除非你非常清楚自己在做什麼。(4)定期執行terraform plan比較 state 和實際資源的差異。 -
Drift(State 與實際資源的漂移):有人繞過 Terraform 直接在 Console 修改了資源(例如手動改了 Security Group 規則),導致 state file 記錄的狀態和實際狀態不同。下次執行
terraform apply時,Terraform 可能會把手動的改動覆蓋掉。預防措施:(1)嚴格禁止手動修改 Terraform 管理的資源——所有變更必須走 IaC 流程。(2)定期執行terraform plan偵測 drift。(3)使用 AWS Config Rules 或其他工具監控 Terraform 管理的資源是否被手動修改。(4)如果確實需要保留手動修改,用terraform import把變更同步回 state。 -
Secrets in State File(State 中的機密資訊):Terraform state file 以明文儲存所有資源的屬性,包括資料庫密碼、API Key 等敏感資訊。即使你用
variable傳入密碼而不是寫死在程式碼中,密碼仍然會出現在 state file 裡。預防措施:(1)State file 所在的 S3 bucket 必須加密且限制存取權限。(2)不要把 state file commit 到 Git(.gitignore加入*.tfstate*)。(3)敏感資源考慮用 secrets management 工具(如 AWS Secrets Manager、HashiCorp Vault)管理,Terraform 只引用 ARN 而不直接管理密碼值。(4)Terraform 1.5+ 支援sensitive標記,可以在 plan 輸出中隱藏敏感值(但 state file 中仍然是明文)。 -
大型 State File 的效能問題:當 Terraform 管理數百個資源時,每次 plan 都需要查詢所有資源的當前狀態,速度會明顯變慢。解決方式:(1)拆分 state——不要把所有資源放在一個 state 裡。按功能拆分(networking、compute、database 各自一個 state)。(2)使用
terraform plan -target=resource只 plan 特定資源(僅限除錯用,正常流程不建議)。(3)考慮使用 Terragrunt 等工具管理多個 Terraform state 之間的依賴關係。 -
Provider 版本升級的 Breaking Change:Provider 升級可能改變資源的行為或屬性格式。例如 AWS Provider 從 4.x 升到 5.x 有大量 breaking change。預防措施:(1)在
required_providers中鎖定版本範圍(例如~> 5.0允許 5.x 但不允許 6.x)。(2)使用.terraform.lock.hcl鎖定精確版本,確保團隊成員使用相同版本。(3)升級前先在 dev 環境測試,確認 plan 沒有預期外的變更。
延伸閱讀
- Production 的規劃與管理 — IaC 搭配環境拆分策略,用同一套 module 管理多套環境
- CD Templates — 建立 Terraform 的 CI/CD pipeline,實現 plan-on-MR、apply-on-merge 的 GitOps 流程
- Secrets & Config 管理 — 處理 Terraform state 中的敏感資訊,搭配 Vault 或 AWS Secrets Manager
- 備份策略與災難復原 — Terraform state file 的備份策略,S3 versioning 與 state 復原