Gateway API 与每节点一个 Envoy — Cilium 服务网格的结构
一句话总结
Cilium 把 Gateway API 资源翻译成 CiliumEnvoyConfig,加载到每个节点各有一个的 Envoy中,eBPF 则把到达服务端口的流量拦截并交给这个 Envoy。不给每个 Pod 添加 sidecar 的这种结构就是 sidecarless 服务网格,加密则通过 WireGuard 或 IPsec 在节点之间透明地进行。
为什么需要它
Ingress 资源的标准只涵盖到按主机和路径分流。想按权重分流、修改请求头或重写 URL,就得使用各控制器不同的注解,而这些注解换到别的控制器就失效了。标准里只有 HTTP 和 HTTPS,没有 TCP、UDP,也不适合多个团队共用一个负载均衡器的集群。Cilium 文档把这些归纳为 Ingress 的局限,并把 Kubernetes SIG-Network 随后设计的 Gateway API 作为答案。
服务网格一侧也有同类问题。给每个 Pod 挂上 sidecar 代理,可以获得 L7 功能,但代理数量会随 Pod 数量增长。Cilium 从一开始就选择了这样的结构:IP、TCP、UDP 处理由 eBPF 在内核中完成,只有 HTTP、gRPC、DNS 等应用协议才交给 Envoy。CCA 的 Service Mesh 领域(16%)会考查这种结构的理由、Gateway API 的优势以及加密选项。
工作原理
Gateway API 资源与角色分离
Gateway API 以角色为中心(role-oriented)设计。文档列举的三种角色是基础设施提供者(Infrastructure Provider)、集群运维人员(Cluster Operator)、应用开发者(Application Developer)。GatewayClass 指定由哪种实现来承担网关(Cilium 为 cilium),Gateway 指定监听器(协议、端口)以及接收哪些命名空间的 Route,HTTPRoute 则写入匹配规则和后端。可以划分权限,让开发者只在自己的命名空间中创建 Route,而不能改动 Gateway 配置。
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-route-1
spec:
parentRefs:
- name: cilium-gw # 어느 Gateway 에 붙는가
rules:
- matches:
- path: {type: PathPrefix, value: /echo}
backendRefs:
- {kind: Service, name: echo-1, port: 8080, weight: 50}
- {kind: Service, name: echo-2, port: 8090, weight: 50}
backendRefs 中的 weight 就是流量分割。文档示例从 50/50 开始,再改为 99/1,并说明可用于金丝雀(Canary)和 A/B 场景。它是标准字段而不是注解,所以换到其他实现中也能照常工作。
Cilium 支持 Gateway API v1.6.1 的 GatewayClass、Gateway、HTTPRoute、GRPCRoute、TLSRoute、BackendTLSPolicy、ReferenceGrant,并通过了 Core 一致性测试;TCPRoute、UDPRoute、ListenerSet 只有在安装了 CRD 时才会启用。前置条件是 kubeProxyReplacement=true 和 l7Proxy=true(默认值),并用 gatewayAPI.enabled=true 启用控制器。默认会创建 LoadBalancer 类型的服务,从 1.16 开始也可以通过主机网络直接暴露。
与其他 Ingress 控制器有什么不同
文档所说的“最大区别”,在于实现与 CNI 结合得有多紧密。一般的控制器以 Deployment 或 DaemonSet 单独运行,并通过 LoadBalancer 服务暴露。Cilium 的 Ingress 和 Gateway API 是网络栈的一部分,所以流量一到达服务端口,就会被 eBPF 代码拦截,再通过 TPROXY 交给 Envoy。结果是,CiliumNetworkPolicy 也适用于经由 ingress 进出的流量。
此时到达 Envoy 的流量,会在策略引擎中获得一个特殊的 ingress 身份。来自外部的流量通常是 world 身份,所以策略判定点有两处:从 world 进入 ingress 的阶段,以及从 ingress 流向后端身份的阶段。如果设置了默认拒绝策略后网关被阻断,说明这两个阶段中有一个没有放行。
内部动作分为两部分。Cilium operator 监视 Gateway API 资源,校验有效性并标记为 accepted,然后翻译成 CiliumEnvoyConfig。Cilium agent 读取该 CiliumEnvoyConfig,把配置交给内置 Envoy 或 Envoy DaemonSet。流量由 Envoy 处理。
没有 sidecar 的网格
每个节点一个 Envoy 的结构同样以相同方式适用于东西向(east-west)流量。GAMMA 是把 HTTPRoute 的 parent 设为 Service 而不是 Gateway 的方式,Cilium 会拦截流向该 Service 的 L7 流量,并路由到每个节点上的 Envoy。Cilium 目前只支持与 Service 位于同一命名空间的 producer Route。不使用 L7 的流量不经过 Envoy,直接走 eBPF 数据路径。与 sidecar 方式相比,代理数量取决于节点数而不是 Pod 数,而且无需重启应用 Pod 就能开启或关闭 L7 功能。
加密选项——WireGuard、IPsec、mTLS
Cilium 通过 IPsec、WireGuard 或 ztunnel(beta),对 Cilium 所管理的 endpoint 之间的流量进行透明加密。开启 WireGuard 后,每个节点的 agent 会与其他所有节点建立 WireGuard 隧道,每个节点生成一对密钥,并通过 CiliumNode 资源的 network.cilium.io/wg-pub-key 注解分发公钥。隧道端点为 UDP 51871,同一节点内的流量不加密(因为在节点上本来就能看到,加密没有收益)。内核必须支持 WireGuard,并用 encryption.enabled=true、encryption.type=wireguard 开启。IPsec 通过 Kubernetes Secret 分发密钥,从 1.18 开始在隧道封装之后再加密,连用于策略的身份也一并隐藏。
Mutual Authentication(beta)通过 SPIFFE/SPIRE 签发工作负载身份,在常规连接之外(out-of-band)完成 mTLS 握手。文档写明:要同时获得认证和机密性,必须开启加密(WireGuard 或 IPsec)。也就是说,Cilium 的 mTLS 是身份验证,真正的加密由节点之间的隧道负责。
在现场相遇的样子
从 NGINX Ingress 迁移到 Cilium Gateway API 的团队,最先遇到的是注解。文档指出,几乎不会原样迁移各实现专有的注解,请求和响应的处理、流量分割、基于请求头、查询参数和方法的路由,都要换成 Gateway API 的标准字段。推荐的顺序是:确定范围和阶段,对现有注解分类,创建含义相同的 Gateway API 资源并行验证,再逐步迁移流量,稳定之后删除 Ingress。
“开启 WireGuard 之后,最初几个数据包是以明文发出的”这样的报告也有。文档的已知问题条目说的正是这件事。在“目的地 IP 是远端 Cilium endpoint”这一信息传播之前,会被视为在集群之外而以明文发送。文档给出的应对办法是开启 encryption.strictMode,或用策略阻断流向 world 的 egress。
接下来的理论要看什么
紧接着的理论中,会看 Gateway 获得的 LoadBalancer IP 和 Pod CIDR 是如何被集群外部的路由器得知的,也就是 BGP Control Plane 和 Egress Gateway。参考:Gateway API Support,Traffic Splitting Example,Migrating from Ingress to Gateway,WireGuard Transparent Encryption,Mutual Authentication。