
先交代一下背景:做 TerraTier(一个 3 层 AWS 架构项目)的时候,我一直在想能不能换个方向折腾点不一样的东西。TerraTier 解决的是基础设施层的问题——网络、安全边界、密钥管理。那这个项目想解决的,是运维层的问题:Terraform 代码跑完之后,如果有人在 AWS 控制台手动改了一通,会发生什么?
于是就有了 TerraGuard AI——一个事件驱动的漂移检测系统,核心思路很简单:Terraform 检测漂移,大模型(LLM,Large Language Model)来分类风险级别,然后智能路由告警,而不是每次 description 字段改了都给你发一条噪音。监控对象是 ECS Fargate 上的 Node.js API、RDS PostgreSQL 数据库和 ALB。检测和分析靠 GitHub Actions + Python + Groq 的 Llama 3.3 API 实现。
这篇文章不讲功能清单,讲的是设计决策背后的逻辑,以及我踩过的坑。
教科书式的 Terraform 漂移检测实现大概是这样:定时跑 terraform plan -detailed-exitcode,检查退出码,非零就发告警。30 行 shell 脚本能搞定,效果也 OK。问题不在检测本身——问题在于收到告警的人。
只要团队里超过一个人会登录 AWS 控制台,漂移几乎就是常态。有人直接在控制台给安全组打了个标签,或者改了个描述,或者调整了一个还没加进 Terraform 的日志组保留期——这些和"生产安全组 port 22 对 0.0.0.0/0 开放"的告警内容完全一样,告警优先级也一样。值班的工程师第三次被 tag 变更叫醒之后,就会开始做出人类面对噪音信号时的正常反应:直接忽略。
告警疲劳是 SRE(Site Reliability Engineering,站点可靠性工程)领域的老问题了,但大多数漂移检测工具的解法是给你更多配置选项——例外清单、忽略规则、阈值调参。这是在治标。真正的原因是:告警系统没有"严重程度"的概念。它知道有东西变了,但不知道这个变化意味着什么。
这个项目最核心的 insight 是:检测和分类是两个不同的问题,需要不同的工具。Terraform 做检测是一把好手——它有完整的资源 schema,直接调 AWS API,plan 输出是确定性的、可靠的。但它完全不理解某个变更从安全或业务角度意味着什么。这是一个推理问题,而语言模型恰恰擅长这个。
所以 TerraGuard AI 用 Terraform 检测漂移,用 LLM 分类风险。各自做自己擅长的事。
这个区别值得多花点篇幅,因为很容易想象 AI 被用来"找"漂移——比如用向量相似度比较当前状态和期望状态之类的。这等于用 AI 去拙劣地复制 Terraform 已经完美做到的事。
这个项目里 LLM 只看到 terraform plan 的解析输出。AI 介入的时候,漂移已经确认了。AI 做的是回答另一类问题:这个变更是安全风险还是配置细节?应该回滚还是加到 Terraform 代码里?具体应该执行什么命令来修复,现在就能执行,不用去查文档?
Prompt 要求 Groq 的 Llama 3.3 扮演一位资深云安全工程师,返回结构化的 JSON:风险级别(CRITICAL / HIGH / MEDIUM / LOW / INFO)、10 分制评分、影响摘要、建议操作(REVERT / ADOPT / INVESTIGATE / MONITOR),以及具体的修复命令。temperature 设成 0.1——不是 0,因为零温度输出有时候会显得很机械,但也要足够接近零,确保分析是一致的、事实性的,而不是在创造内容。
{ "risk_level": "CRITICAL", "risk_score": 9, "category": "Security", "summary": "Security group ingress rule added exposing SSH port 22 to all internet traffic.", "impact": "Any host on the internet can attempt SSH brute-force or exploitation against ECS tasks.", "action": "REVERT", "remediation": "terraform apply -target=aws_security_group.ecs_tasks", "reasoning": "Opening port 22 to 0.0.0.0/0 is a critical security misconfiguration..."
}
根据这个响应,路由规则很简单:CRITICAL 或 HIGH 立刻发 Slack,同时建 GitHub Issue 留审计记录。MEDIUM、LOW、INFO 只建 GitHub Issue 进 backlog——不触发 Slack,不 paging 值班人员。
实际效果就是:Slack 频道里只有真正需要人立刻处理的消息。其他的变成一个自组织的 backlog,在正常工作时间处理。这不是功能,这是整个系统的核心目标。
对于一个 AWS 原生的项目,最显而易见的选择是 Amazon Bedrock。LLM 调用跑在 VPC 内部,认证复用项目里其他基础设施用的同一个 IAM(Identity and Access Management,身份与访问管理)角色,没有外部 API 依赖。
我选了 Groq,出于两个原因,实际影响比预想的大。
第一个原因是速度。Groq 的定制 LPU 硬件跑 Llama 3.3 70B 的速度确实离谱——完整的风险分析几秒钟搞定。同等规模模型 Bedrock 推理要慢不少。当然了,这个项目漂移检测跑在 GitHub Actions 里,定时每 6 小时一次,延迟敏感度没那么高。但对于本地开发和测试——手动跑检测器等输出的时候——速度差异让迭代体验好很多。
第二个原因是成本。Groq 免费额度每天 14,400 次请求,对于一个每 6 小时跑一次、只在检测到漂移时才调 API 的工作流来说绑绑有余。Bedrock 按 token 计费,没有免费档。对于一个同时还要付 ECS、RDS、NAT Gateway 和 ALB 账单的个人作品集项目,不用付推理费用是个很实际的约束。
代价是外部依赖。漂移检测器现在需要从 GitHub Actions runner 发起到 api.groq.com 的出站 HTTPS 连接。如果 Groq 挂了或者在限速,分类步骤就会失败。我在 Python 脚本里做了 fallback:如果 Groq 调用失败或返回无法解析的 JSON,检测器会 fallback 到 MEDIUM 风险评估并建议人工 review,而不是直接崩溃。漂移仍然会被记录和处理,只是没有 AI 分析。
从架构上说,更"纯粹"的定时漂移检测方案是 Lambda 函数配合 EventBridge 定时触发,外加一条额外的 EventBridge 规则监听 CloudTrail 事件来做控制台变更的实时检测。这是这个项目原始设计里的企业级架构。
我选了 GitHub Actions,事后看这是正确的选择。
实践层面的理由:GitHub Actions 是免费的,项目已经配好了,不需要额外部署、不需要做 IAM 权限隔离、不需要维护一个独立的计算资源。Lambda 函数至少要加三个 Terraform 资源、一个 ECR 镜像或部署包来维护、一个 CloudWatch 日志来查问题——这些都是实打实的工作量,但对演示项目能力没什么直接加分。GitHub Actions 里的漂移检测工作流用更少的基础设施表面积和更简单的调试流程(Actions tab 在 GitHub 里,日志直接在那儿)干了同样的活。
概念层面的理由:漂移检测不是延迟敏感型工作负载。6 小时的检测窗口对于作品集项目来说没问题,很多实际生产环境也是如此——在 6 小时内发现安全组变更,相比等人去手动翻 AWS 控制台,已经是很大的改进了。通过 CloudTrail + EventBridge 做实时检测在项目 roadmap 里,但不是前置条件,是在一个可用的基线之上叠加的增强。
这个决策有个实际的代价:GitHub Actions runner 需要 AWS 凭证。这意味着 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 作为仓库 secrets 存储——存储在 GitHub 里的长期凭证,这是一个不如 OIDC role 假设安全的模式。项目里 Terraform 的 IAM 代码已经搭好了 OIDC 设置(aws_iam_openid_connect_provider、aws_iam_role 带 GitHub OIDC trust policy),迁移到无密钥认证是 roadmap 上优先级最高的事项。先用 access key 跑起来是为了不让 OIDC trust policy 调试和 workflow 本身同时进行,这个权衡在同样情况下我还会再做一次。
一个合理的设计问题是:应该监控哪些基础设施?你完全可以说基础设施越复杂 demo 越厉害——20 个资源的 VPC、多环境、Auto Scaling Group,一应俱全。
我选了相反的方向。被监控的基础设施尽量精简,但要真实:1 个 ECS Fargate 服务、1 个 RDS PostgreSQL 数据库、1 个 ALB。这个组合在实际生产环境里太常见了——就是一个负载均衡器后面接 API 后端加持久化存储。没什么花哨的。
保持简单的原因是:这个项目的亮点是漂移检测和分类层,不是基础设施。如果搞一个 40 个资源的多层 VPC 加 Auto Scaling Group,解释这个基础设施本身就要占掉大量篇幅,而且会让 CI/CD 和漂移检测工作流更难被理解。基础设施保持简单易懂,读者可以快速越过"这个基础设施是干嘛的",直接看"漂移检测实际怎么工作的"——后者才是值得花笔墨的地方。
整个 ecs.tf 文件里最实用的一行代码是这个:
lifecycle { ignore_changes = [task_definition]
}
没有这行,terraform apply 和 deploy-app.yml GitHub Actions workflow 会打起来。部署 workflow 每次代码 push 到 app/** 都会更新 ECS task definition 用新的 ECR 镜像 URI(用 git SHA 打 tag)。如果 Terraform 的 ECS service 资源不忽略 task definition 变更,下一次 terraform apply——由漂移检测器或手动基础设施变更触发——就会用 Terraform 变量里的镜像 URI 覆盖 CI/CD 部署的镜像。结果就是:刚部署了新版本的应用,立刻就会在 Terraform 托管状态里产生"漂移"。
ignore_changes lifecycle 设置告诉 Terraform:"我知道这个属性可能和配置里写的不一样,这是有意为之的——它归 CI/CD 管,不归你管。"这是 ECS 上分离基础设施管理(Terraform 的活)和应用部署(CI/CD 的活)的标准模式,是那种"回头看当然是这样,但没踩过坑之前不会想到要这样配"的东西。
部署 workflow 本身的做法值得理解:不是简单 push 镜像然后调 aws ecs update-service --force-new-deployment 就完了。它先从 AWS 下载当前正在运行的 task definition,只替换掉容器镜像 URI,把修改后的定义注册为新版本,然后让 service 使用那个新版本。这样做是因为 task definition 里包含 Secrets Manager ARN、IAM role ARN、环境变量集、健康检查配置——这些都是 Terraform 管的。如果从 CI/CD workflow 直接覆盖整个 task definition,这些字段就全丢了。只下载 live 定义然后只 patch 镜像 URI,保留其他所有东西。
Node.js 应用获取数据库连接信息靠两种方式:普通环境变量和 ECS task definition 里的 secrets 块。
非敏感值——DB_HOST、DB_PORT、DB_NAME、DB_USER、NODE_ENV、PORT——作为普通环境变量传递。它们不敏感:知道 RDS 主机名并不会帮助攻击者进入数据库。
密码不一样。ECS 的 secrets 块通过 ARN 引用 Secrets Manager 的密钥,容器启动时 ECS agent 获取值、作为 DB_PASSWORD 注入容器环境变量,整个过程不会写入 task definition JSON、CloudWatch logs 或 AWS 控制台里任何可见的地方。唯一的痕迹是那个 ARN 引用。
secrets = [ { name = "DB_PASSWORD" valueFrom = aws_secretsmanager_secret.db_password.arn }
]
这还需要一个特定 IAM policy 挂到 ECS 执行角色上(不是 task role——是启动容器的 agent,不是容器本身):
Action = ["secretsmanager:GetSecretValue"]
Resource = aws_secretsmanager_secret.db_password.arn
权限范围很关键。这个 policy 只授权访问一个特定的 secret ARN,而不是 "Resource": "*"。如果攻击者找到方法从容器内部提权到执行角色,他们只能读取一个具体的已知 ARN 的密钥,读取不到账户里其他任何密钥。这个改动一行代码,但显著缩小了爆炸半径。
给 Node.js 应用端口(3000)加上 Terraform 配置——替换掉初始 setup 里的占位符 80——然后跑 terraform apply,报了一个我没见过的错:
Error: deleting ELBv2 Target Group: ResourceInUse: Target group is currently in use by a listener or a rule
发生了什么:改 ALB target group 的端口会强制替换(Terraform 创建新的 target group,然后尝试删除旧的)。但 ALB listener 在 Terraform 尝试删除旧 target group 的时候还指向旧的。listener 更新和 target group 删除执行顺序错了——更准确地说,是并行执行了,删除操作和 listener 更新抢资源,删除输了。
修法很简单:再跑一次 terraform apply。第一次 apply 创建了新的 target group,部分更新了 listener,但旧的 target group 删除跑的时候 listener 变更还没完成。第二次 apply 发现 listener 已经指向新的 target group 了,旧的顺利删掉。
提这个是因为这种错误看起来吓人,实际上完全无害——知道这个规律("Terraform apply 报 ResourceInUse,再跑一次就好了")从文档学要很久,从经验学大概五秒钟。如果你遇到了:再 apply 一次。
更深的教训是关于 Terraform 依赖图的。Terraform 从配置的 depends_on 和隐式引用构建显式依赖图,在图允许的范围内尽量并行。当你在修改一个"有东西依赖它"的资源,同时又在做原地替换而不是 in-place 更新,替换的操作顺序可能不那么直观。看 terraform plan 输出的时候多注意一下 -/+ 标记——表示"销毁并重建"的——apply 之前就能提前发现这个问题。
当前设计最大的缺口是实时检测。每 6 小时跑一次 terraform plan,意味着手动控制台变更和漂移检测告警之间最长有 6 小时的窗口。大多数配置变更这个窗口可能 OK。但对于"安全组规则向互联网开放 port 22"这种,就不行了。
填补这个缺口的架构是现成的:一条 EventBridge 规则监听 CloudTrail 事件里特定的 API 调用(AuthorizeSecurityGroupIngress、ModifyDBInstance、AttachRolePolicy 以及类似的高风险变更操作),触发一个 Lambda 函数在变更发生几秒内按需跑 plan-and-classify 流程。这是项目原始设计里的企业级模式,v2 的时候会加进去。
GitHub Actions 的 OIDC 认证是另一个 immediate 优先级。把 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 存为仓库 secrets 能用,但有不必要的风险——这些凭证会一直有效直到被轮换,而轮换的前提是你记得它们存在。iam.tf 里的 OIDC 设置已经搭好了,只差一个 workflow 改动加删掉一个 secret 就能切换过去。
漂移历史记录存储是我画过草图但还没做的。现在每次漂移检测运行都是无状态的——结果发到 Slack 或 GitHub Issues,然后就结束了。如果你想回答"我们基础设施多久漂移一次、朝哪个方向漂移、是变好还是变坏了?"没有地方查。一个 DynamoDB 表存储每次运行的检测结果,加一个简单的 dashboard,就能把 TerraGuard AI 从被动告警工具变成真正有基础设施合规趋势可观测性的东西。
这个项目最有用的一个框架是:它不是一个 AI 项目外挂了点基础设施,也不是一个基础设施项目外挂了点 Python。它是一个运维自动化项目,恰好在需要人类做判断的环节用了 AI。
这个框架很重要,因为它决定了要真正做好这个项目,你需要理解什么。Groq 集成就十几行 Python——真正有意思的工程在什么时候调它、用什么输入、调完怎么处理输出,这需要把基础设施、CI/CD pipeline、IAM 模型和告警路由当成一个系统来思考。这些单独拎出来都不复杂,有趣的是让它们组合在一起。