目录
- 为什么常规的 SNS 路线很别扭
- 你需要什么
- 第 1 步 —— 建一个会响铃的频道
- 第 2 步 —— 创建 connection 和 API destination
- 第 3 步 —— 创建规则和输入转换器
- 第 4 步 —— 搞清楚事件到底长什么样
- 第 5 步 —— 过滤到「值得一通电话」的程度
- 第 6 步 —— 给恢复通知留一扇更安静的门
- 先测试,再信任
- SNS 和邮件转电话呢?
- 事故期间要紧的限制
- 这套方案给不了你什么
- 排障
- 常见问题
- CloudWatch 能原生拨打电话号码吗?
- 我需要 SNS 主题吗?
- CloudWatch 告警能穿透勿扰模式吗?
- 为什么不直接把 Webhook URL 订阅到 SNS?
- 这在 AWS 上要花钱吗?
- 能针对不同告警打给不同的人吗?
- 多个区域或多个账号的告警怎么办?
- 小结
- 相关内容
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。
- 打开 EventBridge → Integration → API destinations → Connections → Create connection
- Name:
echobell - Authorization type:API key
- API key name:
x-source,Value:eventbridge - 创建完成后,进入 API destinations → Create API destination
- Name:
echobell-calling - API destination endpoint:第 1 步拿到的 Echobell 频道 URL
- HTTP method:
POST - Invocation rate limit:每秒
1次就足够,同时也是一道安全阀 - 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-call | ALARM,prod- 前缀 | 来电 | 穿透专注模式响铃 |
alarms-warn | ALARM,warn- 前缀 | 时效性 | 直接上锁屏,不响铃 |
alarms-recovered | OK | 普通 | 普通推送 |
先测试,再信任
别等到生产环境出事。强制把告警置为你想要的状态:
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 测试。