概述

凌晨两点,手机炸了。某出行项目的运维群弹出一连串告警: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"
          ]
        }
      ]
    }
  ]
}

几个关键字段:

字段含义为什么重要
versionstate 文件格式版本Terraform 升级时自动迁移,但你得知道当前版本
serial每次 apply 自增的序号用于检测并发写入冲突,serial 不匹配会报错
lineagestate 文件的唯一标识 ID两个不同 lineage 的 state 合并会直接拒绝
resources资源列表核心数据,每个资源包含 ID、属性、依赖
dependencies资源依赖关系决定了创建和销毁的顺序

三层数据对比机制

Terraform 的 plan 命令不是简单读一下 state 文件。它同时对比三组数据源:

  1. 本地配置文件.tf 文件):你期望的基础设施状态
  2. state 文件.tfstate):Terraform 认为的上一次部署状态
  3. 云平台真实状态(通过 API 实时拉取):线上实际跑着什么

三层对比后,Terraform 算出差异:哪些资源需要新建、哪些需要更新、哪些需要销毁。

这个机制的关键在于——state 文件是 Terraform 连接"代码"和"真实云资源"的唯一桥梁。如果 state 文件丢了或者和真实状态不一致,Terraform 就"瞎"了,要么重复创建资源,要么误删资源。

这也是为什么 state 文件不能提交到 Git 仓库。state 文件包含敏感信息(资源 ID、IP 地址、密码等),而且多人同时修改会产生冲突。正确做法是放在远程后端统一管理。

远程后端选型:S3 + DynamoDB 为什么是标配

本地存储的三大坑

默认情况下,Terraform 把 state 文件存在本地工作目录的 terraform.tfstate。这在个人玩具项目里没问题,生产环境里会踩三个坑:

  1. 多人协作冲突:A 和 B 同时改基础设施,各自的 terraform.tfstate 不一致,谁后 apply 谁覆盖谁
  2. state 文件丢失:本地磁盘损坏或者容器被销毁,state 文件没了,所有资源变成"孤儿"——云上还跑着,但 Terraform 不知道它们存在
  3. 安全隐患: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_idlockId,结果 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
}

这套配置的防护层次:

  1. prevent_destroy = true 防止 Terraform 误删 S3 桶
  2. 版本控制保留所有历史版本,随时可回滚
  3. KMS 加密防止数据泄露
  4. 公网访问全屏蔽,只能通过 IAM 认证访问
  5. 生命周期策略控制成本,旧版本自动降级到 Glacier

状态锁:防止多人同时 apply 的核心机制

锁的工作原理

状态锁的原理很直白:执行 terraform applyterraform 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 文件记录的状态不一致。原因通常有三个:

  1. 手动修改:有人直接在控制台改了配置(比如加了一条安全组规则),没有走 Terraform
  2. 自动伸缩:K8s 的 Cluster Autoscaler 或者 HPA 创建了 Terraform 不知道的节点
  3. 第三方工具修改:其他自动化脚本或工具修改了资源

一个真实的配置漂移事故

某出行项目的支付网关凌晨告警,排查发现安全组规则被 Terraform 覆盖。时间线:

  1. 17:00 安全团队通过安全扫描发现支付网关的 443 端口对 0.0.0.0/0 开放,紧急在控制台添加了一条限制来源 IP 的规则
  2. 19:00 运维团队不知道安全团队的修改,执行了一次例行 terraform apply
  3. Terraform 对比 state 文件后发现安全组规则和配置不一致,自动"纠正"——把安全团队手动添加的规则删了
  4. 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.tfprovider.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 条铁律

  1. 永远不要把 state 文件提交到 Git——加入 .gitignore,用远程后端管理
  2. 永远不要手动编辑 state 文件——用 terraform state 命令操作
  3. 生产环境必须用远程后端——S3 + DynamoDB 是最低标准
  4. S3 桶必须开启版本控制——这是灾难恢复的最后一道防线
  5. DynamoDB 锁表的 hash_key 必须叫 LockID——注意大小写
  6. state 文件按环境+模块拆分——不要用一个 state 管所有资源
  7. CI/CD 中配置 TF_LOCK_TIMEOUT——避免锁卡死阻塞 pipeline
  8. 定期运行漂移检测——每天至少一次 terraform plan -detailed-exitcode
  9. 敏感信息不要写入 state——用 Secrets Manager 或 Vault
  10. 所有状态操作先备份——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"也需要被管理。听起来套娃,但这确实是生产环境中最可靠的做法。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Terraform 官方文档 - State — HashiCorp,Terraform state 的官方概念说明和最佳实践
  2. Terraform 官方文档 - Backends — HashiCorp,S3 后端的配置参数和锁机制说明
  3. AWS Prescriptive Guidance - Terraform Backend Best Practices — AWS,S3 + DynamoDB 后端的生产级安全配置指南
  4. Terraform Workspaces — HashiCorp,Workspace 机制的使用场景和限制说明
  5. Terragrunt — Gruntwork,Terragrunt 工具的官方文档和 DRY 实践指南
  6. Terraformer — Google Cloud Platform,批量导入存量云资源的工具仓库