22-Terraform
基础设施即代码(Infrastructure as Code, IaC)是把基础设施的创建、变更、销毁用代码描述并版本化管理的实践。Terraform 是 HashiCorp 出品的 IaC 工具,曾一度是这个领域的事实标准。本章覆盖 Terraform 的核心概念、工作流、State 管理、模块化、多环境实践与常见陷阱,并从 Terragrunt 规模化实践中提取工程化要点。
IaC 解决什么问题#
没有 IaC 之前,基础设施状态散落在工程师脑子里、过时的 Wiki 文档里、控制台的历史记录里。带来几个致命问题:
- 无法复现:生产环境出了问题,无法在测试环境精确复现,因为两个环境配置已悄悄漂移。
- 变更追溯困难:安全合规要求"三个月前这个端口怎么开放的",没人知道。
- 团队协作摩擦:新人不知道为什么某个资源这样配置,不敢改不敢删,技术债越积越多。
IaC 用代码描述期望状态,用 Git 管理版本,用 CI/CD 执行变更。每次变更有历史记录、代码审查、自动化测试。
核心概念#
Provider#
Provider 是 Terraform 与各种 API 通信的桥梁。AWS、GCP、阿里云、GitHub、Kubernetes 都有对应的 Provider:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # 允许 5.x,不跨大版本
}
}
required_version = ">= 1.6.0"
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
}
}
}Resource#
Resource 是 Terraform 管理的最小单元,对应一个真实的基础设施资源:
# 语法:resource "<类型>" "<本地名称>" { ... }
resource "aws_s3_bucket" "logs" {
bucket = "my-company-logs-${var.environment}"
tags = {
Name = "Application Logs"
}
}
# 引用其他资源的属性
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id # 引用上面 bucket 的 id
versioning_configuration {
status = "Enabled"
}
}Module#
Module 是可复用的 Terraform 代码单元,类似函数。把通用基础设施(VPC、EKS、RDS)封装成 Module,多个环境引用同一份 Module 传不同参数:
module "eks" {
source = "../../modules/eks-cluster"
# 或使用 Terraform Registry 公共模块
# source = "terraform-aws-modules/eks/aws"
# version = "~> 20.0"
cluster_name = "prod-cluster"
cluster_version = "1.30"
node_groups = {
general = {
instance_types = ["m5.xlarge"]
min_size = 2
max_size = 20
desired_size = 3
}
}
}State#
State 是 Terraform 的核心,记录"Terraform 认为真实世界现在是什么状态"。它的作用:
- 记录 Terraform 管理的资源列表及其 ID
- 计算
plan时的 diff(期望状态 vs 当前状态) - 追踪资源依赖关系
State 是敏感数据:可能包含数据库密码、私钥等,不能放到 Git 里。
基本工作流#
# 初始化:下载 Provider 插件
terraform init
# 格式化代码
terraform fmt -recursive
# 语法检查
terraform validate
# 预览变更(最重要的命令,必须仔细看)
terraform plan -out=tfplan
# 应用变更
terraform apply tfplan
# 销毁资源(危险!生产环境谨慎使用)
terraform destroyplan 的输出要仔细看:
# aws_instance.web will be updated in-place ← 原地更新,低风险
~ resource "aws_instance" "web" {
~ instance_type = "t3.small" -> "t3.medium"
}
# aws_db_instance.main must be replaced ← 销毁重建!高风险
-/+ resource "aws_db_instance" "main" {
~ identifier = "prod-db" -> "prod-db-v2"
}看到 must be replaced 要非常小心——某些资源(RDS、ElastiCache)重建会有停机时间。
State 管理#
Remote State(S3 + DynamoDB)#
生产环境 State 必须远端存储并加锁,防止多人并发操作:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/eks/terraform.tfstate"
region = "us-west-2"
encrypt = true
kms_key_id = "arn:aws:kms:us-west-2:123456789012:key/mrk-xxx"
use_lockfile = true # S3 原生锁(1.10 引入、1.11 起默认推荐)
# dynamodb_table = "terraform-state-lock" # DynamoDB 锁已于 1.11 弃用,未来版本移除
}
}创建 backend 资源(这部分不能用 Terraform 管理自己的 backend):
aws s3api create-bucket --bucket my-terraform-state --region us-west-2 \
--create-bucket-configuration LocationConstraint=us-west-2
aws s3api put-bucket-versioning --bucket my-terraform-state \
--versioning-configuration Status=Enabled
# S3 原生锁(use_lockfile)无需额外资源;下面的 DynamoDB 锁表仅为兼容旧配置,
# 1.11 起 dynamodb_table 已弃用,新项目直接用 use_lockfile = true
aws dynamodb create-table --table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST阿里云 OSS 后端:
backend "oss" {
bucket = "my-terraform-state"
prefix = "prod/eks"
region = "cn-hangzhou"
encrypt = true
tablestore_endpoint = "https://xxx.cn-hangzhou.ots.aliyuncs.com"
tablestore_table = "terraform-lock"
}State 操作#
# 查看 state 中的资源列表
terraform state list
# 查看单个资源的 state 详情
terraform state show aws_eks_cluster.main
# 将已有资源导入 state(资源已存在,但不在 state 中)
terraform import aws_s3_bucket.legacy my-existing-bucket-name
# 移动资源(重构代码时)
terraform state mv aws_security_group.old_name aws_security_group.new_name
# 从 state 中移除资源(不删除真实资源,只停止 Terraform 管理)
terraform state rm aws_instance.temporary模块化与多环境#
目录结构#
infrastructure/
├── modules/ # 可复用模块
│ ├── eks-cluster/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── rds-instance/
│ └── networking/
├── environments/ # 环境配置
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
└── global/ # IAM、Route53 等全局资源Workspace vs 独立目录#
Workspace 允许同一套代码管理多个环境的 State,但实际生产中更推荐用独立目录隔离环境:
- Workspace:同一份代码 + 同一目录,通过
terraform workspace select dev/prod切换。风险是容易误操作——在 prod workspace 里跑 dev 的配置。 - 独立目录:每个环境一个目录,各自有 backend 配置和 tfvars。隔离彻底,不会误操作。
经验:Workspace 适合临时环境(PR preview),生产环境用独立目录。
漂移检测与修正#
有人直接在控制台改了资源,导致真实状态和 State 不一致:
# 检测漂移(不做任何变更,只刷新 state)
terraform plan -refresh-only
# 将真实状态同步到 State(不修改真实资源)
terraform apply -refresh-only常见坑#
1. State 漂移#
手动改控制台导致漂移。解决:CI 定期跑 terraform plan -refresh-only,有 diff 就告警。
2. Destroy 顺序问题#
Terraform 通常能自动处理依赖顺序,但某些情况需手动指定 depends_on:
resource "aws_eks_fargate_profile" "coredns" {
depends_on = [aws_eks_addon.coredns] # 必须等 EKS 集群完全就绪
}3. Provider 版本漂移#
不锁版本每次 terraform init 可能下载不同版本:
# 生成 .terraform.lock.hcl 后提交到 Git
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_arm644. 敏感值泄露到 State#
数据库密码、私钥如果放在 resource 里,会明文存在 State 中:
# 不要这样做
resource "aws_db_instance" "main" {
password = "my-hardcoded-password" # 会出现在 state 里!
}
# 用 random_password 生成 + Secrets Manager 存储
resource "random_password" "db" {
length = 32
special = false
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = random_password.db.result
}5. 改了 identifier 触发重建#
某些资源改了不可变字段会触发销毁重建(RDS、ElastiCache),生产环境会有停机。plan 看到 must be replaced 要特别警惕。
规模化实践(从 Terragrunt 提取)#
当 state 超过 20 个时,纯 Terraform 的管理会变得痛苦——backend.tf 到处复制、provider 配置到处复制、跨 state 引用要手写 terraform_remote_state、批量 apply 要手动 cd。
Terragrunt 是 Terraform 的 wrapper,解决规模化问题:
DRY 的 backend 和 provider#
一份根 terragrunt.hcl 定义 backend 和 provider,所有子单元通过 include 继承:
# live/terragrunt.hcl(根配置)
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
bucket = "my-company-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-west-2"
encrypt = true
use_lockfile = true # S3 原生锁;dynamodb_table 已于 TF 1.11 弃用
}
}# live/prod/us-west-2/vpc/terragrunt.hcl(子单元)
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::git@github.com:org/terraform-modules.git//vpc?ref=v1.5.0"
}
inputs = {
cidr_block = "10.0.0.0/16"
}跨 state 依赖#
# live/prod/us-west-2/eks/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-mock"
private_subnet_ids = ["subnet-mock-1", "subnet-mock-2"]
}
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}Terragrunt 在 apply eks 之前会先检查 ../vpc 是否已 apply,从其 state 读 outputs 注入到当前 inputs。mock_outputs 让 plan/validate 阶段不报错。
铁律:mock_outputs_allowed_terraform_commands 只列 ["validate", "plan"],永远不要加 apply。
批量操作#
# 在整个 live/prod 目录下按依赖顺序 plan 所有 state
cd live/prod
terragrunt run --all plan
# apply 所有(限并发避免打爆 API)
terragrunt run --all apply --parallelism 4CLI 重构后,旧的
run-all子命令与--terragrunt-前缀参数已废弃(legacy 参数在 v0.85.0 移除),改用run --all+--parallelism。
run --all 递归扫描所有 terragrunt.hcl,解析 dependency 构建 DAG,按拓扑顺序调用 Terraform(无依赖的并发)。
什么时候用 Terragrunt#
- State 少于 10 个:直接写 Terraform 更简单。
- State 超过 20 个 + 多环境/多 region:Terragrunt 的甜蜜区。
- 完全 Pulumi 体系:Pulumi 有自己的 Stack + Component,Terragrunt 管不了。
小结#
Terraform 的核心工作流是 init → plan → apply,plan 是最重要的命令——看到 must be replaced 要警惕。State 是敏感数据,必须远端存储(S3/OSS + 锁表),不能进 Git。多环境用独立目录隔离比 Workspace 更安全。模块化是复用的关键,目录结构按 modules + environments 分层。规模化场景(state 超过 20 个)引入 Terragrunt 解决 DRY、跨 state 依赖、批量操作三大痛点。核心实践不变:state 远端存储 + lock + PR 驱动的 plan + 强制 policy 检查。