2026 年 2 月,我们一个 60 人工程团队共用的 Terraform 基础设施仓库,出现 state lock 死锁:一个团队的 CI 跑到一半被 kill,DynamoDB 里残留 lock 记录,后续所有团队的 plan / apply 全部卡死,7 小时影响 4 个 release window,SRE 群里炸了 230 条消息。我们以为是 AWS DynamoDB 故障,排查 5 天才发现真凶是"残留 lock + force-unlock 滥用 + state file 并发写竞争"三重叠加。这是 Terraform 多团队协作的经典反模式。
这次复盘是 IaC 工程化的深度教程。从最初看 terraform plan 卡住 2 分钟报 "Error acquiring the state lock"、到用 AWS DynamoDB 查 lock 表、再到用 terraform force-unlock 救急、最终落地"state 拆分 + lock 监控 + workspace 隔离"的工程化方案。这篇给一份"Terraform 多团队协作 SOP + 反模式清单"。
项目背景:多团队共享基础设施仓库
| 维度 | 规模/参数 |
|---|---|
| Terraform 版本 | 1.7.4 |
| 团队数 | 12 个团队、60 工程师 |
| 资源数 | 3800+ AWS 资源 |
| state 文件大小 | 47 MB |
| 每日 CI 跑次数 | ~400 次 plan + ~60 次 apply |
| state backend | S3 + DynamoDB(锁) |
| 正常 plan 时间 | 3-5 分钟 |
| 事故期 plan 时间 | 卡死 60+ 分钟 |
这套 Terraform 仓库管理我们整个 AWS 基础设施,从 VPC、EKS、RDS 到 S3、Lambda,12 个团队全部往里塞资源。看起来"统一管理"很美好,实际单一 state 文件让并发成了大问题——这就是问题所在。
事故时间线
| 时间 | 事件 |
|---|---|
| D1 09:00 | 支付团队 CI 跑 terraform apply,运行 15 分钟 |
| D1 09:15 | K8s 集群滚动更新触发 CI runner 重启,apply 被 SIGKILL |
| D1 09:16 | DynamoDB 里残留 lock,Owner=ci-runner-pay-001 |
| D1 09:30 | 风控团队 plan 报 Error acquiring lock |
| D1 10:00 | 更多团队 PR 阻塞,7 个 release window 受影响 |
| D1 10:30 | SRE 介入,执行 force-unlock |
| D1 10:45 | 2 个团队同时 apply,state 文件冲突 corrupted |
| D1 12:00 | 从 S3 版本历史恢复 state,业务恢复 |
| D2-D5 | 深度复盘 + 重构方案 |
第一轮:误以为 DynamoDB 故障
# 1. 查 DynamoDB 状态
aws dynamodb describe-table --table-name terraform-state-lock
# Table status: ACTIVE,正常
# 2. 查 lock 记录
aws dynamodb scan --table-name terraform-state-lock
# {
# "LockID": "terraform/prod/infrastructure.tfstate-md5",
# "Info": "{...,\"Who\":\"ci-runner-pay-001@runner-77f6\",\"Created\":\"2026-02-12T09:00:12Z\"}"
# }
# 锁存在,但 owner 进程已经死了 1 小时!
# 3. 查 CI runner 状态
kubectl get pods -n ci-runners | grep pay-001
# pay-001-77f6 Terminating ...
# 已经 terminating 1 小时,但 lock 没释放
第二轮:为什么 lock 没自动释放?
# Terraform 的 state lock 机制
# 1. 开始 plan/apply 前,向 DynamoDB 写一条 lock 记录
# 2. 完成后(无论成功失败),主动删除 lock
# 3. 进程被 KILL → lock 留在那里
# 关键:Terraform 没有 TTL 机制!
# DynamoDB 表本身可以设 TTL,但 Terraform 没有用
# 看 lock 记录
aws dynamodb get-item --table-name terraform-state-lock \
--key '{"LockID":{"S":"terraform/prod/infrastructure.tfstate-md5"}}'
# {
# "Item": {
# "LockID": {...},
# "Info": {...},
# "Digest": "..." ← Terraform 自定义字段,无 TTL
# }
# }
# 解决方案 A:DynamoDB 表加 TTL
aws dynamodb update-time-to-live --table-name terraform-state-lock \
--time-to-live-specification "Enabled=true, AttributeName=expiry"
# 但 Terraform 写 lock 时不会写 expiry 字段,无效
# 解决方案 B:自定义清理脚本
# cron 每 5 分钟扫一次,找出超过 30 分钟的 lock 并清理
第三轮:force-unlock 的滥用
# 紧急救场:force-unlock
terraform force-unlock -force
# 但有风险!
# 场景 A:owner 进程已死(本次情况)
# force-unlock 安全,因为没人在写 state
# 场景 B:owner 进程还活着(常见误判)
# force-unlock 后,owner 完成时会写 state
# 但另一个进程已经拿到 lock 并修改了 state
# 最终 owner 写入会覆盖另一个进程的修改 → state 损坏
# 我们的反模式:SRE 看到 lock 就 force-unlock
# 没有验证 owner 进程是否真的死了
# 结果:9:16 force-unlock 后,9:30 两个团队同时 apply
# 正确做法
# 1. 先 kubectl get pod $owner 看 owner 是否存活
# 2. 看 owner pod 的 logs 确认是否还在跑 terraform
# 3. 用 ps -ef | grep terraform 确认进程
# 4. 全部确认死亡后,才 force-unlock
第四轮:state 并发写竞争
# state file 是单一 JSON 文件
# 任何 apply 都会读全文 + 修改 + 写全文
# 多团队并发 apply,即使没有 lock,也会冲突
# 我们 9:30 - 10:00 的冲突时间线
# 团队 A:get state v100 → 修改 → 写 v101(15s)
# 团队 B:get state v100 → 修改 → 写 v101(同时,后写)
# 结果:v100 → v101(B 的版本),A 的修改全丢
# Terraform 的乐观锁
# 写 state 时会带上 serial(版本号)
# 如果 backend 检测到 serial 已变,会拒绝写
# 但 lock 没释放就跑,触发不到这个机制
# state file 大小问题
# 47 MB,每次 plan 要下载全文,5-10s 网络开销
# 大文件 PUT 到 S3 也慢
# 多团队并发,S3 流量打满
# 实际生产应该 < 5 MB
问题本质:三重叠加
修法 1:state 拆分(按团队/服务隔离)
# 重构前:单 state 文件
# terraform/prod/infrastructure.tfstate(47MB)
# 包含 12 个团队的资源
# 重构后:按团队拆分 state
# terraform/prod/pay/main.tfstate(4MB,支付团队)
# terraform/prod/risk/main.tfstate(3MB,风控团队)
# terraform/prod/iam/main.tfstate(2MB,平台团队)
# ...
# backend 配置每个 state 独立
terraform {
backend "s3" {
bucket = "company-tfstate"
key = "prod/pay/main.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock-pay" # 锁也拆分
encrypt = true
}
}
# 团队间用 remote_state data source 引用
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "company-tfstate"
key = "prod/network/main.tfstate" # 只读,不锁
region = "us-east-1"
}
}
resource "aws_eks_cluster" "pay" {
vpc_config {
subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnets
}
}
修法 2:state lock 自动清理
# Lambda 函数,每 5 分钟扫一次
# 清理超过 60 分钟的 lock
import boto3
import json
from datetime import datetime, timezone, timedelta
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
ec2 = boto3.client('ec2')
tables = [
'terraform-state-lock-pay',
'terraform-state-lock-risk',
# ... 每个团队的 lock 表
]
for table_name in tables:
table = dynamodb.Table(table_name)
resp = table.scan()
for item in resp.get('Items', []):
info = json.loads(item['Info'])
created = datetime.fromisoformat(info['Created'].replace('Z', '+00:00'))
age = datetime.now(timezone.utc) - created
if age > timedelta(minutes=60):
# 检查 owner 是否还活着
owner = info.get('Who', '')
if not is_owner_alive(owner):
# 安全删除 lock
table.delete_item(Key={'LockID': item['LockID']})
notify_team(f"Cleaned stale lock: {item['LockID']}")
else:
notify_sre(f"WARNING: Lock {item['LockID']} old but owner alive!")
def is_owner_alive(owner):
# owner 格式: "ci-runner-pay-001@runner-77f6"
# 查 K8s API,看 pod 是否存在
pod_name = owner.split('@')[1]
# ... call K8s API
return False
修法 3:Atlantis 流水线管控
# Atlantis:Terraform 的 PR-based 自动化平台
# 强制串行化每个仓库的 apply
# atlantis.yaml
version: 3
projects:
- name: pay-infra
dir: terraform/prod/pay
workflow: pay-workflow
autoplan:
when_modified:
- "*.tf"
enabled: true
apply_requirements:
- approved
- mergeable
workflow_lock_timeout: 1800 # 30 分钟超时
workflows:
pay-workflow:
plan:
steps:
- init
- plan
apply:
steps:
- apply
- run: ./scripts/notify-team.sh
# Atlantis 优势
# 1. PR 评论触发,有审计
# 2. 自动 plan 出 diff,review 容易
# 3. 同一项目串行执行,无锁竞争
# 4. apply 超时自动释放锁
# 5. 失败有清晰报错
修法 4:CI runner 优雅关闭
# K8s CI runner 添加 PreStop hook
apiVersion: v1
kind: Pod
spec:
containers:
- name: terraform-runner
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# 给当前 terraform 进程 60 秒优雅退出
pid=$(pgrep terraform)
if [ -n "$pid" ]; then
kill -TERM $pid
for i in $(seq 1 60); do
if ! kill -0 $pid 2>/dev/null; then
break
fi
sleep 1
done
fi
# 主动释放 lock(如果 terraform 没自己释放)
# ./scripts/cleanup-lock.sh
terminationGracePeriodSeconds: 90 # 大于 PreStop 60 秒
# K8s 节点 drain 时
# 1. 发 SIGTERM 给 pod
# 2. PreStop 给 terraform 60 秒退出
# 3. terraform 完成 apply,释放 lock,正常退出
# 4. Pod terminated,无残留 lock
修法 5:state 大小治理
# 检查 state 大小
terraform state list | wc -l
# 1850 个资源,太多
# 拆分思路
# 1. 按生命周期分:network(很少变)、compute(经常变)
# 2. 按团队分:每个团队独立 state
# 3. 按服务分:每个微服务一个 state
# 拆分操作:terraform state mv
# 1. 复制 .tf 文件到新仓库
cp -r modules/payment new-repo/
# 2. 从旧 state 中移除资源
cd old-repo
terraform state rm 'aws_db_instance.payment_db'
# 3. 在新 repo 中 import
cd new-repo
terraform import aws_db_instance.payment_db payment-db-prod
# 4. 验证 plan 无 diff
terraform plan
# No changes. Your infrastructure matches the configuration.
# 5. 拆完后,旧 state 减肥
# 47 MB → 8 MB(平台基础设施)+ 各团队 < 5 MB
决策树:Terraform 出问题怎么办
我们立的 12 条 Terraform 工程纪律
- state 必须按团队/服务拆分,单 state < 5MB,< 200 资源;
- lock 表自动清理,超过 60 分钟无活进程的 lock 自动 delete;
- 禁止人工 force-unlock,必须走脚本验证 owner 状态;
- 所有 apply 走 Atlantis,禁止本地直接 apply 生产;
- CI runner 加 PreStop hook,优雅退出,释放锁;
- state 启用 S3 版本控制,保留 30 天历史;
- state 启用 S3 备份,跨 region 复制;
- 团队间用 remote_state data source,只读引用,不直接修改;
- 每个 module 必须有 README + 示例;
- 禁用 terraform destroy 生产环境,必须先 terraform state rm + 手动确认;
- provider 版本固定,terraform.lock.hcl 必须提交;
- state 内容定期 dump 备份,防止意外覆盖。
引申一:Terragrunt 与多环境管理
# Terragrunt 是 Terraform 的 wrapper
# 解决"DRY"和"多环境"问题
# 目录结构
# environments/
# prod/
# terragrunt.hcl ← 环境配置
# pay/
# terragrunt.hcl ← 引用 module
# staging/
# terragrunt.hcl
# pay/
# terragrunt.hcl
# environments/prod/terragrunt.hcl
remote_state {
backend = "s3"
config = {
bucket = "company-tfstate-prod"
key = "${path_relative_to_include()}/main.tfstate"
region = "us-east-1"
dynamodb_table = "tfstate-lock-prod"
}
}
# environments/prod/pay/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::ssh://git@github.com/company/tf-modules//pay?ref=v1.2.3"
}
inputs = {
environment = "prod"
vpc_id = "vpc-prod"
# ...
}
# 优势
# 1. 每个目录独立 state(自动拆分)
# 2. 多环境用相同 module,只换 inputs
# 3. terragrunt run-all 跨多个 state 操作
Terragrunt 是大型 Terraform 仓库的"事实标准"。它解决了原生 Terraform 在多环境、多团队、DRY 上的痛点,但学习曲线陡峭。我们后来把整个 Terraform 仓库迁移到 Terragrunt,所有团队、所有环境用统一规范,state 自动按目录拆分,跨环境引用也很自然。但要注意:Terragrunt 是 wrapper,debug 时要清楚是 wrapper 问题还是 Terraform 问题。
引申二:OpenTofu 与 Terraform 分裂
2023 年 HashiCorp 宣布 Terraform 改为 BUSL 1.1 协议,社区分叉出 OpenTofu(原 OpenTF)。OpenTofu 完全兼容 Terraform 1.5,后续会有自己的演进路径。我们 2026 年评估后,内部新项目优先用 OpenTofu(MPL 2.0 开源、Linux Foundation 治理),老项目继续用 Terraform。两者 100% 兼容 state 文件和 .tf 语法,迁移成本极低。如果你在意供应商锁定和开源治理,OpenTofu 是更好的选择;如果重度依赖 HashiCorp Cloud,继续 Terraform。
引申三:Pulumi / CDK 等 IaC 替代方案
| 工具 | 语言 | state 管理 | 生态 | 适用场景 |
|---|---|---|---|---|
| Terraform | HCL DSL | S3+DynamoDB | 最广 | 多云、大型团队 |
| OpenTofu | HCL DSL | 同 Terraform | 开源治理 | 多云、追求开源 |
| Pulumi | TS/Python/Go | Pulumi Cloud / 自管 | 开发者友好 | 代码逻辑复杂 |
| AWS CDK | TS/Python/Java | CloudFormation | AWS 深度 | 纯 AWS 项目 |
| Crossplane | K8s CRD | K8s etcd | K8s 原生 | 云原生团队 |
IaC 工具百花齐放,各有优劣。Terraform 仍是事实标准,但 Pulumi 在"开发者体验"和"复杂逻辑"上更胜一筹(可以用 TypeScript 写循环、函数、单元测试)。我们一个团队尝试 Pulumi 重写部分基础设施,开发体验确实好,但生态广度还是不如 Terraform。选型建议:稳态选 Terraform/OpenTofu,新项目可以 Pulumi 试点,K8s 重度用户考虑 Crossplane。
引申四:GitOps 与 IaC 的关系
GitOps(ArgoCD / Flux)和 IaC(Terraform)看似都是"声明式",但定位不同。IaC 管"创建/销毁"基础设施(VPC、EKS 集群、IAM),GitOps 管"部署"工作负载(Deployment、Service、Ingress)。两者配合:Terraform 创建 EKS 集群,ArgoCD 部署 K8s 应用到这个集群。这是 2026 年云原生最佳实践。我们的拓扑:Terraform 管 AWS 资源(IaC 层),Crossplane 桥接 K8s 和 AWS(中间层),ArgoCD 管 K8s 应用(GitOps 层)。三层各司其职,职责清晰。
引申五:Terraform Cloud vs 自建
# Terraform Cloud(HCP)
# 优势:
# 1. 托管 state(无需自建 S3/DynamoDB)
# 2. 内置 VCS 集成(GitHub / GitLab)
# 3. 内置 cost estimation(IaC 成本预估)
# 4. 内置 policy as code(Sentinel / OPA)
# 5. 团队权限管理
# 劣势:
# 1. 收费(每 worker $20/月)
# 2. state 数据在 HashiCorp(合规可能受限)
# 3. 国内访问慢
# 自建(S3 + DynamoDB + Atlantis)
# 优势:state 自管、可定制、低成本
# 劣势:运维成本、缺少高级功能
# 我们的选择
# 国内业务:自建(S3 + Atlantis + Vault)
# 海外业务:Terraform Cloud(Business tier)
是否上 Terraform Cloud 取决于团队规模和需求。< 20 人团队建议直接上 Terraform Cloud,省去自建运维成本;> 50 人团队可能需要更深度的定制,自建 + Atlantis 更灵活。HCP 的成本估算和 Sentinel 策略是杀手锏特性,我们海外业务每次 PR 自动算出新增云成本,运维成本意识大幅提升。
引申六:Sentinel / OPA Policy as Code
# 用 OPA(Open Policy Agent)做合规检查
# policy.rego
package terraform.aws.security
# 拒绝公网暴露的 S3 bucket
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.acl == "public-read"
msg := sprintf("S3 bucket %s 不允许公网读取", [resource.address])
}
# 拒绝没有加密的 RDS
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
resource.change.after.storage_encrypted != true
msg := sprintf("RDS %s 必须开启加密", [resource.address])
}
# 拒绝大于 8 vCPU 的 instance(成本控制)
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
instance_type := resource.change.after.instance_type
large_types := {"m5.4xlarge", "m5.8xlarge", "m5.16xlarge"}
large_types[instance_type]
msg := sprintf("EC2 %s 实例类型 %s 需要架构审批", [resource.address, instance_type])
}
Policy as Code 是 IaC 工程化的"安全网"。不靠人工 review,所有 PR 自动跑 OPA / Sentinel 检查,违反策略的 PR 自动 block。我们落地后,公网 S3 bucket 创建归零,RDS 加密率 100%,大 instance 必须走架构 review。这套机制让基础设施的安全合规从"事后补救"变成"事前防御",是大团队 IaC 落地的必经之路。
引申七:Terraform 性能优化
# 大型仓库 plan 慢的优化
# 1. 用 -target 限定范围
terraform plan -target=aws_eks_cluster.main
# 只对特定资源 plan,跳过其他
# 2. 并行度调优
terraform apply -parallelism=20 # 默认 10
# 但要注意 provider rate limit
# 3. 用 refresh=false 跳过刷新
terraform plan -refresh=false
# 仅看 .tf 文件变化,不查询云 API
# 但可能漏掉 drift
# 4. state pull/push 离线模式
terraform state pull > local.tfstate
# 本地编辑后 push 回去
terraform state push local.tfstate
# 用于大批量手动修改
# 5. 多 provider 并行
terraform {
required_providers {
aws_us_east_1 = {
source = "hashicorp/aws"
configuration_aliases = [aws.us_east_1]
}
aws_eu_west_1 = {
source = "hashicorp/aws"
configuration_aliases = [aws.eu_west_1]
}
}
}
# 不同 region 用不同 alias,并发请求
大型仓库的 Terraform 性能是个工程问题。4000 个资源的 plan 默认要 8 分钟,优化后能压到 2 分钟。常用招数:state 拆分(根本解)、-target 限定、parallelism 调优、refresh=false 跳过查询。我们后来上了 Terragrunt + 拆分,单个目录 plan 时间从 8 分钟降到 30 秒,工程师体验大幅提升。
引申八:IaC 漂移检测与自动修复
# Drift detection:云上资源被手动改了,与 .tf 不一致
terraform plan -detailed-exitcode
# 0 = 无 diff
# 1 = 错误
# 2 = 有 diff(漂移)
# 定时跑 drift detection
# 每 6 小时一次,有 diff 自动告警
0 */6 * * * cd /repo && terraform plan -detailed-exitcode || alert
# 自动修复(谨慎!)
terraform apply -auto-approve
# 注意:可能覆盖紧急修复
# 推荐策略
# 1. drift 仅告警,不自动修复
# 2. 人工 review 是不是有人改了云
# 3. 如果是合理变更,反向同步到 .tf
# 4. 如果是误改,terraform apply 修回
# 工具:driftctl(原)、Terraform Cloud 内置 drift
云上资源漂移是 IaC 的最大敌人。有人手动改云控制台,Terraform 不知道,下次 plan 时报"未管理资源",或被错误覆盖。我们立规:任何人不许在控制台改生产环境,必须通过 PR + Terraform。但总有"特殊情况"破例,所以定时 drift detection 是必备。发现漂移后第一步不是 apply,而是查谁、为啥改,合规化后反向同步到 .tf。
引申九:Secret 管理与 IaC 结合
# 错误做法:secret 写在 .tf 里
resource "aws_db_instance" "default" {
password = "Super$ecret123" # ❌ 提交到 Git 就泄漏
}
# 正确做法 A:AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/rds/password"
}
resource "aws_db_instance" "default" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
# 正确做法 B:HashiCorp Vault
provider "vault" {}
data "vault_generic_secret" "db" {
path = "secret/prod/rds"
}
resource "aws_db_instance" "default" {
password = data.vault_generic_secret.db.data["password"]
}
# state 中的 secret 也要保护
# state 本身就含敏感数据,所以
# 1. S3 bucket 必须加密 + access log
# 2. 只有 CI/SRE 能访问
# 3. 定期审计 state 访问
Secret 管理是 IaC 工程化的盲区。很多团队把 secret 写死在 .tf 里,提交 Git 后被同事 / 攻击者拿到。正确做法是 Secrets Manager / Vault 集中管理,Terraform 通过 data source 读取。我们一个团队曾因为 RDS password 提交 GitHub 公开仓库,被攻击者撞库,4 小时损失 80 万。事后立规:任何 secret 都不许出现在 .tf 文件,git pre-commit hook 用 gitleaks 扫描提交内容,有 secret 直接 block。
引申十:多云策略与 IaC 抽象
多云(AWS + GCP + Azure)是大企业的常态,但 Terraform 的 provider 是云特定的。aws_s3_bucket 和 google_storage_bucket 是两个完全不同的资源,无法直接通用。解决思路:用 module 封装"概念"(如 storage),module 内部根据 cloud 参数调不同 provider。或者用 Crossplane / Pulumi 这种更抽象的工具。我们一个跨云项目尝试过统一 module,发现维护成本极高,最终采取"每个云独立目录、共享 outputs"的折衷方案。多云的真正难点不是 IaC,而是网络、身份、监控的统一,这是未来 5 年的演进方向。
引申十一:Terraform Workspace 的正确使用与陷阱
# Workspace:同一份代码,多个 state
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select prod
terraform apply
# 代码中引用 workspace
locals {
env = terraform.workspace # "prod"
instance_count = local.env == "prod" ? 5 : 1
}
# 优点:简单
# 缺点(重要!):
# 1. 所有 workspace 共用同一份 .tf 代码
# 2. 改 dev 代码可能误改 prod
# 3. workspace 不适合"多环境隔离"
# 4. 只适合"临时分支测试"
# 反模式:用 workspace 区分 prod / staging
# 正模式:用不同目录 + 不同 backend 配置区分环境
# environments/prod/main.tf → state: prod.tfstate
# environments/staging/main.tf → state: staging.tfstate
Workspace 是 Terraform 最容易被误用的特性。HashiCorp 官方文档明确说"Workspace 不适合区分环境",但很多团队还是用 workspace 来管理 dev/staging/prod。我们一个团队曾经用 workspace 区分环境,某次 PR 想改 dev 配置,合并后 prod 也跟着变了,差点酿成事故。正确做法是不同目录 + 不同 backend + 不同 IAM 权限,从代码层面强隔离。这是 IaC 工程化最重要的一条铁律。
引申十二:Terraform Module 设计原则
# 一个好的 module 结构
# modules/vpc/
# main.tf ← 主资源
# variables.tf ← 输入参数
# outputs.tf ← 输出值
# versions.tf ← provider 版本
# README.md ← 文档 + 示例
# variables.tf 加约束
variable "cidr_block" {
type = string
description = "VPC CIDR block"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "enable_nat" {
type = bool
default = false
}
# outputs.tf 暴露必要值
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# 调用方
module "prod_vpc" {
source = "git::ssh://git@github.com/company/tf-modules.git//vpc?ref=v1.2.3"
cidr_block = "10.0.0.0/16"
enable_nat = true
}
# 关键原则
# 1. 版本化(git tag,禁用 master 引用)
# 2. 输入做 validation
# 3. 输出只暴露必要值(避免泄漏内部细节)
# 4. README 必有,含使用示例
# 5. 单元测试(terratest / kitchen-terraform)
Module 是 Terraform 工程化的核心。好的 module 让多个团队复用代码,差的 module 让所有团队互相干扰。我们的 module 仓库管理 80+ 个 module,从 VPC、EKS、RDS 到自定义业务组件,所有团队通过版本化引用。改一个 module 不影响线上,要走 PR + tag + 灰度。这套机制让 60 人团队的基础设施迭代有序可控,这是大团队 IaC 落地的精髓。
引申十三:Terraform 测试体系
// terratest 单元测试
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVPCModule(t *testing.T) {
opts := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"cidr_block": "10.0.0.0/16",
"enable_nat": true,
},
}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
vpcID := terraform.Output(t, opts, "vpc_id")
assert.NotEmpty(t, vpcID)
subnets := terraform.OutputList(t, opts, "private_subnet_ids")
assert.Equal(t, 3, len(subnets))
}
// 测试覆盖
// 1. 输入校验(传非法值应该报错)
// 2. 输出正确性(资源真的创建了)
// 3. 幂等性(连续 apply 两次,第二次应该 no change)
// 4. 销毁清洁(destroy 后云上无残留)
Terraform 代码也要测试。terratest 让你能像测业务代码一样测 IaC,跑在临时 AWS 账号上,自动 apply / verify / destroy。我们 CI 中每个 module PR 必须跑 terratest,通过才能合并。这套机制把 IaC 的质量从"靠 review"提升到"靠测试",生产事故率下降 80%。值得每个团队投入。
引申十四:Cost Management 与 IaC
# Infracost:在 PR 上自动算成本
# 安装
brew install infracost
# 在 PR 中跑
infracost breakdown --path=. --format=json --out-file=infracost-base.json
git checkout feature-branch
infracost diff --path=. --compare-to=infracost-base.json
# 输出
# Project: company/infrastructure
# + aws_instance.web +$24.82 /month
# + aws_rds_cluster.db +$432.16 /month
# Monthly cost change: +$456.98 (+12%)
# GitHub Action 集成
- name: Run Infracost
uses: infracost/actions/setup@v2
- name: Generate Infracost diff
run: infracost diff --path=. --format=json --out-file=/tmp/infracost.json
- name: Post Infracost comment
uses: infracost/actions/comment@v1
with:
path: /tmp/infracost.json
behavior: update
# 设置 budget alert
# 每月 PR 累计成本变化 > $10000 自动通知 CTO
云成本是 IaC 工程化的隐藏维度。每一行 .tf 都对应真金白银,但大多数工程师写代码时没有成本意识。Infracost 把成本可视化到 PR 上,review 时立刻能看到"这个 PR 每月多花多少",成本意识形成正反馈。我们落地后,工程师主动选 spot / 小机型 / 节流策略,基础设施成本季度环比下降 18%。这是 IaC 最容易被忽视但最有 ROI 的工程化项目。
引申十五:Terraform 与 SRE 文化
IaC 落地的真正难点不是技术,是文化。没有 SRE 文化的团队,即使上了 Terraform,也会"控制台改了再补 .tf"、"force-unlock 一把梭"、"prod 直接 apply"。我们花了 2 年时间推 SRE 文化:每个团队配一个 SRE BP、所有变更必走 PR、定期做事故复盘并归档、年度做"基础设施健康度评分"。技术工具是放大器,文化是底座。没有文化的 IaC 只会变成"高效率制造事故的机器",有文化的 IaC 才是"全公司基础设施的统一治理平台"。这是这次复盘最深刻的认知,也是每个 DevOps 工程师在 Tech Lead 路上必须思考的命题。
引申十六:Terraform 与多账户管理
# 多 AWS Account 管理(组织化)
# 生产、测试、开发各自独立账号
# AWS Organizations + IAM Identity Center
provider "aws" {
alias = "prod"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::PROD_ACCOUNT:role/TerraformExecutor"
}
}
provider "aws" {
alias = "shared_services"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::SHARED_ACCOUNT:role/TerraformExecutor"
}
}
# 资源指定 provider
resource "aws_vpc" "prod" {
provider = aws.prod
cidr_block = "10.0.0.0/16"
}
resource "aws_route53_zone" "shared" {
provider = aws.shared_services
name = "internal.company.com"
}
# 跨账号 VPC peering
resource "aws_vpc_peering_connection" "prod_to_shared" {
provider = aws.prod
peer_vpc_id = aws_vpc.shared.id
peer_owner_id = "SHARED_ACCOUNT"
vpc_id = aws_vpc.prod.id
}
# 关键纪律
# 1. 每个账号独立 state(prod 出问题不影响 dev)
# 2. CI 用最小权限 IAM Role(AssumeRole 跨账号)
# 3. 禁用根账号操作,全部走 IAM Identity Center
# 4. 跨账号资源(peering、Route53)放在 shared_services 账号
多 AWS 账号是大企业的标配,但 Terraform 跨账号管理需要精心设计。核心思路是"每个账号 1 个 state + provider alias + AssumeRole 跨账号操作"。我们公司有 38 个 AWS 账号(按业务、环境、合规分),Terraform 仓库 12 个,严格按账号边界拆分。这套结构虽然复杂,但事故隔离性极高,某个账号出问题不影响其他。这是企业级 IaC 工程化必经之路。
引申十七:Terraform 灾备与恢复演练
# 场景:state 文件被误删 / 损坏 / 恶意篡改
# 防御 1:S3 版本控制
aws s3api put-bucket-versioning \
--bucket company-tfstate \
--versioning-configuration Status=Enabled
# 恢复
aws s3api list-object-versions --bucket company-tfstate --prefix prod/pay/main.tfstate
# 拿到要恢复的 VersionId
aws s3api copy-object \
--copy-source "company-tfstate/prod/pay/main.tfstate?versionId=ABC123" \
--bucket company-tfstate \
--key prod/pay/main.tfstate
# 防御 2:跨 region 复制
aws s3api put-bucket-replication --bucket company-tfstate --replication-configuration ...
# 防御 3:每日导出
0 2 * * * aws s3 cp s3://company-tfstate/prod/ /backup/tfstate-$(date +%Y%m%d)/ --recursive
# 演练:每季度跑一次"模拟 state 丢失"
# 1. 备份当前 state
# 2. 删除 state
# 3. 用版本恢复
# 4. terraform plan 验证 no changes
# 5. 记录恢复时间(RTO)
# 我们的 RTO:从触发恢复到完全可用 12 分钟
State 文件是 IaC 的"皇冠明珠",一旦丢失或损坏,基础设施就处于"无主"状态。必须像数据库一样对 state 做容灾:版本控制、跨 region 复制、定期备份、定期演练。我们每季度的"灾备演练日",会随机选一个 state 模拟丢失,从备份恢复并验证。这套机制 3 年来救了我们两次:一次是新人误执行 terraform destroy 想清理,但其实指向了 prod backend;另一次是 S3 bucket 被勒索软件加密。两次都靠版本恢复 + 演练经验,30 分钟内完全恢复。
引申十八:IaC 团队的招聘与培养
IaC 工程师是稀缺人才,招聘和培养都有讲究。纯 DevOps 背景的人懂工具但不懂业务,纯开发背景的人懂业务但缺少基础设施全局观,理想的 IaC 工程师是两者结合。我们招聘 IaC 工程师的硬性要求:5 年以上工程经验、深入用过 Terraform/Pulumi/CDK 之一、对网络/安全/数据库有体系认知、能用代码思维写测试。培养路径:新人先做 module 维护(熟悉规范)、再做小项目主负责(独立思考)、最后能 own 整个仓库或多账号(架构思维)。一个高级 IaC 工程师能让 60 人团队的基础设施效率提升 3-5 倍,值得用 CTO 级别的薪资抢。
引申十九:从 Terraform 到 Platform Engineering
IaC 的终极目标不是"让 SRE 写 .tf",而是"让开发自助申请基础设施"。Platform Engineering 是 2026 年最火的方向:把基础设施能力封装成 Internal Developer Platform(IDP),开发通过表单或 CLI 自助申请数据库、Lambda、消息队列,后台自动生成 .tf + PR + apply。Backstage、Crossplane、Port 都是这个方向的代表工具。SRE 团队从"基础设施的搬砖工"变成"基础设施的产品经理",通过 Platform 把能力规模化交付给所有开发团队。这是云原生时代基础设施工程化的最终形态,也是每个 SRE 团队 2026-2028 年的演进方向。这次事故让我们更坚定了平台化的路线,IaC 只是起点不是终点,值得每一位 DevOps 工程师投入心血深度耕耘。
总结
这次 Terraform state 死锁雪崩事故,本质是"残留 lock + force-unlock 滥用 + state 并发写竞争"三重反模式叠加。每个问题单独存在都能跑,组合在 12 团队 60 人共用单 state 的场景下就是灾难。修复路径"state 拆分 + lock 自动清理 + Atlantis 流水线管控"三步走,把 plan 卡死从 60 分钟降到 0,工程师再也不用半夜被 lock 救场叫起来。
更重要的认知是:IaC 不是"写写 .tf 文件",而是一整套包括 state 治理、流水线管控、策略合规、漂移检测的工程体系。每一项都不是 Terraform 文档里能找到的,而是用一次次事故换来的实战经验。希望这篇能让所有用 Terraform 的团队少走弯路,把 IaC 从"基础工具"做到"工程体系",这是云原生时代基础设施工程师的核心能力,也是 60 人以上工程团队必须掌握的硬功夫,值得每一位 DevOps 工程师投入时间深度学习与精进。
事故复盘后我把这次教训写成《Terraform 多团队协作最佳实践》内部 wiki,组织全公司 SRE 培训三轮、所有工程师必须看视频回放并通过测试。这种从"事故 → 复盘 → 沉淀 → 培训 → 防御"的闭环,才是大公司 SRE 文化的精髓。一次事故的真正价值不在修复本身,而在把教训转化为整个团队的认知,让未来不再有人踩同样的坑。这是工程师对组织最大的贡献,也是个人成长最快的路径,值得每一位走在 Tech Lead 路上的工程师反复体会与践行。基础设施工程化的征途漫长,但每一次扎实的复盘与沉淀,都是向 Platform Engineering 终极形态迈进的坚实一步,也是云原生时代基础设施工程师的核心修养与终身追求,也是企业基础设施治理能力的真正分水岭与竞争壁垒。
—— 别看了 · 2026