外观
AI 生成 Terraform 上线前:OPA 策略审查实测
核心结论先行
AI 生成的 Terraform 代码可以直接用,但绝不能直接 apply。我的底线做法是:在 GitLab CI 里对 terraform plan 的二进制结果做 OPA/Rego 策略审查,让"删生产库、安全组全开、绕过标签规范"这类变更在合并前就红灯失败。这套策略即代码(Policy as Code)工作流,是我目前在 AI 代码治理上性价比最高的一道闸门。
下面把整套流程、可直接跑的 Rego 策略和踩过的坑一次讲透。
1
AI 生成代码
Copilot/Cursor 产出 .tf 与 MR
2
terraform plan
生成二进制 plan 并转为 JSON
3
OPA eval
Rego 策略遍历资源变更
4
门禁裁决
deny 即失败,阻断高危合并
为什么不能只信 AI 和 terraform validate
用 Copilot 或 Cursor 生成基础设施代码,效率确实高:一段"给我一个带公网访问的 RDS"的注释,它能秒回一份结构完整的 .tf。问题在于,AI 优化的是"看起来合理",而不是"在你这套环境里安全"。我见过它顺手写上 0.0.0.0/0 全网开放、把实例规格抬一档、甚至在重构时把某个仍在用的资源从 state 里"优化"掉。
terraform validate 和 tflint 解决不了这件事——它们只检查语法和 provider 约束,根本不知道这次变更会创建、修改还是删除什么。真正承载语义的是 terraform plan 的输出:哪个资源动作是 delete,before 和 after 差在哪。这正是 OPA(Open Policy Agent)+ Rego 该介入的位置。
方案对比:为什么选 OPA 而不是纯脚本或纯扫描器
我在选型时认真比较过三条路:
- checkov / tfsec 这类静态扫描:规则库全、上手快,但它们扫的是源码文本,对"这次 plan 到底删没删生产资源"这种动态语义支持弱,且自定义复杂规则时代价不低。
- Bash + grep plan 文本:能临时顶一下,但 plan 的人类可读输出随版本变化,grep 极脆,规则一多就变成没人敢改的屎山。
- OPA/Rego 审查 plan JSON:直接吃
terraform show -json plan.bin的结构化数据,规则可单元测试、可版本化、可在多个仓库复用。代价是 Rego 有学习曲线。
我的取舍是:checkov 留作基线静态扫描,OPA 专注 plan 语义层,两者不冲突。Rego 那点学习成本,换来的是规则可读、可测、可评审,这笔账很划算。
1
格式与静态层
terraform fmt -checktflintcheckov 基线
2
计划语义层
OPA 审查 plan JSON识别 create/update/delete比对 before/after
3
人工裁决层
安全组 0.0.0.0/0生产库删除成本突增
实操:从 plan 二进制到 Rego 门禁
第一步:导出结构化 plan
关键点是必须用 -out 保存二进制计划,再转 JSON。这样能保证 OPA 审查的和最终 apply 的是同一个计划,防止"审查一版、apply 另一版"。
bash
# 初始化并生成二进制计划(CI 中需注入对应的云厂商凭证)
terraform init -input=false
terraform plan -input=false -out=tfplan.bn
# 转成 OPA 能读取的结构化 JSON
terraform show -json tfplan.bn > tfplan.json第二步:写第一条 Rego 策略
新建 policy/terraform.rego。下面这条规则拦截两件最要命的事:删除任何数据库实例,以及把安全组向全网开放。
rego
package terraform.plan
import future.keywords.if
# 工具函数:从资源地址里判断是否属于"受保护"资源
is_protected(addr) if {
contains(addr, "aws_db_instance")
}
# 规则一:禁止删除受保护资源(生产库)
deny[msg] if {
some rc
rc := input.resource_changes[_]
is_protected(rc.address)
rc.change.actions[_] == "delete"
msg := sprintf("高危操作:试图删除受保护资源 %v,已被策略拦截", [rc.address])
}
# 规则二:禁止安全组对 0.0.0.0/0 全网开放
deny[msg] if {
some rc
rc := input.resource_changes[_]
rc.type == "aws_security_group_rule"
cidrs := rc.change.after.cidr_blocks
some cidr
cidrs[cidr]
cidr == "0.0.0.0/0"
msg := sprintf("安全组 %v 不允许对全网开放,请收敛来源网段", [rc.address])
}resource_changes 是 plan JSON 的核心数组,每个元素的 change.actions 是 ["create"]、["update"]、["delete"] 之类的数组;before / after 则能让你做更细的字段级比对(比如实例规格、是否加密)。
第三步:在 GitLab CI 里做成强制门禁
.gitlab-ci.yml 中加一个独立 stage,用官方 OPA 镜像执行评估。--fail-defined 的含义是:只要 deny 集合非空就以非零码退出,CI 直接红灯。
yaml
stages:
- validate
- plan
- policy
plan:
stage: plan
image: hashicorp/terraform:latest
script:
- terraform init -input=false
- terraform plan -input=false -out=tfplan.bn
- terraform show -json tfplan.bn > tfplan.json
artifacts:
paths:
- tfplan.json
expire_in: 1 hour
opa-policy:
stage: policy
image: openpolicyagent/opa:latest
needs: ["plan"]
script:
# 有任何 deny 即失败;-f pretty 输出人类可读的违规信息
- opa eval -i tfplan.json -d policy/ 'data.terraform.plan.deny'
--format pretty --fail-defined再配合 GitLab 的 Merge Request + 受保护分支 + 必须流水线成功 设置,这一步就是物理意义上的"合并不了",而不是靠 reviewer 的自觉。
高危变更平均发现环节接入前:apply 后才暴露(无法量化为时长)
0
策略评估耗时秒级,约 3s
3
Plan 级问题漏检率(团队自评)核心规则纳入后接近 0
0
无效 MR 往返下降约 60%
60
进阶:让规则可测试、可复用
Rego 支持原生单元测试,这是我坚持用它而不是脚本的重要原因。同目录放 terraform_test.rego,构造一份最小的假 plan,CI 里跑 opa test policy/:
rego
package terragram.plan_test
# 注意:测试包名需与被测包一致,这里仅示意结构
mock_delete_db := {
"resource_changes": [
{
"address": "aws_db_instance.prod",
"type": "aws_db_instance",
"change": {"actions": ["delete"]}
}
]
}
test_delete_db_must_deny if {
deny[_] with input as mock_delete_db
}把通用策略沉淀成一个独立的 policy 仓库或 bundle,多个业务仓库通过 opa eval -b 加载,团队的安全基线就只维护一份,而不是在几十个项目里复制粘贴。
避坑总结:这些坑我都替你踩过
坑一:直接审查 .tf 源码或 plan 的文本输出。 现象是规则时灵时不灵,Terraform 一升级输出格式微调,grep 全线崩溃。修正方法只有一个——始终审查 terraform show -json 的结构化结果,并以 -out 二进制计划保证审查与 apply 同源。
坑二:用 count 或 for_each 后地址对不上。 AI 很爱用 for_each,资源地址会变成 module.x.aws_sg.rule["80"] 这种带键的形式,按固定字符串匹配会漏。我后来一律用 contains(addr, ...) 或正则匹配类型前缀,而不是写死完整地址。
坑三:把规则写得太严,团队开始绕过流水线。 我最初把"任何公网 IP"都禁掉,结果测试环境也没法干活,有人开始加 [skip ci]。正确姿势是按环境/标签分级:生产库删除硬拦截,测试环境给 warn 或要求 MR 标签豁免,而不是一刀切。红线要少而硬,才有人尊重。
坑四:只在 CI 跑,本地没有反馈。 等推上去才红灯,往返很慢。我给团队配了 pre-commit 钩子,本地提交前就跑一遍 plan + opa eval,绝大多数低级问题在写代码当下就被挡回,无效 MR 往返明显减少。
坑五:误把 OPA 当成唯一防线。 这个坑我排查过一次更隐蔽的问题:provider 版本漂移导致 plan 行为变化,OPA 只看计划却不锁版本。后来固定 terraform init -backend=false 配合 .terraform.lock.hcl,并保留 checkov 做静态基线——OPA 是语义闸门,不是全部。
写在最后
AI 时代,写 IaC 的边际成本趋近于零,但删掉一个生产数据库的成本一点没变。策略即代码的价值,是把老工程师脑子里那些"这个不能动、那个要审批"的隐性经验,固化成机器每次都严格执行、且不会因为赶时间而放水的显式规则。人负责判断规则该不该有,机器负责一万次不走样地执行——这大概是人和 AI 协作时最该守住的分工。
这套工作流我会继续在团队里迭代规则库,也欢迎你把自己的红线策略补充进来。本文首发于开发者日记,记录一个普通团队在 AI 代码洪流里搭堤坝的真实过程。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。