上周五下班前,我们团队上演了一出惊心动魄的“恐怖故事”。两个运维开发在没有充分沟通的情况下,同时在各自的本地终端里执行了 terraform apply。因为当时我们直接把 terraform.tfstate 存放在一个共享的 S3 桶里,却没有配置任何状态锁机制。结果两个进程并发读写同一个状态文件,导致 S3 中的 State 直接混乱。更惨的是,其中一个 apply 判定某台线上关键数据库实例“已不存在”,直接触发了销毁重建!虽然最后通过历史备份抢救回来了,但这绝对是运维生涯里不可承受的低级失误。

痛点分析:为什么只用 S3 存 State 还不够?

许多人刚接触 Terraform 时,只知道把状态文件放到远端 S3 存储桶里,觉得这样大家就能共享最新的配置了。但 S3 本质上只是一个对象存储,它不具备强一致性的分布式锁功能。

如果多个人同时执行写操作:

  1. A 和 B 同时拉取了当前的 State 缓存;
  2. A 开始创建资源,B 开始修改资源;
  3. A 先执行完并写入 S3,随后 B 执行完覆盖了 S3;
  4. 这时候,A 刚刚创建的资源在最新的 State 里彻底“丢失”了,Terraform 失去了对这些物理资源的追踪,变成无主孤魂。

为了解决这个问题,Terraform 提供了 Backend 锁机制。对于 AWS 环境,标准方案就是 S3(存状态) + DynamoDB(管状态锁)

第一步:用 Terraform 创建 S3 桶和 DynamoDB 表

我们先用一段基础的 Terraform 代码来初始化这些安全基础设施。注意,DynamoDB 的主键(Hash Key)必须命名为 LockID,类型为 S(String),这是 Terraform 的硬性规定,不能自定义。

# backend-infra.tf
provider "aws" {
  region = "us-east-1"
}

# 存放 tfstate 的安全 S3 桶
resource "aws_s3_bucket" "terraform_state" {
  bucket        = "company-infra-tfstate-prod"
  force_destroy = false
}

# 开启 S3 版本控制,万一 State 坏了还能回滚
resource "aws_s3_bucket_versioning" "enabled" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

# 开启服务端加密,保护敏感数据
resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# 阻断所有公网访问,防止 State 泄露
resource "aws_s3_bucket_public_access_block" "public_access" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 创建 DynamoDB 锁表
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "company-infra-tflocks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

第二步:配置项目的 Backend 引用

当上面的基础设施创建完毕后,我们就可以在实际的基础设施项目代码中声明这个远程 backend 了。

# backend.tf
terraform {
  required_version = ">= 1.5.0"

  backend "s3" {
    bucket         = "company-infra-tfstate-prod"
    key            = "global/s3/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "company-infra-tflocks"
    encrypt        = true
  }
}

配置好之后,我们在项目根目录下运行 terraform init。Terraform 会检测到 Backend 变更,并提示你将本地的 State 文件搬迁到 S3 桶里。

强锁机制的工作流原理

配置完成之后,我们再次尝试并发操作。

当 A 运行 terraform planterraform apply 时,Terraform 会自动往 DynamoDB 的 company-infra-tflocks 表里插入一条主键为 global/s3/terraform.tfstate-md5 的锁记录。

如果此时 B 也在运行 apply,Terraform 在修改前会去查表,一旦发现锁已存在,就会直接报错并退出:

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        8eb52b57-a3f1-d007-4228-example
  Path:      company-infra-tfstate-prod/global/s3/terraform.tfstate
  Operation: OperationTypeApply
  Who:       deploy-user@bastion-01
  Version:   1.5.7
  Created:   2026-07-13 07:30:00Z
  Info:      

这样就完美避免了多人并发修改 State 的隐患。

突发情况:锁死了解不开怎么办?

有时候,如果网络突然中断,或者 CI/CD 容器在运行 apply 时被强行杀掉,可能会导致锁没有被正常释放。这时候其他人再执行就会一直报 Error acquiring the state lock

此时,我们需要获取报错信息里的 ID(例如上面的 8eb52b57-a3f1-d007-4228-example),在确认没有其他人正在部署的情况下,使用下面的命令强制解锁:

terraform force-unlock 8eb52b57-a3f1-d007-4228-example

切记!这个命令非常危险,必须配合团队沟通使用,否则一旦强行解锁时真的有人在做 apply,State 依然会损毁。

你们团队目前在多人协同这块,是用的自建 S3 Backend 锁方案,还是干脆上了 Terraform Cloud / Enterprise 这种 SaaS 平台?