Terraform Targeted Plan/Apply 全过程学习
场景:在共享 production root/state 中,尽量低风险地新增一条 CloudWatch 核心群升级链路。
目标不是“重构所有告警”,而是:
- 保留现有首发群通知
- 新增核心群升级通知
- 尽可能只影响这次新增的资源
- 需求输入与约束
用户最终选择:
- 保留当前 Terraform root
- 不拆独立 state
- 用 targeted plan/apply 控制 blast radius
关键约束:
- 绝不能顺手改其他生产配置
- 变更前必须先看 plan
- 变更要可解释、可回滚、可验证
- 先做根因分析,不急着改
先确认的事实:
- CloudWatch 普通 SNS/Lambda action 只会在状态变化时触发,不会自动重复提醒
- 现网有些 alarm 有 OKActions,有些没有
- 现有 AnotherMe-ApiServer-Error-Alarm 专用 forwarder 只处理 ALARM
- 当前仓库里真正受 IaC 管理的相关 CloudWatch alarm 只有 2 条:
- AnotherMe-ApiServer-Error-Alarm
- XAI-Low-Balance
- 方案收敛
没做 ACK + escalation 的完整 SRE 流程,原因:
- 对 10 人小团队偏重
- 需要飞书交互回调、状态机、存储、身份处理
最终采用: “分级升级,不做 ACK”
这次最小落地:
- 首发群:继续走原链路
- 核心群:新增 2 条 delayed/core alarm
- 资源设计原则
核心原则:新增,不替换
不修改:
- CloudWatchAlarmsToFeishu
- ApiServerErrorsToFeishu
- FeishuAlarmForwarder
- FeishuApiServerAlarmForwarder
- 现有首发 alarm
只新增:
- 新 SNS topic
- 新 Lambda forwarder
- 新订阅和权限
- 2 条新的 core alarm
-
实际新增的 6 个 AWS 资源
-
aws_sns_topic.cloudwatch_alarms_core
-
aws_lambda_function.feishu_alarm_core_forwarder
-
aws_sns_topic_subscription.cloudwatch_alarms_core_lambda
-
aws_lambda_permission.allow_sns_cloudwatch_alarms_core
-
aws_cloudwatch_metric_alarm.api_server_error_alarm_core
-
aws_cloudwatch_metric_alarm.api_billing_xai_low_balance_core
- 代码层面的新增文件
Terraform:
- terraform/aws/environments/production/monitoring_cloudwatch_alarm_escalation.tf
- terraform/aws/environments/production/variables.tf
- terraform/aws/environments/production/main.tf
Python:
- terraform/aws/environments/production/cloudwatch_alarm_core_forwarder.py
测试:
- terraform/aws/environments/production/test_cloudwatch_alarm_core_forwarder.py
说明文档:
- docs/plans/2026-04-08-cloudwatch-core-escalation.md
- 为什么先写测试
先写了一个最小单测,钉住新 forwarder 的核心行为:
- ALARM 卡片头是红色
- OK 卡片头是绿色
- 人类可读 region 会被转换成 CloudWatch URL 可用的 region code
失败 -> 实现 -> 通过
这是这次变更里最小但关键的行为保护。
- 测试命令与实际过程
失败阶段: python3 -m unittest discover -s terraform/aws/environments/production -p 'test_cloudwatch_alarm_core_forwarder.py' -v
首次失败原因:
- 模块导入时就读取 FEISHU_WEBHOOK_URL
- 本地测试环境没有这个变量
修正:
- 改成运行时校验,而不是 import 时校验
- 新 forwarder 的职责
FeishuAlarmCoreForwarder 做的事很克制:
- 解析 CloudWatch SNS 消息
- 组装一个简洁的飞书卡片
- 区分 ALARM / OK / INSUFFICIENT_DATA 颜色
- 生成 CloudWatch 控制台链接
- POST 到核心群 webhook
它没有做:
- ACK
- 去重
- 状态机
- 定时提醒
- 复杂鉴权
- Secret 处理策略
用户提供了核心群 webhook。
但没有把 webhook 硬编码进 Git。 做法:
- 在 variables.tf 中新增 sensitive variable: cloudwatch_alarm_core_feishu_webhook_url
- plan/apply 时通过 -var 注入
这样仓库代码不含明文 webhook。
注意:
- 值仍会进入 Terraform state
- 也会进入 Lambda 环境变量
- 两条新 alarm 的具体设计
A. AnotherMe-ApiServer-Error-Alarm-Core
- metric: ApiServerErrorCount
- threshold: >= 1
- period: 300s
- evaluation_periods: 6
- datapoints_to_alarm: 6 => 连续 30 分钟仍报错才升级
B. XAI-Low-Balance-Core
- metric: Remaining
- threshold: < 10
- period: 1800s
- evaluation_periods: 2
- datapoints_to_alarm: 2 => 连续 60 分钟低余额才升级
两者都配置 ALARM + OK 到新的 core topic
- 为什么不是直接 terraform apply
因为当前 root 绑定整个 production 的同一个 state。
如果直接普通 apply:
- Terraform 可能顺带看见其他 drift
- 可能提出与你这次需求无关的修改
所以采用两层控制:
- targeted plan
- saved plan apply
- 第一次 targeted plan
核心命令:
terraform -chdir=terraform/aws/environments/production plan
-var='cloudwatch_alarm_core_feishu_webhook_url=...'
-target=aws_sns_topic.cloudwatch_alarms_core
-target=aws_lambda_function.feishu_alarm_core_forwarder
-target=aws_sns_topic_subscription.cloudwatch_alarms_core_lambda
-target=aws_lambda_permission.allow_sns_cloudwatch_alarms_core
-target=aws_cloudwatch_metric_alarm.api_server_error_alarm_core
-target=aws_cloudwatch_metric_alarm.api_billing_xai_low_balance_core
结果: Plan: 6 to add, 0 to change, 0 to destroy
- 先做 saved plan,再 apply
核心命令:
terraform -chdir=terraform/aws/environments/production plan
-out=/tmp/cloudwatch-core-escalation.tfplan
...同一组 -target ...
意义:
- apply 不再重新计算
- 只执行这份已确认的计划
- 最大程度避免“临时多出别的变更”
- 实际 apply
执行的命令: terraform -chdir=terraform/aws/environments/production apply -auto-approve /tmp/cloudwatch-core-escalation.tfplan
实际结果:
- 6 added
- 0 changed
- 0 destroyed
这一步非常关键,因为它不是“按当前目录状态重新算”,而是“执行已保存计划”。
- Apply 输出里值得注意的点
正常创建顺序大致是:
- SNS topic
- 2 条 CloudWatch alarm
- Lambda
- Lambda permission
- SNS subscription
警告:
- 有 targeted apply warning
- 有其他历史资源的 deprecated warning
理解要点:
- warning 不等于这次修改了那些资源
- 只要 saved plan 里没有它们,apply 就不会去改它们
- 实施后的验证方式
A. Terraform apply 回执
- Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
B. terraform state list 确认 6 个资源都进入 state
C. terraform state show 重点核验:
- FeishuAlarmCoreForwarder 已有 ARN 和环境变量
- AnotherMe-ApiServer-Error-Alarm-Core 的 actions 指向 CloudWatchAlarmsCoreToFeishu
- XAI-Low-Balance-Core 的 actions 指向 CloudWatchAlarmsCoreToFeishu
- SNS subscription endpoint 指向新 Lambda
- 这次没有动到什么
明确没动:
- 现有首发群 SNS topic
- 现有首发群 Lambda
- 现有 2 条首发 alarm
- 其他所有控制台里已有的 CloudWatch alarms
这是“加一条升级链路”,不是“重构整个监控体系”。
- 当前方案的局限
这不是完整 SRE incident 流程,只是适合小团队的低成本升级机制。
没有:
- ACK
- 未 ACK 升级
- 值班轮转
- 事件状态机
- 自动工单
但它已经解决了一个真实问题: “原群里首发消息被漏看时,重要告警会在一段时间后再打到更小更强的核心群”
- 回滚思路
如果要撤销这次变更,目标也只针对这 6 个资源。
最安全做法:
- 先做 targeted destroy plan
- 确认只涉及这 6 个地址
- 再 apply 该 destroy plan
因为这次是“纯新增”,所以回滚相对简单,不涉及恢复旧配置。
-
这次值得记住的 Terraform 学习点
-
在共享 root/state 中,风险控制比写代码更重要。
-
“新增资源,不改旧链路”是最实用的保守策略。
-
plan 先看 blast radius,不能直接相信直觉。
-
saved plan apply 比现算现 apply 更稳。
-
sensitive variable 不会进 Git,但仍会进 state。
-
targeted apply 是例外手段,不应成为日常常态。
-
apply 成功后,state show 是非常实用的核验方法。
- 建议你复习时按这个顺序看
A. 为什么不能直接普通 apply B. 为什么先 targeted plan C. 为什么还要 saved plan D. saved plan 和 apply 的边界是什么 E. 如何证明这次只新增 6 个资源 F. 如何把 secret 风险压低但不阻塞上线
这 6 个问题理解了,Terraform 在生产环境里的“保守变更思路”就抓住了。
-
你可以自己复现的最小练习
-
找一个只新增、不会替换的资源组合
-
先写清楚“不会动到什么”
-
跑 targeted plan
-
看 plan 是否是纯 add
-
保存为 tfplan
-
再 apply 这份 tfplan
-
用 state show 验证结果
这套练习比单纯学 HCL 语法更接近生产实战。