服务连不上 Redis:VNet 集成改变了出站 IP

测试环境的推送服务连不上 Azure Redis。开启 VNet 集成和 route all 后,访问 Redis 的连接从 NAT gateway 的 IP 出去,而 Redis 防火墙只放行了 App Service 的默认出站 IP。

文章目录
  1. 问题出在哪一层
  2. 出站 IP 并不是看到的那个
  3. 解决办法
  4. 顺带准备 TLS
  5. 待收尾的事
  6. 复盘

测试环境里有个推送服务连不上 Azure Redis。服务是跑在 Azure App Service 上的 Node.js 应用,用 BullMQ 做任务队列,队列数据存放在 Azure Cache for Redis。队列初始化失败导致依赖队列的推送发不出去。

这篇内容整理自我事后写的排查报告。这里不靠记忆去还原当时一步步排查的过程,只陈述记录下来的证据。

问题出在哪一层

连接失败通常分好几种情况:

  • host 或 port 填错了。
  • 密码或 TLS 配置不对,连接虽然建立成功,但握手或鉴权失败。
  • 网络根本不通,连接直接被拒绝。

报告里记录当时的配置是 host 正常、port 为 6379 且关闭了 TLS。错误现象是网络阻断,也就是连接被拒绝,而不是鉴权报错。这表明问题出在网络层,Redis 没有接受请求。

Azure Redis 配了防火墙,只放行加白名单的 IP。因此核心问题在于:这个 Web App 到底是用哪个 IP 去连 Redis 的?

出站 IP 并不是看到的那个

App Service 的控制台页面上列有一组 outbound IP addresses,Redis 防火墙加的也正是这几个 IP。纸面上看完全对得上。

但是这个 Web App 开启了 VNet integration,并且打开了 route all(vnetRouteAllEnabled: true),子网里还挂了一台 NAT gateway。报告里指出,这意味着访问 Redis 的连接实际是从这台 NAT gateway 出去的。在 Redis 看来,请求来源是 NAT gateway 的公网 IP,根本不是 App Service 页面上列出的那些地址。

这个结论只针对访问 Redis 的这条路径,我没有去核对服务访问的所有外部目标或私网路由。

控制台页面上显示的 outbound IP 并没有错,只是在当前的网络拓扑下,这条连接使用的出口已经变了。

把下面三点对照来看,原因就很清楚了:

  1. Web App 开启了 VNet integration 和 route all,绑定了挂有 NAT gateway 的子网。
  2. NAT gateway 有一个固定的公网 IP。
  3. Redis 防火墙只加了 App Service 默认的 outbound IP,没有加 NAT 的公网 IP。

解决办法

网络层面的改法只有一步:把 NAT gateway 的公网 IP 加进 Redis 防火墙的白名单。

az redis firewall-rules create \
  --resource-group <resource-group> --name <redis-name> \
  --rule-name AllowNatGateway \
  --start-ip <nat-public-ip> --end-ip <nat-public-ip>

改完后跑了服务的 health check,Redis、队列和应用自身全部通过,Redis 响应时间只有几毫秒。当时环境还在用非 TLS 端口,因此这次解决跟 TLS 没有关系。

顺带准备 TLS

当时测试环境用的是非 TLS 端口 6379,我顺便改了队列的连接配置,支持通过环境变量开启 TLS。在我们这里的配置中,开启 TLS 会走 6380 端口,并把 servername 设为 Redis 的 host(SNI)。我没测试过不配 servername 会怎样,所以不能说它是强制要求的。

const connection = {
  host: process.env.REDIS_HOST ?? 'localhost',
  port: Number(process.env.REDIS_PORT ?? 6379),
  password: process.env.REDIS_PASSWORD,
  ...(process.env.REDIS_TLS_ENABLED === 'true'
    ? { tls: { servername: process.env.REDIS_HOST } } // SNI: how we connect to Azure Redis
    : {}),
};

按计划,后续切 TLS 只需要改两个 App Settings(开关和端口号)并重启服务,不需要改代码,要回滚也是直接改回配置。这个方案写在报告里,切 TLS 的操作当时还没有实际跑过。

待收尾的事

报告里列了后续建议处理的事项,这些只是建议,还没有确认执行:

  • 清理旧的防火墙规则。 如果这个服务发往 Redis 的流量全部走 NAT gateway,那之前配给 App Service 默认 outbound IP 的规则就没用了。报告里写的是“如果”,我没有确认是不是还有其他 client、deployment slots 或回滚路径在用这些 IP,单凭手头的证据不会直接删掉它们。
  • 等 TLS 稳定后关闭非 TLS 端口。
  • 加报警。 在 Application Insights 里配置 Redis 连接失败的报警,而不是等 QA 发现推送发不出去才发现问题。
  • 把 NAT 出口补进文档。 要避免“一处改了网络配置,另一处的白名单没跟上”,最好的办法是把这种检查写进变更 checklist。

复盘

这个问题本身跟 Redis 关系不大,核心在于“出站 IP”的判定。只要网络路由有改动,比如开启 VNet integration、挂载 NAT gateway 或调整出口,下游依赖 IP 白名单的服务(数据库、Redis、第三方 API 等)就可能把请求拦在外面。而且控制台页面上标出的 outbound IP 并不会提醒你它已经不再是某条连接的实际出口。

遇到类似连不上的情况,最该问的还是那句话:这次连接到底来自哪个 IP,这个 IP 在不在白名单里。先看报错类型确定属于哪一层,再把实际的出口跟防火墙规则对一遍。