4. 远程状态与状态锁定

Terraform 的本地状态文件 terraform.tfstate 记录了资源 ID、属性、依赖关系与元数据,是 Terraform 判断“现实世界是否与配置一致”的唯一依据。一旦多人协作、CI/CD 流水线自动执行,本地状态就会立刻暴露三个致命问题:状态文件只存在于某台机器上、并发执行会互相覆盖、状态中可能包含数据库密码等敏感明文。远程状态(Remote State)与状态锁定(State Locking)正是为解决这三类问题而设计的工程机制。

一、后端选型:状态存到哪里

Terraform 通过 backend 块声明状态存储位置。常见后端包括 local、s3、azurerm、gcs、consul、terraform cloud 等。在 AWS 技术栈中,最主流的是 S3 后端:S3 提供高持久性对象存储、版本控制与服务器端加密,配合 DynamoDB 实现锁定。选型时需关注四个维度:是否支持锁定、是否支持版本回滚、是否支持加密、是否支持工作区隔离。

后端锁定机制版本/回滚加密适用场景
local无手动备份无单人学习、临时验证
s3 + dynamodbDynamoDB 条件写入S3 版本控制SSE-S3/KMSAWS 生产环境、CI/CD
azurermBlob LeaseBlob 版本服务端加密Azure 环境
gcs对象元数据对象版本Google 托管密钥GCP 环境
Terraform Cloud内置内置内置托管式协作

二、S3 后端与状态文件存储

一个生产可用的 S3 后端配置通常包含以下字段:

terraform {
  backend "s3" {
    bucket         = "acme-tfstate-prod"
    key            = "network/vpc/terraform.tfstate"
    region         = "ap-southeast-1"
    dynamodb_table = "acme-tf-lock"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:ap-southeast-1:123456789012:key/abcd-..."
  }
}

其中 bucket 与 key 共同决定状态文件的唯一路径;encrypt = true 启用 SSE,配合 kms_key_id 使用客户托管密钥,可满足合规审计要求。建议同时开启 S3 版本控制,这样每次 terraform apply 写入新状态时都会保留历史版本,一旦误操作可通过指定版本号恢复。状态文件本身不应被手工编辑,任何修改都应通过 Terraform 命令完成。

三、DynamoDB 锁:并发控制的实现原理

DynamoDB 锁表需要一张主键为 LockID(字符串)的表。Terraform 在执行 plan、apply 等写操作前,会向该表写入一条记录,主键值等于状态文件路径(如 acme-tfstate-prod/network/vpc/terraform.tfstate)。写入使用条件表达式 attribute_not_exists(LockID),若记录已存在则写入失败,Terraform 报错并终止,从而保证同一时刻只有一个执行者能修改状态。

锁记录包含 Info 字段,记录执行者、操作类型与时间戳,便于排查“谁锁住了状态”。当操作完成或失败时,Terraform 会删除该记录释放锁。若流水线被强制中断导致锁未释放,可使用 terraform force-unlock <LOCK_ID> 手动解锁,但必须先确认没有其他进程正在运行,否则可能造成状态损坏。

S3 后端 + DynamoDB 锁:并发执行控制 执行者 A CI 流水线 / 工程师 执行者 B 并发 apply 请求 DynamoDB 锁表 LockID = 状态路径 条件:attribute_not_exists Info: 执行者 / 操作 / 时间 S3 状态桶 terraform.tfstate 版本控制 + SSE-KMS 历史版本可回滚 ① 请求加锁 ✓ 写入成功 ② 写入状态 ① 请求加锁 ✗ 条件失败 Error: state locked 终止执行,不触碰状态 ③ 完成后释放锁 同一状态路径同一时刻仅允许一个执行者持有锁;锁记录写入失败即拒绝并发操作

四、状态隔离:按环境与模块拆分

状态隔离的核心原则是“爆炸半径最小化”。如果把 VPC、数据库、应用全部放在同一个状态文件里,一次 apply 失败可能影响所有资源,且 plan 需要刷新全部资源,速度极慢。推荐按“环境 × 层”拆分:

  • 环境维度:dev、staging、prod 各自独立状态,互不影响。
  • 层级维度:网络层(VPC/子网)、数据层(RDS/Redis)、应用层(ECS/Lambda)分别独立状态。
  • 路径约定:env/层/terraform.tfstate,例如 prod/network/terraform.tfstate、prod/app/terraform.tfstate。

层与层之间通过 terraform_remote_state 数据源引用输出值,而不是直接读取对方状态文件。例如应用层需要网络层的 VPC ID:

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-tfstate-prod"
    key    = "prod/network/terraform.tfstate"
    region = "ap-southeast-1"
  }
}

resource "aws_ecs_service" "app" {
  network_configuration {
    subnets = data.terraform_remote_state.network.outputs.private_subnet_ids
  }
}

这种方式的优点是依赖显式、可审计;缺点是远程状态数据源会读取对方全部输出,若对方输出敏感值需谨慎。更严格的做法是使用 SSM Parameter Store 或 Consul 传递少量关键值。

五、跨环境状态管理实践

跨环境管理有两种主流模式:目录隔离与工作区隔离。目录隔离为每个环境建立独立目录,各自拥有 backend 配置与变量文件,隔离彻底、易于理解,是生产环境首选。工作区隔离使用 terraform workspace,同一份配置通过 terraform.workspace 变量区分环境,适合差异极小的场景,但容易因切换工作区误操作,且所有环境共享同一后端配置。

建议:生产环境使用目录隔离,每个环境一个独立状态路径;开发环境可酌情使用工作区以节省配置量。无论哪种方式,都应为每个状态路径配置独立的 DynamoDB 锁记录(同一张锁表即可,主键不同)。
常见陷阱:terraform force-unlock 只能在确认没有其他进程运行时使用;若在 CI 中并发触发流水线,应通过流水线层面的并发控制(如 GitHub Actions 的 concurrency、GitLab 的 resource_group)与 DynamoDB 锁双重防护,避免锁被误释放。

最后,状态文件迁移是常见操作。当需要把本地状态迁移到 S3 时,先配置好 backend 块,执行 terraform init -migrate-state,Terraform 会提示是否将本地状态复制到远程,确认后本地状态被上传并保留备份。迁移完成后,务必验证 terraform plan 输出为 “No changes”,以确认状态与真实资源一致。