4. S3后端深度配置实战

把 Terraform 状态从本地搬到 S3,并不是“建个桶、改个 backend 块”就结束了。状态文件是基础设施的唯一事实来源,一旦被并发写坏、被误删或被越权读取,恢复成本极高。本节围绕五个关键配置面展开:S3 桶与版本控制、DynamoDB 锁表、IAM 权限最小化、服务端加密与 KMS 集成、访问日志审计。它们共同构成一个可协作、可追溯、可恢复的远程状态后端。

一、S3 桶创建与版本控制

状态桶应独立于业务资源,单独创建、单独管理,避免“用 Terraform 管理存放 Terraform 状态的桶”这种自引用困境。推荐做法是先用手工或独立的 bootstrap 配置创建状态桶,再让业务配置引用它。桶名需全局唯一,通常带上组织、环境、用途后缀,例如 acme-tfstate-prod-cn。

创建时至少要做三件事:开启版本控制、开启服务端加密、阻止公共访问。版本控制是状态恢复的底线——每次 terraform apply 都会覆盖状态对象,只有保留历史版本,才能在状态损坏时回滚到上一个可用版本。阻止公共访问则通过 BlockPublicAcls、BlockPublicPolicy、IgnorePublicAcls、RestrictPublicBuckets 四项全部置真来兜底。

resource "aws_s3_bucket" "tfstate" {
  bucket = "acme-tfstate-prod-cn"
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "tfstate" {
  bucket                  = aws_s3_bucket.tfstate.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

版本控制开启后,建议再配一条生命周期规则,把非当前版本在 90 天后转为 Glacier 或直接过期删除,既控制成本又保留足够的回滚窗口。注意:生命周期删除非当前版本是不可逆的,窗口期要结合团队的故障恢复预期设定。

二、DynamoDB 锁表设计

S3 本身不提供并发写保护,两个工程师同时 apply 会互相覆盖状态。Terraform 通过 DynamoDB 实现状态锁:apply 前写入一条以状态文件路径为键的锁记录,完成后删除。锁表结构是固定的,主键必须叫 LockID,类型为字符串。

resource "aws_dynamodb_table" "tf_lock" {
  name         = "acme-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

锁表用按需计费即可,锁操作频率极低,几乎不产生费用。锁表与状态桶应放在同一区域,减少跨区延迟。若锁记录因进程崩溃残留,terraform apply 会报 “Error acquiring the state lock” 并附带 LockID,此时确认无人操作后可用 terraform force-unlock <LOCK_ID> 强制解锁——这是危险操作,必须确认持有者确实已停止。

三、IAM 权限最小化

状态后端的 IAM 策略应精确到“这个桶的这个前缀 + 这张锁表”,而不是 s3:* 或 dynamodb:*。Terraform 对状态桶实际需要的动作包括:s3:GetObject、s3:PutObject、s3:DeleteObject、s3:ListBucket,以及读取版本所需的 s3:GetBucketVersioning;对锁表需要 dynamodb:GetItem、PutItem、DeleteItem。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketVersioning"],
      "Resource": "arn:aws:s3:::acme-tfstate-prod-cn"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::acme-tfstate-prod-cn/env/prod/*"
    },
    {
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],
      "Resource": "arn:aws:dynamodb:cn-north-1:123456789012:table/acme-tfstate-lock"
    }
  ]
}

关键点在于把对象级权限限制到具体前缀(如 env/prod/*),这样不同环境可以共用桶但互不越权。生产环境的写权限应只授予 CI/CD 角色,人工账号默认只读,从流程上降低误操作概率。

四、服务端加密与 KMS 集成

S3 默认的 SSE-S3 加密已能满足合规底线,但状态文件常含数据库密码、密钥等敏感值,建议升级为 SSE-KMS,使用客户自管密钥(CMK),从而获得独立的密钥策略、轮换能力和 CloudTrail 解密审计。

resource "aws_kms_key" "tfstate" {
  description             = "KMS key for terraform state"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.tfstate.arn
    }
    bucket_key_enabled = true
  }
}

bucket_key_enabled = true 能显著减少对 KMS 的调用次数、降低成本。使用 CMK 后,读取状态的 IAM 主体还必须被密钥策略授予 kms:Decrypt 和 kms:GenerateDataKey,否则会出现“能列桶却读不了对象”的权限断层,这是实战中最常见的坑之一。

五、状态文件访问日志审计

谁在什么时候读了状态、谁改了状态,必须可追溯。开启 S3 服务器访问日志或 CloudTrail 数据事件,把对象级读写记录投递到独立的日志桶。日志桶同样要阻止公共访问,并设置生命周期归档。

resource "aws_s3_bucket_logging" "tfstate" {
  bucket        = aws_s3_bucket.tfstate.id
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "tfstate-access/"
}

审计时重点关注三类事件:非预期主体的 GetObject(可能泄露敏感值)、DeleteObject(状态被删)、以及锁表的 PutItem 冲突。结合 CloudTrail 的 userIdentity 字段,可以定位到具体角色与会话。

Terraform S3 后端协作治理全景 Terraform 客户端 terraform apply CI/CD 角色 / 人工 IAM 最小权限 s3:Get/Put/DeleteObject dynamodb:Get/Put/DeleteItem kms:Decrypt / GenerateDataKey DynamoDB 锁表 主键 LockID (String) PAY_PER_REQUEST S3 状态桶 版本控制 Enabled 阻止公共访问 ×4 SSE-KMS + Bucket Key env/prod/terraform.tfstate 生命周期归档非当前版本 KMS CMK 密钥轮换开启 密钥策略授权解密 访问日志桶 S3 访问日志 CloudTrail 数据事件 加锁/解锁 读写状态 加密/解密 投递日志 鉴权 协作闭环:客户端先向锁表申请锁 → 通过 IAM 鉴权 → 读写经 KMS 加密的状态对象 → 访问行为写入日志桶 → 完成后释放锁。 任一环节缺失都会导致并发覆盖、越权读取或无法追溯,五者需同时配置才构成完整治理。

六、配置落地与验证

把上述资源就绪后,在业务配置的 backend 块中引用它们:

terraform {
  backend "s3" {
    bucket         = "acme-tfstate-prod-cn"
    key            = "env/prod/terraform.tfstate"
    region         = "cn-north-1"
    dynamodb_table = "acme-tfstate-lock"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:cn-north-1:123456789012:key/xxxx"
  }
}

执行 terraform init 后,Terraform 会提示 “Successfully configured the backend 's3'”。验证是否真正生效,可依次检查:aws s3api get-bucket-versioning 返回 Enabled;aws s3api get-bucket-encryption 显示 aws:kms;在另一终端同时执行 apply,第二个进程应报锁冲突;用只读角色执行 apply 应被拒绝;日志桶中能看到对应的 GetObject 记录。五项全部通过,才算完成本节的后端深度配置。

状态桶、锁表、KMS 密钥属于“基础设施的基础设施”,建议用独立的 bootstrap 目录管理,并限制其变更权限,避免业务配置误删后端本身。