CloudWatch 电话告警:用 EventBridge 响铃,无需 Lambda

SNS 打不了电话,载荷也难解析。用 EventBridge 把 CloudWatch 告警转成会响铃的来电 Webhook:配置、过滤与常见坑。

更新于

目录

CloudWatch 告警不能给你打电话。它内置的动作只有 SNS、Auto Scaling、EC2 和 Systems Manager,而 SNS 的终点是邮件、短信,或者一个你得正好盯着的队列。那些真能让手机响起来的教程,都要你搭一个 Lambda 函数再配一个 Twilio 账号。这两样都可以省掉:让 EventBridge 接收告警状态变更,把它重组成一个小小的 JSON 请求体,再 POST 给 Echobell 的来电频道。

这篇文章讲完整链路:频道、EventBridge 规则、输入转换器、让铃声保持意义的过滤条件,以及为什么把 SNS 直接指向 Webhook 看起来行得通、实际上基本不行。

为什么常规的 SNS 路线很别扭

常见的建议是「告警 → SNS 主题 → 订阅者」。用来打电话,这条路有三个问题:

  • SNS 没有语音协议。 订阅者类型有邮件、短信、移动推送、SQS、Lambda、Firehose 和 HTTP(S)。短信容易漏看,还会被勿扰模式挡住;语音电话得你自己搭。
  • HTTPS 消息是字符串里套字符串。 SNS 以 Content-Type: text/plain 投递它的信封,而告警本身是一份序列化后塞进信封 Message 字段的 JSON 文档。按字段取值的 Webhook 接收端——比如 AlarmName、NewStateValue——只能看到一整块不透明的内容。
  • 信封很大。 加上签名、签名证书 URL 和退订链接,信封在你的告警文本开始之前就已经超过 1 KB。Echobell 会把通知正文截断到 1500 字节,所以「整个请求体照搬」的模板会恰好在有用的内容开始的地方被截断。

你可以用 Lambda 把消息拆开再转发,让 SNS 能用。但 CloudWatch 其实已经把每一次告警状态变更以干净的 JSON 事件发布到了 EventBridge——不需要配置任何动作——而 EventBridge 自己就能重组事件并调用 HTTPS 端点。

你需要什么

  • 一个 AWS 账号,并有权限创建 EventBridge 规则、连接(connection)和 API destination
  • 装好 Echobell(App Store / Google Play)
  • 至少一个现有的 CloudWatch 告警
  • 十分钟

你这边不需要接收任何入站流量。出站请求由 EventBridge 发出。

第 1 步 —— 建一个会响铃的频道

在 Echobell 里新建频道,通知类型选 来电(Calling)。来电频道的行为像一通打进来的电话而不是推送,所以它能穿过 iOS 专注模式和勿扰模式。

第 3 步的转换器会发送一个扁平的请求体,包含 alarm、state、reason、region 和 link 字段,所以模板可以写得很短:

标题:{{state}}: {{alarm}}
正文:{{reason}}

在高级设置里配好链接模板,这样点通知就能直接在控制台里打开这个告警:

{{link}}

从频道详情页复制频道的 Webhook URL:

https://hook.echobell.one/t/<channel-token>

第 2 步 —— 创建 connection 和 API destination

EventBridge 通过 API destination 调用外部 HTTPS 端点,而它需要一个携带认证信息的 connection。Echobell 靠 URL 里的密钥 token 认证,所以这个 connection 只是走个形式——但没有它 EventBridge 不让你创建 destination。

  1. 打开 EventBridge → Integration → API destinations → Connections → Create connection
  2. Name:echobell
  3. Authorization type:API key
  4. API key name:x-source,Value:eventbridge
  5. 创建完成后,进入 API destinations → Create API destination
  6. Name:echobell-calling
  7. API destination endpoint:第 1 步拿到的 Echobell 频道 URL
  8. HTTP method:POST
  9. Invocation rate limit:每秒 1 次就足够,同时也是一道安全阀
  10. Connection:echobell

这个 API key 头会随每个请求一起发送。如果你想在 token 之外再加一道门,可以在 Echobell 频道条件里检查它:

header["x-source"] == "eventbridge"

第 3 步 —— 创建规则和输入转换器

在与告警相同的区域进入 EventBridge → Rules → Create rule。规则是区域级的;eu-west-1 里的告警对 us-east-1 里的规则是不可见的。

使用 default 事件总线和**事件模式(event pattern)**规则。下面这个模式会匹配所有进入 ALARM 的告警:

{
  "source": ["aws.cloudwatch"],
  "detail-type": ["CloudWatch Alarm State Change"],
  "detail": {
    "state": { "value": ["ALARM"] }
  }
}

目标选择 EventBridge API destination,并选 echobell-calling。让控制台新建一个执行角色——它只会为这个 destination 授予 events:InvokeApiDestination,别的什么都不给。

然后展开 Additional settings → Configure target input → Input transformer:

Input path

{
  "alarm": "$.detail.alarmName",
  "state": "$.detail.state.value",
  "reason": "$.detail.state.reason",
  "region": "$.region",
  "time": "$.time"
}

Template

{
  "alarm": "<alarm>",
  "state": "<state>",
  "reason": "<reason>",
  "region": "<region>",
  "time": "<time>",
  "link": "https://console.aws.amazon.com/cloudwatch/home?region=<region>#alarmsV2:alarm/<alarm>"
}

EventBridge 会以 application/json 发送转换结果,Echobell 因此能直接读取 {{alarm}} 和 {{state}}。

第 4 步 —— 搞清楚事件到底长什么样

输入路径读取的是 CloudWatch 发布的事件。精简后是这样:

{
  "version": "0",
  "id": "c4c1c1c9-6542-e61b-6ef0-8c4d36933a92",
  "detail-type": "CloudWatch Alarm State Change",
  "source": "aws.cloudwatch",
  "account": "123456789012",
  "time": "2026-10-16T03:12:44Z",
  "region": "us-east-1",
  "resources": ["arn:aws:cloudwatch:us-east-1:123456789012:alarm:prod-api-5xx"],
  "detail": {
    "alarmName": "prod-api-5xx",
    "state": {
      "value": "ALARM",
      "reason": "Threshold Crossed: 1 out of the last 1 datapoints [42.0] was greater than the threshold (10.0).",
      "timestamp": "2026-10-16T03:12:44.000+0000"
    },
    "previousState": { "value": "OK" },
    "configuration": { "description": "API 5xx errors on the prod load balancer" }
  }
}

改模板之前值得先知道几件事:

  • 告警信息在 detail 底下。 $.alarmName 什么都匹配不到;$.detail.alarmName 才行。
  • state.reason 是自动生成的文本。 它会写出数据点和阈值,正是你凌晨三点最想听到的内容。
  • 告警名是原样插入的。 告警名或 reason 里出现一个双引号,就会破坏转换器生成的 JSON。告警名请只用字母、数字、连字符和下划线——这本来也是好习惯——否则请求会以 400 失败。
  • 链接需要一个朴素的名字。 名字里带空格或 # 就无法正确深链;控制台搜索仍然能找到它。

第 5 步 —— 过滤到「值得一通电话」的程度

一个每个 ALARM 都响铃的告警频道,是教会你自己无视它的最快方式。过滤要放在两个地方。

在 EventBridge 模式里,它在任何东西发出之前就已生效:

目标模式改动
只要生产环境的告警在 detail 里加 "alarmName": [{ "prefix": "prod-" }]
跳过标记为不紧急的告警把它们命名为 warn-…,并使用 "alarmName": [{ "anything-but": { "prefix": "warn-" } }]
只要指定的告警"alarmName": ["prod-api-5xx", "prod-db-cpu"]
只要真正的告警,不要测试状态保留 "state": { "value": ["ALARM"] }——INSUFFICIENT_DATA 事件永远不会匹配
只限一个账号或区域在顶层加 "account": ["123456789012"] 或 "region": ["us-east-1"]

在 Echobell 里,用频道条件来表达模式表达不了的东西,比如时间窗口:

state == "ALARM" && (hour >= 17 || hour < 9)

Echobell 的时间变量是 UTC,先从你自己的时区换算一下。完整变量表见条件参考。

第 6 步 —— 给恢复通知留一扇更安静的门

再加一条 "value": ["OK"] 的规则,可以在问题解除时通知你,而不响铃。再建一个 时效性(Time Sensitive)(或普通)类型的 Echobell 频道,再建一个指向它的 API destination,并复用同一个输入转换器。标题模板 {{state}}: {{alarm}} 读起来已经就是「OK: prod-api-5xx」。

规则状态频道类型行为
alarms-to-callALARM,prod- 前缀来电穿透专注模式响铃
alarms-warnALARM,warn- 前缀时效性直接上锁屏,不响铃
alarms-recoveredOK普通普通推送

先测试,再信任

别等到生产环境出事。强制把告警置为你想要的状态:

aws cloudwatch set-alarm-state \
  --alarm-name prod-api-5xx \
  --state-value ALARM \
  --state-reason "Manual test of the Echobell path"

告警会在下一次评估时回到它的真实状态,但这次状态变更事件是真的,并且会经过你的规则。如果只想测试 Echobell 这一侧,直接触发频道:

curl -X POST https://hook.echobell.one/t/<channel-token> \
  -H 'Content-Type: application/json' \
  -d '{"alarm":"prod-api-5xx","state":"ALARM","reason":"Manual test","region":"us-east-1","link":"https://console.aws.amazon.com/cloudwatch/"}'

如果 curl 能响而告警测试不响,问题出在规则、转换器或 destination 上,而不在频道。

SNS 和邮件转电话呢?

如果你到处都已经有 SNS 主题,不想动 EventBridge,还有另外两条路:

  • SNS → Lambda → Webhook。 几行代码解析 Message,再把一个扁平的请求体 POST 到同一个 Echobell URL。能用,但你从此多了一个函数、一个角色和它的日志要维护。
  • SNS → 邮件 → Echobell 邮件触发。 把频道的邮件触发地址订阅到主题上。告警以邮件形式到达,模板里的 {{subject}} 和 {{text}} 就成了通知内容。这是所有方案里配置最少的,代价是 SNS 邮件订阅需要点一次确认链接,而且文本不会被重组。

把 Echobell 的 Webhook URL 直接作为 HTTPS 端点订阅到主题上,是应该避开的那条路:确认请求会作为通知到达,之后每条告警都是上面说的那种又长又臭的信封。

事故期间要紧的限制

  • 1 MiB 请求体。 超过 1 MiB 的触发请求会被 Echobell 以 HTTP 413 拒绝。转换后的告警只有几百字节,这里碰不到。
  • 每个 token 每分钟 120 次请求。 超过之后触发接口会返回 429、RATE_LIMIT_EXCEEDED 和一个 Retry-After。destination 每秒 1 次的速率限制能让你待在线内,而且 EventBridge 会重试投递失败的事件。
  • 通知正文 1500 字节。 更长的渲染结果会被截断。state.reason 一两句话,放得下。

这套方案给不了你什么

  • 没有确认(ack)。 接起电话不会告诉 CloudWatch 任何事,也不会让其他订阅者静音。
  • 没有升级和轮换。 订阅了频道的人全都会被打电话;不会有「下一个人」。
  • 告警之外没有额外去重。 一个每隔几分钟就 OK → ALARM 抖动的告警会每次都响。请通过在告警本身上拉长评估周期或设置 DatapointsToAlarm 来解决。
  • 没有双向同步。 修好了问题,也不会清掉你手机上的任何东西。

如果这些是硬伤,你需要的是一个完整的 on-call 平台。如果你要的只是「生产告警一触发就叫我」,那这就是控制台里的几个页面而已。

排障

告警触发了但什么都没到。 打开规则的 Monitoring 标签页:Invocations 显示模式是否匹配,FailedInvocations 显示投递是否失败。与告警不在同一区域的规则永远不会匹配。

规则匹配了但调用以 400 失败。 转换器产生了非法 JSON。检查告警名或 reason 里有没有引号或反斜杠。

通知是空白的。 模板读取了转换器没有发送的字段。请求体是扁平的:写 {{alarm}},不是 {{detail.alarmName}}。

同一个告警反复响。 告警在抖动。拉长评估周期,或要求更多数据点;这是 CloudWatch 的设置,不是 Echobell 的。

响了一个你没预料到的告警。 检查模式里是否漏了 prefix 过滤,并记住组合告警(composite alarm)以及由其他工具创建的告警(比如 Auto Scaling 策略)同样会发出状态变更。

常见问题

CloudWatch 能原生拨打电话号码吗?

不能。CloudWatch 告警动作只有 SNS 通知、Auto Scaling、EC2 动作和 Systems Manager。语音电话需要外部服务:通过某个服务商拨号的 Lambda、on-call 平台,或者像 Echobell 这样的来电 Webhook。

我需要 SNS 主题吗?

不需要。无论告警有没有配置动作,CloudWatch 都会自行把告警状态变更事件发给 EventBridge。你可以让告警的动作列表保持为空。

CloudWatch 告警能穿透勿扰模式吗?

只有当它以来电式提醒到达时才能。来电类型的 Echobell 频道行为像一通打进来的电话,iOS 的专注模式和勿扰模式会放行。短信或普通推送则不行。

为什么不直接把 Webhook URL 订阅到 SNS?

SNS 发送的是 text/plain,告警被序列化后塞在 Message 字符串里,所以模板读不到单个字段,而且信封长到会被截断。SNS 还会先发一条订阅确认,它会作为通知到达。

这在 AWS 上要花钱吗?

告警状态变更事件进入 default 事件总线不收费;API destination 的调用按请求计费。在一组健康告警的量级下,这个金额可以忽略不计,但请查看你所在区域当前的 EventBridge 定价。

能针对不同告警打给不同的人吗?

可以。每类受众建一个频道,每个频道配一条规则,让每个人用自己想要的通知类型订阅。因为没有确认机制,所以选了「来电」的人都会被打电话。

多个区域或多个账号的告警怎么办?

规则是按区域的,所以在你用到的每个区域都创建一条规则(同样的 API destination 定义在每个区域各重复一次)。多个账号的话,把事件发送到某个账号里的中央总线,并把规则放在那里。

小结

整条路径是五部分:一个来电频道、指向它的 connection 和 API destination、一条只匹配值得叫醒人的告警的规则、一个把事件压平的输入转换器,以及在你依赖这一切之前强制执行一次 set-alarm-state 测试。这里其余的所有内容,都是为了让它保持足够窄,让那声铃响一个月之后仍然有意义。

在 iPhone 上下载 Echobell 或在 Google Play 获取,然后跑一遍 set-alarm-state 测试。

相关内容