---
title: "CloudWatch 电话告警：用 EventBridge 响铃，无需 Lambda"
description: "SNS 打不了电话，载荷也难解析。用 EventBridge 把 CloudWatch 告警转成会响铃的来电 Webhook：配置、过滤与常见坑。"
date: 2026-10-16
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - AWS
  - CloudWatch
  - EventBridge
  - 电话告警
  - Webhook
  - on-call
---

# CloudWatch 电话告警：从 EventBridge 直达手机，无需 Lambda

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](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-cloudwatch-alarm-phone-call-alerts-zh&mt=8) / [Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid)）
- 至少一个现有的 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` 的告警：

```json
{
  "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**

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

**Template**

```json
{
  "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 发布的事件。精简后是这样：

```json
{
  "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，先从你自己的时区换算一下。完整变量表见[条件参考](/zh/docs/conditions)。

## 第 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` | 普通 | 普通推送 |

## 先测试，再信任

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

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

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

```bash
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 邮件触发。** 把频道的[邮件触发地址](/zh/docs/email-trigger)订阅到主题上。告警以邮件形式到达，模板里的 `{{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](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-cloudwatch-alarm-phone-call-alerts-zh&mt=8) 或[在 Google Play 获取](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid)，然后跑一遍 `set-alarm-state` 测试。

## 相关内容

- [频道条件参考](/zh/docs/conditions)
- [Webhook 集成指南](/zh/docs/webhook)
- [邮件触发指南](/zh/docs/email-trigger)
- [Sentry 电话告警](/zh/blog/sentry-phone-call-alerts)
- [API 宕机电话告警](/zh/blog/phone-call-alerts-api-downtime)
- [开发者的告警疲劳修复指南](/zh/blog/fix-alert-fatigue-developer-guide)
- [让关键告警穿透 iOS 专注模式](/zh/blog/how-to-bypass-ios-focus-mode-for-critical-alerts)
