概述
凌晨两点,手机炸了。某出行项目的运维群弹出一连串告警:RDS 主从切换失败、EIP 绑定状态异常、安全组规则缺失。排查了一圈,根因是有人手动在控制台改了安全组规则,Terraform 的 state 文件和云上实际状态不一致,一次 terraform apply 把手动修改的资源全部"纠正"回旧配置——直接覆盖了线上紧急热修复。
这不是个例。Terraform 的 state 文件是整个 IaC 体系的"唯一真相源"(Single Source of Truth),但它同时也是最脆弱的环节。state 文件丢失、锁死、配置漂移,这三个问题我在多个生产环境中全部遇到过。这篇文章不讲 Terraform 基础语法(相关文章:Terraform 基础设施即代码入门 已经讲过),而是聚焦于:生产环境中 state 文件怎么存、怎么锁、怎么迁移、出了事怎么救。
你会看到:
- state 文件的内部结构和它为什么这么重要
- S3 + DynamoDB 远程后端的完整生产级配置(含权限隔离方案)
- 状态锁卡死的排查和解除方法
- 配置漂移的检测、修复和预防策略
- 状态文件拆分迁移的实战步骤(附
terraform state mv的避坑指南) - 一个真实的"state 文件被误删"灾难恢复全过程
state 文件到底是什么
先说人话:state 文件就是 Terraform 的"资产清单"。
你用 Terraform 创建了一台 EC2、一个 RDS、一个 VPC,Terraform 需要记住这些资源的 ID、属性、依赖关系。不然下次 terraform plan 的时候,它怎么知道哪些资源已经创建过、哪些需要更新?
state 文件就是一个 JSON 文件,记录了所有被 Terraform 管理的资源的当前状态。每次 terraform apply 后自动更新。
state 文件的内部结构
打开一个 terraform.tfstate 文件,你会看到类似这样的结构(简化版):
{
"version": 4,
"terraform_version": "1.9.0",
"serial": 42,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web_server",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0abcd1234efgh5678",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.medium",
"tags": {
"Name": "web-server-prod",
"Environment": "production"
}
},
"dependencies": [
"aws_security_group.web_sg",
"aws_subnet.private_a"
]
}
]
}
]
}
几个关键字段:
| 字段 | 含义 | 为什么重要 |
|---|---|---|
version | state 文件格式版本 | Terraform 升级时自动迁移,但你得知道当前版本 |
serial | 每次 apply 自增的序号 | 用于检测并发写入冲突,serial 不匹配会报错 |
lineage | state 文件的唯一标识 ID | 两个不同 lineage 的 state 合并会直接拒绝 |
resources | 资源列表 | 核心数据,每个资源包含 ID、属性、依赖 |
dependencies | 资源依赖关系 | 决定了创建和销毁的顺序 |
三层数据对比机制
Terraform 的 plan 命令不是简单读一下 state 文件。它同时对比三组数据源:
- 本地配置文件(
.tf文件):你期望的基础设施状态 - state 文件(
.tfstate):Terraform 认为的上一次部署状态 - 云平台真实状态(通过 API 实时拉取):线上实际跑着什么
三层对比后,Terraform 算出差异:哪些资源需要新建、哪些需要更新、哪些需要销毁。
这个机制的关键在于——state 文件是 Terraform 连接"代码"和"真实云资源"的唯一桥梁。如果 state 文件丢了或者和真实状态不一致,Terraform 就"瞎"了,要么重复创建资源,要么误删资源。
这也是为什么 state 文件不能提交到 Git 仓库。state 文件包含敏感信息(资源 ID、IP 地址、密码等),而且多人同时修改会产生冲突。正确做法是放在远程后端统一管理。
远程后端选型:S3 + DynamoDB 为什么是标配
本地存储的三大坑
默认情况下,Terraform 把 state 文件存在本地工作目录的 terraform.tfstate。这在个人玩具项目里没问题,生产环境里会踩三个坑:
- 多人协作冲突:A 和 B 同时改基础设施,各自的
terraform.tfstate不一致,谁后 apply 谁覆盖谁 - state 文件丢失:本地磁盘损坏或者容器被销毁,state 文件没了,所有资源变成"孤儿"——云上还跑着,但 Terraform 不知道它们存在
- 安全隐患:state 文件以明文存储敏感信息(数据库密码、密钥等),放在本地等于裸奔
后端选型对比
| 后端类型 | 状态锁定 | 版本控制 | 适用场景 | 推荐度 |
|---|---|---|---|---|
| S3 + DynamoDB | 支持 | S3 版本控制 | AWS 环境 | 强烈推荐 |
| Azure Blob | 原生支持 | 软删除 | Azure 环境 | 推荐 |
| GCS | 原生支持 | 对象版本控制 | GCP 环境 | 推荐 |
| HCP Terraform | 内置 | 内置 | 多云/团队协作 | 推荐(付费) |
| Consul | 支持 | KV 版本 | 已有 Consul 集群 | 可用 |
| 本地文件 | 不支持 | 靠 Git | 仅个人开发 | 不推荐 |
| Kubernetes Secret | 支持 | 无 | K8s 内部资源管理 | 特殊场景 |
我在多个项目中最常用的是 S3 + DynamoDB 组合。原因很简单:
- S3 提供 99.999999999%(11 个 9)的持久性,state 文件几乎不可能丢
- DynamoDB 提供分布式锁,防止多人同时 apply 导致 state 覆盖
- S3 版本控制 + 生命周期策略可以自动保留历史版本,出问题随时回滚
- 权限可以用 IAM 精确控制,不同环境用不同 IAM 角色
S3 + DynamoDB 完整配置
下面是一套可以直接用于生产的后端配置:
# backend.tf — 远程状态后端配置
terraform {
required_version = ">= 1.5.0"
backend "s3" {
bucket = "my-company-terraform-state-prod"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock-prod"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
关键字段解释:
bucket:存放 state 文件的 S3 桶,必须提前创建并开启版本控制key:state 文件在 S3 中的路径。按"环境/模块/terraform.tfstate"分层,别全堆在一个文件里dynamodb_table:状态锁表,必须有一列名为LockID的主键encrypt:开启服务端加密kms_key_id:使用 KMS 托管密钥加密,比 S3 默认加密更安全
DynamoDB 锁表的创建:
# dynamodb.tf — 状态锁表(只需创建一次)
resource "aws_dynamodb_table" "terraform_state_lock" {
name = "terraform-state-lock-prod"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
server_side_encryption {
enabled = true
}
tags = {
Name = "terraform-state-lock"
Environment = "production"
ManagedBy = "Terraform"
}
}
踩坑提醒:DynamoDB 锁表的
hash_key必须叫LockID(区分大小写)。我见过有人写成lock_id或lockId,结果 Terraform 报错 “ConditionalCheckFailedException” 还查了半天。
S3 桶的安全配置
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-terraform-state-prod"
lifecycle { prevent_destroy = true } # 防止误删
}
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"
kms_master_key_id = aws_kms_key.terraform_state.arn
}
}
}
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
}
这套配置的防护层次:
prevent_destroy = true防止 Terraform 误删 S3 桶- 版本控制保留所有历史版本,随时可回滚
- KMS 加密防止数据泄露
- 公网访问全屏蔽,只能通过 IAM 认证访问
- 生命周期策略控制成本,旧版本自动降级到 Glacier
状态锁:防止多人同时 apply 的核心机制
锁的工作原理
状态锁的原理很直白:执行 terraform apply 或 terraform plan 前,Terraform 先在 DynamoDB 表里写一行记录。如果发现锁已存在,就拒绝执行,报错退出。
$ terraform apply
Acquiring state lock. This may take a few moments...
Releasing state lock. This may take a few moments...
执行完成后,Terraform 自动释放锁。正常情况下你不需要手动干预。
锁卡死了怎么办
问题出在异常场景:Terraform 执行到一半进程被杀(比如 CI/CD 超时)、网络中断、本地机器蓝屏。这时候锁不会被自动释放,下次执行就报错:
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-2024-01-15-160523
Path: prod/network/terraform.tfstate
Operation: OperationTypeApply
Who: ci-runner@build-server-01
Created: 2026-07-25 12:30:00.123456789 +0000 UTC
Info:
遇到这种情况,先确认那个进程确实已经死了。去 CI/CD 平台看 job 状态、去服务器看进程列表。确认进程已退出后,手动解锁:
# 强制解锁(确认进程已退出后使用)
terraform force-unlock a1b2c3d4-2024-01-15-160523
警告:
force-unlock是一把双刃剑。如果你在另一个 Terraform 进程还在跑的时候解锁,两个进程会同时写 state 文件,导致 state 损坏。只在你 100% 确认原进程已终止的情况下使用。我在某次生产事故中,CI/CD pipeline 超时后自动重试,结果两个 pipeline 同时拿到锁(DynamoDB 最终一致性导致),state 文件被写花,花了 4 小时从 S3 版本控制里回滚。
CI/CD 中的锁超时配置
在 CI/CD 环境中,建议配置锁的超时和重试策略:
# 设置锁超时为 5 分钟(默认无超时)
export TF_LOCK_TIMEOUT=5m
# 在 CI 脚本中自动清理残留锁
terraform plan -lock-timeout=5m
如果 CI/CD 中 Terraform 被超时杀掉,建议在 pipeline 的清理步骤里加上锁检查:
# .gitlab-ci.yml 的 after_script 或 GitHub Actions 的 post 步骤
after_script:
- |
# 检查是否有残留锁
LOCK_INFO=$(terraform show -json 2>/dev/null | grep -o 'LockID.*' || true)
if [ -n "$LOCK_INFO" ]; then
echo "Warning: 发现残留锁,正在清理..."
LOCK_ID=$(echo "$LOCK_INFO" | grep -oE '[a-f0-9-]{36}')
terraform force-unlock -force "$LOCK_ID"
fi
配置漂移:线上最常见的 Terraform 事故
什么是配置漂移
配置漂移(Configuration Drift)是指云平台上的真实资源状态和 Terraform state 文件记录的状态不一致。原因通常有三个:
- 手动修改:有人直接在控制台改了配置(比如加了一条安全组规则),没有走 Terraform
- 自动伸缩:K8s 的 Cluster Autoscaler 或者 HPA 创建了 Terraform 不知道的节点
- 第三方工具修改:其他自动化脚本或工具修改了资源
一个真实的配置漂移事故
某出行项目的支付网关凌晨告警,排查发现安全组规则被 Terraform 覆盖。时间线:
- 17:00 安全团队通过安全扫描发现支付网关的 443 端口对 0.0.0.0/0 开放,紧急在控制台添加了一条限制来源 IP 的规则
- 19:00 运维团队不知道安全团队的修改,执行了一次例行
terraform apply - Terraform 对比 state 文件后发现安全组规则和配置不一致,自动"纠正"——把安全团队手动添加的规则删了
- 19:05 支付网关被扫描器发现暴露面扩大,触发安全告警
这个事故的根因是缺少配置漂移检测机制。Terraform 是声明式工具,它默认认为"state 文件记录的就是正确的",任何偏差都是"错误"需要纠正。
检测配置漂移
定期运行 terraform plan 检测漂移,但不 apply:
# 在 CI/CD 中定期检测漂移(不执行变更)
terraform plan -detailed-exitcode
# exit code:
# 0 = 无变更(无漂移)
# 1 = 执行出错
# 2 = 有变更(检测到漂移)
在 CI/CD 中集成漂移检测:
# .github/workflows/drift-detection.yml
name: Terraform Drift Detection
on:
schedule:
- cron: '0 6 * * *' # 每天早上 6 点检测
jobs:
drift-detection:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
- name: Check Drift
run: |
terraform plan -detailed-exitcode -out=drift.tfplan || EXIT_CODE=$?
if [ "${EXIT_CODE:-0}" = "2" ]; then
echo "::warning::检测到配置漂移!"
terraform show -no-color drift.tfplan
# 发送告警到钉钉/飞书
curl -X POST "$DINGTALK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"Terraform 配置漂移告警:检测到未通过 IaC 管理的资源变更,请检查\"}}"
exit 1
fi
修复配置漂移的三种策略
| 策略 | 适用场景 | 操作方式 | 风险 |
|---|---|---|---|
| 重新导入 | 手动创建的资源需纳入管理 | terraform import | 低 |
| 更新代码 | 手动修改是正确的,代码需要跟上 | 修改 .tf 文件后 apply | 低 |
| 回滚变更 | 手动修改是错误的,需要恢复 | terraform apply 直接纠正 | 中 |
重新导入的完整示例:
# 先在代码中定义资源(空配置)
# resource "aws_security_group_rule" "https_ingress" {
# # 先留空,import 后补充
# }
# 执行导入
terraform import aws_security_group_rule.https_ingress sgr-0abcd1234efgh5678
# 导入后 terraform state show 查看实际配置
terraform state show aws_security_group_rule.https_ingress
# 根据实际配置补全 .tf 文件
# 然后运行 terraform plan 确认无差异
terraform plan
我的建议:对于安全组规则这类高频手动修改的资源,不要把所有规则都塞在一个 Terraform 模块里。按职责拆分——基础网络规则由 Terraform 管理,临时安全规则由独立模块或 Ansible 管理。这样安全团队手动加的规则不会因为 Terraform apply 被覆盖。在某电商平台的项目中,我们把安全组规则分成"基线规则"(Terraform 管理)和"动态规则"(Ansible 管理,独立 state),漂移事故从每月 2-3 次降到零。
状态文件拆分与迁移
为什么要拆分
随着基础设施规模增长,一个 state 文件管所有资源会遇到这些问题:
terraform plan越来越慢(state 文件太大,每次都要拉取全部资源状态)- 变更影响范围太大(改一个安全组,plan 里显示几百个资源的刷新)
- 权限控制粒度太粗(所有人都能看到所有环境的 state)
推荐按"环境 + 模块"拆分 state 文件:
# state 文件的组织结构
prod/
network/terraform.tfstate # VPC、子网、路由表
database/terraform.tfstate # RDS、ElastiCache
compute/terraform.tfstate # EC2、EKS、ALB
security/terraform.tfstate # 安全组、IAM、WAF
staging/
network/terraform.tfstate
database/terraform.tfstate
...
用 remote_state 跨模块引用
拆分后,模块之间需要引用彼此的输出。比如 compute 模块需要 network 模块的 VPC ID 和子网 ID:
# compute/main.tf — 从 network 模块的 state 文件中读取输出
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-company-terraform-state-prod"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
}
}
# 使用 network 模块的输出
resource "aws_instance" "app_server" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_a_id
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
tags = {
Name = "app-server-prod"
}
}
注意:
remote_state是只读的。compute 模块只能读取 network 模块的输出,不能修改 network 的资源。这种单向依赖关系避免了循环引用。
terraform state mv:迁移资源的正确姿势
把资源从一个 state 文件迁移到另一个,核心命令是 terraform state mv:
# 语法
terraform state mv [options] SOURCE DESTINATION
# 示例:把 EC2 实例从旧 state 迁移到新 state
terraform state mv -state-out=terraform.tfstate.backup \
aws_instance.web_server \
aws_instance.app_server
# 跨 state 文件迁移(从 network state 迁移到 compute state)
terraform state mv -state=prod/network/terraform.tfstate \
-state-out=prod/compute/terraform.tfstate \
aws_instance.web_server \
aws_instance.web_server
状态迁移的标准流程
下面是一个完整的状态迁移操作流程,我在实际项目中执行过多次:
# 步骤1:备份当前 state(必须!)
cp terraform.tfstate terraform.tfstate.backup.$(date +%Y%m%d%H%M%S)
# 步骤2:拉取最新的远程 state
terraform state pull > current_state.json
# 步骤3:查看要迁移的资源
terraform state list | grep web_server
# 步骤4:在目标模块中创建对应的资源定义(空壳)
# target_module/main.tf
# resource "aws_instance" "web_server" {
# # 先留空,迁移后再补充配置
# }
# 步骤5:执行迁移
terraform state mv \
-state=source_state/terraform.tfstate \
-state-out=target_state/terraform.tfstate \
aws_instance.web_server \
aws_instance.web_server
# 步骤6:验证迁移结果
terraform state list # 源 state 中应该没有这个资源了
cd target_module && terraform state list # 目标 state 中应该有这个资源
# 步骤7:在目标模块中补充资源配置
# 编辑 target_module/main.tf,补全 resource 块
# 步骤8:运行 plan 确认无差异
terraform plan # 应该显示 "No changes. Your infrastructure matches the configuration."
# 步骤9:推送两个 state 文件到远程后端
terraform state push source_state/terraform.tfstate
cd target_module && terraform state push target_state/terraform.tfstate
踩坑记录:
terraform state mv执行后,资源在源 state 中被删除,在目标 state 中被添加。如果中途出错,两个 state 文件都可能处于不一致状态。必须先备份,而且迁移操作不要在 CI/CD 中自动执行,手动操作更可控。我曾经在一次批量迁移中,脚本里的state mv命令拼错了资源地址,导致 30 个资源被移到了错误的地方。最后从 S3 版本控制回滚了 state 文件,但花了整个下午。
terraform state rm 和 import
除了 mv,还有两个常用的状态操作命令:
# terraform state rm:从 state 中移除资源(不删除云上资源)
# 适用场景:把资源从 Terraform 管理范围中移除,保留云上实际资源
terraform state rm aws_instance.old_server
# terraform import:把已存在的云资源导入到 state 中
# 适用场景:手动创建的资源需要纳入 Terraform 管理
terraform import aws_security_group.web_sg sg-0abcd1234
import 的麻烦之处在于它只导入 state,不生成代码。你需要手动写 .tf 文件,然后运行 terraform plan 看差异,反复调整直到 plan 显示 “No changes”。
对于大量存量资源的导入,推荐使用 Terraformer 工具(Google Cloud Platform 团队开发),它可以批量扫描云资源并自动生成 .tf 文件和 .tfstate 文件:
# 安装 Terraformer
wget https://github.com/GoogleCloudPlatform/terraformer/releases/download/v0.8.24/terraformer-all-linux-amd64
chmod +x terraformer-all-linux-amd64
mv terraformer-all-linux-amd64 /usr/local/bin/terraformer
# 批量导入 AWS 资源
terraformer import aws --regions=us-east-1 --resources=ec2,vpc,security-group
# 导入后会生成 generated/aws/ec2/ 目录,里面有 .tf 和 tfstate 文件
state 文件灾难恢复:从误删中救回
场景:state 文件被覆盖
某次运维操作中,团队成员在错误的目录执行了 terraform init,然后执行了 terraform apply,结果 Terraform 创建了一个全新的空 state 文件,覆盖了远程后端的旧 state。
这时候云上还有 200 多个资源在跑,但 Terraform 的 state 文件是空的。
恢复步骤
第一步:从 S3 版本控制恢复
# 查看历史版本
aws s3api list-object-versions \
--bucket my-company-terraform-state-prod \
--prefix prod/network/terraform.tfstate \
--query 'Versions[*].[VersionId,LastModified,IsLatest]' \
--output table
# 找到被覆盖前的版本,恢复它
aws s3api get-object \
--bucket my-company-terraform-state-prod \
--key prod/network/terraform.tfstate \
--version-id YOUR_VERSION_ID \
recovered_state.tfstate
# 验证恢复的 state 文件
terraform state pull > current_remote_state.json
diff recovered_state.tfstate current_remote_state.json
# 确认无误后推送回远程
terraform state push recovered_state.tfstate
第二步:验证 state 一致性
# 运行 plan,确认 state 和真实资源一致
terraform plan
# 如果 plan 显示大量资源需要"创建",说明 state 恢复成功但部分资源可能被手动修改
# 如果 plan 显示 "No changes",说明完全恢复
第三步:如果 S3 版本控制也被关了
极端情况下,S3 版本控制没有开启(是的,我遇到过),只能靠 terraform import 逐个资源重新导入。这时候 Terraformer 批量导入工具能救命:
# 扫描所有 AWS 资源并生成代码和 state
terraformer import aws --regions=us-east-1 --resources=ec2,vpc,rds,elb,security-group,s3,iam
# 检查生成的 state 文件
cat generated/aws/ec2/terraform.tfstate | python3 -m json.tool | head -50
# 手动合并到现有 state(逐个资源 mv)
for resource in $(terraformer plan | grep "will be created" | awk '{print $2}'); do
terraform state mv -stateout=main.tfstate generated_state $resource
done
教训:S3 桶的版本控制必须在创建时就开启。用 Terraform 管 S3 桶本身时,把版本控制作为强制的 baseline 配置。在某新能源物流平台的迁移项目中,我接手时发现客户的 S3 桶没有版本控制、没有加密、甚至没有公网访问屏蔽。我花了两天做了完整的后端加固,后面确实出了一次 state 误删事故,靠 S3 版本控制 5 分钟就恢复了。
state 文件的安全审计
state 文件中的敏感信息
state 文件以明文存储所有资源属性,包括:
- 数据库密码(如果用
password参数) - TLS 私钥
- IAM 访问密钥
- 安全组规则中的 IP 白名单
这些信息如果泄露,等于把生产环境的钥匙交给了攻击者。
安全加固措施
# 1. 敏感变量标记为 sensitive
variable "db_password" {
type = string
sensitive = true
}
# 2. 输出标记为 sensitive
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = true
}
# 3. 从 Secrets Manager 读取密码,不硬编码在 state 中
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/master-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# 这样 password 不会出现在 state 文件的 attributes 中
}
state 文件访问权限控制
resource "aws_iam_policy" "terraform_state_access" {
name = "terraform-state-access"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.terraform_state.arn}/prod/*"
},
{
Effect = "Allow"
Action = ["s3:ListBucket", "s3:GetBucketVersioning"]
Resource = aws_s3_bucket.terraform_state.arn
},
{
Effect = "Allow"
Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
Resource = aws_dynamodb_table.terraform_state_lock.arn
}
]
})
}
按环境隔离 IAM 权限:prod 环境的 state 只允许 prod 角色访问,staging 只允许 staging 角色访问。CI/CD 中使用不同的 IAM 角色,避免一个 pipeline 被攻破后所有环境的 state 都泄露。
多环境管理:Workspace vs 目录分离
Workspace 模式
Terraform 的 Workspace 机制可以在同一套代码中隔离不同环境的 state 文件:
# 创建 workspace
terraform workspace new prod
terraform workspace new staging
# 切换 workspace
terraform workspace select prod
# 在不同 workspace 中 apply,state 文件自动隔离
terraform apply
但 Workspace 有一个关键限制:它只隔离 state 文件,不隔离资源配置。如果你的 dev 和 prod 用的是同一套 .tf 文件,通过 terraform.workspace 变量区分,很容易写出这样的代码:
# 危险写法:dev 和 prod 用同一套代码,靠 workspace 区分
resource "aws_instance" "web" {
count = terraform.workspace == "prod" ? 3 : 1
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
}
一旦在错误的 workspace 里执行了 apply,后果不堪设想。
目录分离模式
我更推荐的方式是目录分离:每个环境有独立的代码目录和独立的 state 后端配置。
infrastructure/
modules/ # 共享模块
vpc/
rds/
eks/
environments/
prod/ # 生产环境
backend.tf # S3 key = prod/network/terraform.tfstate
main.tf
variables.tf
terraform.tfvars
staging/ # 预发环境
backend.tf # S3 key = staging/network/terraform.tfstate
main.tf
variables.tf
terraform.tfvars
dev/ # 开发环境
backend.tf # S3 key = dev/network/terraform.tfstate
main.tf
variables.tf
terraform.tfvars
| 对比项 | Workspace | 目录分离 |
|---|---|---|
| 代码复用 | 同一套代码 | 通过共享模块复用 |
| 配置隔离 | 弱(靠变量区分) | 强(完全独立) |
| 误操作风险 | 高(切错 workspace) | 低(目录隔离) |
| state 路径 | 自动隔离 | 手动配置 |
| 适用规模 | 小型项目 | 中大型项目 |
我的推荐:团队人数少于 3 人、环境少于 3 个的小项目用 Workspace 够了;但凡有多个团队协作、或者 prod/staging 配置差异较大的场景,一律用目录分离。在某出行项目的实践中,我们从 Workspace 迁移到目录分离后,误操作事故从每季度 1-2 次降到了零。代价是代码重复度增加,但通过共享模块可以把重复度控制在 20% 以内(相关文章:监控即代码:用 Terraform 和 YAML 管理告警规则 中也有类似的多环境目录管理思路)。
Terragrunt:大规模 Terraform 的编排工具
当 Terraform 项目规模进一步增长(几十个模块、多个环境、多个云账号),纯目录分离会带来大量重复的 backend.tf 和 provider.tf 配置。这时候 Terragrunt 能帮上忙。
Terragrunt 是一个 Terraform 的轻量包装器,核心解决两个问题:DRY(不要重复自己)和远程状态管理。
# terragrunt.hcl — 环境级别的公共配置
remote_state {
backend = "s3"
config = {
bucket = "my-company-terraform-state-${get_env("ENV")}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock-${get_env("ENV")}"
encrypt = true
}
}
# 公共 provider 配置
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Environment = "${get_env("ENV")}"
ManagedBy = "Terraform"
Owner = "SRE"
}
}
}
EOF
}
每个模块只需要一个简短的 terragrunt.hcl:
# prod/network/terragrunt.hcl
include {
path = find_in_parent_folders("env.hcl")
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
cidr_block = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
enable_nat_gateway = true
}
Terragrunt 的优势:
- 所有 backend 和 provider 配置只写一次
- 自动处理模块间的依赖顺序(
dependency块) - 支持
run-all批量执行多个模块 - 兼容所有 Terraform 命令
我的建议:模块数超过 20 个、或者需要管理多个云账号/多个环境时,引入 Terragrunt。在此之前,目录分离 + 共享模块足够了。别为了用工具而用工具——Terragrunt 本身也有学习成本,小项目引入它反而增加复杂度。
生产环境的最佳实践清单
state 管理的 10 条铁律
- 永远不要把 state 文件提交到 Git——加入
.gitignore,用远程后端管理 - 永远不要手动编辑 state 文件——用
terraform state命令操作 - 生产环境必须用远程后端——S3 + DynamoDB 是最低标准
- S3 桶必须开启版本控制——这是灾难恢复的最后一道防线
- DynamoDB 锁表的
hash_key必须叫LockID——注意大小写 - state 文件按环境+模块拆分——不要用一个 state 管所有资源
- CI/CD 中配置
TF_LOCK_TIMEOUT——避免锁卡死阻塞 pipeline - 定期运行漂移检测——每天至少一次
terraform plan -detailed-exitcode - 敏感信息不要写入 state——用 Secrets Manager 或 Vault
- 所有状态操作先备份——
terraform state pull备份到本地再操作
总结
Terraform state 管理不是"配个后端就完事"的简单话题。生产环境中的 state 文件是整个基础设施管理的根基,一旦出问题,影响范围是全局性的。
回顾几个关键点:
后端选型:S3 + DynamoDB 是 AWS 环境的标配。S3 负责持久存储和版本控制,DynamoDB 负责分布式锁。两者缺一不可——没有锁的 S3 后端在多人协作时会导致 state 覆盖,没有版本控制的 S3 在 state 误删时无法恢复。
状态锁:force-unlock 只在确认原进程已退出后使用。CI/CD 中配置 TF_LOCK_TIMEOUT 避免锁卡死阻塞 pipeline。DynamoDB 锁表的 hash_key 必须叫 LockID。
配置漂移:定期运行 terraform plan -detailed-exitcode 检测漂移。对于安全组等高频手动修改的资源,拆分到独立模块或独立工具管理,避免 Terraform apply 覆盖手动修改。
状态迁移:terraform state mv 前必须备份。迁移操作不要在 CI/CD 中自动执行。大规模存量资源导入用 Terraformer 批量处理。
灾难恢复:S3 版本控制是最后一道防线。如果连版本控制都没开,只能靠 terraform import 逐个恢复。把 S3 桶的版本控制作为强制的 baseline 配置,用 prevent_destroy = true 防止误删。
多环境管理:小型项目用 Workspace,中大型项目用目录分离。模块数超过 20 个时引入 Terragrunt。
最后一条经验:把 state 文件当成生产数据库来对待——定期备份、控制访问权限、监控异常变更。我在多个项目中推行的做法是:state 后端的基础设施(S3 桶、DynamoDB 表、KMS 密钥)用独立的 Terraform 模块管理,这个模块的 state 又存在另一个 S3 桶里。也就是说"管 state 的 state"也需要被管理。听起来套娃,但这确实是生产环境中最可靠的做法。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Terraform 官方文档 - State — HashiCorp,Terraform state 的官方概念说明和最佳实践
- Terraform 官方文档 - Backends — HashiCorp,S3 后端的配置参数和锁机制说明
- AWS Prescriptive Guidance - Terraform Backend Best Practices — AWS,S3 + DynamoDB 后端的生产级安全配置指南
- Terraform Workspaces — HashiCorp,Workspace 机制的使用场景和限制说明
- Terragrunt — Gruntwork,Terragrunt 工具的官方文档和 DRY 实践指南
- Terraformer — Google Cloud Platform,批量导入存量云资源的工具仓库