4. 编写第一条合规策略
在完成策略即代码(Policy as Code)的基本认知之后,本节要真正落地第一条可执行的合规策略。核心目标是:让策略引擎读取 Terraform plan 的 JSON 表示,对其中资源属性做校验,并在本地完成一次可复现的测试。策略不是写在文档里的规范,而是能在流水线中阻断不合规变更的可执行代码。
一、先理解策略校验的输入对象
Terraform 在 plan 阶段会生成一份执行计划,其中包含将要创建、修改或销毁的资源及其属性。策略引擎并不直接解析 HCL,而是消费 terraform show -json plan.out 输出的结构化 JSON。因此第一步是把计划固化为可被策略读取的文件:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
这份 tfplan.json 中,planned_values.root_module.resources 列出计划中的资源,resource_changes 则描述每个资源的变化动作(create、update、delete、no-op)。策略校验通常关注 resource_changes,因为它能区分“新增”和“修改”,从而对不同类型的变更施加不同约束。
二、选择 Sentinel 还是 OPA
两者都能完成属性校验,但定位不同。Sentinel 是 HashiCorp 官方策略语言,与 Terraform Cloud/Enterprise 集成紧密,语法偏声明式,适合在 HCP 体系内做准入控制。OPA(Open Policy Agent)使用 Rego 语言,通用性强,可同时约束 Kubernetes、Terraform、CI 配置等多种输入,适合多云、多工具统一治理。若团队已使用 Terraform Cloud,优先 Sentinel;若希望策略跨工具复用,优先 OPA。本节以 OPA/Rego 为例,因为它在本地即可完成测试,不依赖托管平台。
三、编写第一条 Rego 策略
假设我们要校验:所有计划创建的 S3 存储桶必须开启服务端加密,且禁止使用公共读 ACL。策略文件 policy/s3_encryption.rego 如下:
package terraform.s3
import future.keywords.in
# 收集计划中所有将被创建的 aws_s3_bucket 资源
created_buckets[resource] {
some resource in input.resource_changes
resource.type == "aws_s3_bucket"
resource.change.actions[_] == "create"
}
# 违规:未配置服务端加密
deny[msg] {
some resource in created_buckets
not resource.change.after.server_side_encryption_configuration
msg := sprintf("S3 存储桶 %s 未开启服务端加密", [resource.address])
}
# 违规:ACL 为 public-read 或 public-read-write
deny[msg] {
some resource in created_buckets
resource.change.after.acl in ["public-read", "public-read-write"]
msg := sprintf("S3 存储桶 %s 使用了公共 ACL: %s", [resource.address, resource.change.after.acl])
}
这段策略的关键点有三处:其一,input 就是 tfplan.json 解析后的对象,resource_changes 是入口;其二,change.after 表示变更后的属性值,校验应基于它而非 before;其三,deny 是 OPA 的约定规则名,返回非空集合即表示策略不通过。若使用 Sentinel,等价逻辑会写成 rule 加 main 判断,并通过 tfplan/v2 导入计划数据。
四、本地测试:用 conftest 驱动
OPA 本身提供 opa eval,但更贴近工程实践的是 conftest,它能直接对 JSON 文件执行策略并输出可读结果:
conftest test tfplan.json --policy policy/
若计划中某个 S3 桶缺少加密配置,输出会类似:
FAIL - tfplan.json - terraform.s3 - S3 存储桶 aws_s3_bucket.logs 未开启服务端加密
1 test, 0 passed, 0 warnings, 1 failure
看到 FAIL 说明策略生效且成功拦截了不合规变更;若输出 PASS,则说明当前计划满足全部约束。这一步是本地策略测试的核心:用真实 plan 文件驱动策略,观察 deny 是否被触发。
五、用单元测试固化策略行为
仅靠一次手工测试不够,策略也需要回归测试。OPA 支持为 Rego 编写 _test.rego 单元测试,用构造的输入验证规则:
package terraform.s3_test
import data.terraform.s3
test_bucket_without_encryption_denied {
count(s3.deny) > 0 with input as {
"resource_changes": [{
"address": "aws_s3_bucket.demo",
"type": "aws_s3_bucket",
"change": {
"actions": ["create"],
"after": {"acl": "private"}
}
}]
}
}
test_bucket_with_encryption_allowed {
count(s3.deny) == 0 with input as {
"resource_changes": [{
"address": "aws_s3_bucket.demo",
"type": "aws_s3_bucket",
"change": {
"actions": ["create"],
"after": {
"acl": "private",
"server_side_encryption_configuration": [{
"rule": [{"apply_server_side_encryption_by_default": [{"sse_algorithm": "AES256"}]}]
}]
}
}
}]
}
}
执行 opa test policy/ -v,两个用例都应通过。这样,策略的“拦截”和“放行”两条路径都被固定下来,后续修改策略时不会误伤合规资源。
六、结果判读与常见问题
本地测试时,最需要观察的是 deny 消息是否精确指向违规资源地址。若策略未触发,常见原因有三类:一是 actions 判断写成了 ["create"] 精确匹配,而实际计划中可能是 ["create", "delete"] 组合,应改用 actions[_] == "create";二是属性路径写错,例如加密配置在 plan JSON 中可能嵌套在 after.server_side_encryption_configuration 数组内,需要逐层确认;三是策略包名与 conftest 的命名空间不匹配,导致规则未被加载。排查时可用 opa eval -d policy/ -i tfplan.json "data.terraform.s3.deny" 直接查看 deny 集合内容。
conftest test 与 opa test 都纳入本地开发习惯:前者验证真实 plan,后者验证策略逻辑边界。两者结合,才能在进入 CI 之前就发现策略缺陷。七、从本地测试走向流水线
本地测试通过后,这条策略就可以原样接入 CI:在 terraform plan 之后、terraform apply 之前插入 conftest test tfplan.json --policy policy/,失败即中断流水线。此时策略文件、测试文件和 plan 文件构成一个最小可复现单元,后续新增策略只需扩展 policy/ 目录并补充对应 _test.rego。第一条策略的价值不在于规则本身多复杂,而在于它打通了“计划 → JSON → 策略 → 判定 → 测试”的完整链路,为后续所有合规治理提供了可复制的工程范式。