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 字段,可以定位到具体角色与会话。
六、配置落地与验证
把上述资源就绪后,在业务配置的 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 记录。五项全部通过,才算完成本节的后端深度配置。