Terraform Targeted Plan/Apply 全过程学习

场景:在共享 production root/state 中,尽量低风险地新增一条 CloudWatch 核心群升级链路。

目标不是“重构所有告警”,而是:

  1. 保留现有首发群通知
  2. 新增核心群升级通知
  3. 尽可能只影响这次新增的资源
  1. 需求输入与约束

用户最终选择:

  • 保留当前 Terraform root
  • 不拆独立 state
  • 用 targeted plan/apply 控制 blast radius

关键约束:

  • 绝不能顺手改其他生产配置
  • 变更前必须先看 plan
  • 变更要可解释、可回滚、可验证
  1. 先做根因分析,不急着改

先确认的事实:

  • CloudWatch 普通 SNS/Lambda action 只会在状态变化时触发,不会自动重复提醒
  • 现网有些 alarm 有 OKActions,有些没有
  • 现有 AnotherMe-ApiServer-Error-Alarm 专用 forwarder 只处理 ALARM
  • 当前仓库里真正受 IaC 管理的相关 CloudWatch alarm 只有 2 条:
    • AnotherMe-ApiServer-Error-Alarm
    • XAI-Low-Balance
  1. 方案收敛

没做 ACK + escalation 的完整 SRE 流程,原因:

  • 对 10 人小团队偏重
  • 需要飞书交互回调、状态机、存储、身份处理

最终采用: “分级升级,不做 ACK”

这次最小落地:

  • 首发群:继续走原链路
  • 核心群:新增 2 条 delayed/core alarm
  1. 资源设计原则

核心原则:新增,不替换

不修改:

  • CloudWatchAlarmsToFeishu
  • ApiServerErrorsToFeishu
  • FeishuAlarmForwarder
  • FeishuApiServerAlarmForwarder
  • 现有首发 alarm

只新增:

  • 新 SNS topic
  • 新 Lambda forwarder
  • 新订阅和权限
  • 2 条新的 core alarm
  1. 实际新增的 6 个 AWS 资源

  2. aws_sns_topic.cloudwatch_alarms_core

  3. aws_lambda_function.feishu_alarm_core_forwarder

  4. aws_sns_topic_subscription.cloudwatch_alarms_core_lambda

  5. aws_lambda_permission.allow_sns_cloudwatch_alarms_core

  6. aws_cloudwatch_metric_alarm.api_server_error_alarm_core

  7. aws_cloudwatch_metric_alarm.api_billing_xai_low_balance_core

  1. 代码层面的新增文件

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
  1. 为什么先写测试

先写了一个最小单测,钉住新 forwarder 的核心行为:

  • ALARM 卡片头是红色
  • OK 卡片头是绿色
  • 人类可读 region 会被转换成 CloudWatch URL 可用的 region code

失败 -> 实现 -> 通过

这是这次变更里最小但关键的行为保护。

  1. 测试命令与实际过程

失败阶段: python3 -m unittest discover -s terraform/aws/environments/production -p 'test_cloudwatch_alarm_core_forwarder.py' -v

首次失败原因:

  • 模块导入时就读取 FEISHU_WEBHOOK_URL
  • 本地测试环境没有这个变量

修正:

  • 改成运行时校验,而不是 import 时校验
  1. 新 forwarder 的职责

FeishuAlarmCoreForwarder 做的事很克制:

  • 解析 CloudWatch SNS 消息
  • 组装一个简洁的飞书卡片
  • 区分 ALARM / OK / INSUFFICIENT_DATA 颜色
  • 生成 CloudWatch 控制台链接
  • POST 到核心群 webhook

它没有做:

  • ACK
  • 去重
  • 状态机
  • 定时提醒
  • 复杂鉴权
  1. Secret 处理策略

用户提供了核心群 webhook。

但没有把 webhook 硬编码进 Git。 做法:

  • 在 variables.tf 中新增 sensitive variable: cloudwatch_alarm_core_feishu_webhook_url
  • plan/apply 时通过 -var 注入

这样仓库代码不含明文 webhook。

注意:

  • 值仍会进入 Terraform state
  • 也会进入 Lambda 环境变量
  1. 两条新 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

  1. 为什么不是直接 terraform apply

因为当前 root 绑定整个 production 的同一个 state。

如果直接普通 apply:

  • Terraform 可能顺带看见其他 drift
  • 可能提出与你这次需求无关的修改

所以采用两层控制:

  1. targeted plan
  2. saved plan apply
  1. 第一次 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

  1. 先做 saved plan,再 apply

核心命令: terraform -chdir=terraform/aws/environments/production plan
-out=/tmp/cloudwatch-core-escalation.tfplan
...同一组 -target ...

意义:

  • apply 不再重新计算
  • 只执行这份已确认的计划
  • 最大程度避免“临时多出别的变更”
  1. 实际 apply

执行的命令: terraform -chdir=terraform/aws/environments/production apply -auto-approve /tmp/cloudwatch-core-escalation.tfplan

实际结果:

  • 6 added
  • 0 changed
  • 0 destroyed

这一步非常关键,因为它不是“按当前目录状态重新算”,而是“执行已保存计划”。

  1. Apply 输出里值得注意的点

正常创建顺序大致是:

  • SNS topic
  • 2 条 CloudWatch alarm
  • Lambda
  • Lambda permission
  • SNS subscription

警告:

  • 有 targeted apply warning
  • 有其他历史资源的 deprecated warning

理解要点:

  • warning 不等于这次修改了那些资源
  • 只要 saved plan 里没有它们,apply 就不会去改它们
  1. 实施后的验证方式

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
  1. 这次没有动到什么

明确没动:

  • 现有首发群 SNS topic
  • 现有首发群 Lambda
  • 现有 2 条首发 alarm
  • 其他所有控制台里已有的 CloudWatch alarms

这是“加一条升级链路”,不是“重构整个监控体系”。

  1. 当前方案的局限

这不是完整 SRE incident 流程,只是适合小团队的低成本升级机制。

没有:

  • ACK
  • 未 ACK 升级
  • 值班轮转
  • 事件状态机
  • 自动工单

但它已经解决了一个真实问题: “原群里首发消息被漏看时,重要告警会在一段时间后再打到更小更强的核心群”

  1. 回滚思路

如果要撤销这次变更,目标也只针对这 6 个资源。

最安全做法:

  • 先做 targeted destroy plan
  • 确认只涉及这 6 个地址
  • 再 apply 该 destroy plan

因为这次是“纯新增”,所以回滚相对简单,不涉及恢复旧配置。

  1. 这次值得记住的 Terraform 学习点

  2. 在共享 root/state 中,风险控制比写代码更重要。

  3. “新增资源,不改旧链路”是最实用的保守策略。

  4. plan 先看 blast radius,不能直接相信直觉。

  5. saved plan apply 比现算现 apply 更稳。

  6. sensitive variable 不会进 Git,但仍会进 state。

  7. targeted apply 是例外手段,不应成为日常常态。

  8. apply 成功后,state show 是非常实用的核验方法。

  1. 建议你复习时按这个顺序看

A. 为什么不能直接普通 apply B. 为什么先 targeted plan C. 为什么还要 saved plan D. saved plan 和 apply 的边界是什么 E. 如何证明这次只新增 6 个资源 F. 如何把 secret 风险压低但不阻塞上线

这 6 个问题理解了,Terraform 在生产环境里的“保守变更思路”就抓住了。

  1. 你可以自己复现的最小练习

  2. 找一个只新增、不会替换的资源组合

  3. 先写清楚“不会动到什么”

  4. 跑 targeted plan

  5. 看 plan 是否是纯 add

  6. 保存为 tfplan

  7. 再 apply 这份 tfplan

  8. 用 state show 验证结果

这套练习比单纯学 HCL 语法更接近生产实战。

进入实现前先钉住行为确认 plan 纯 add 后抽象成经验