ᕕ( ᐛ )ᕗ Jimyag's Blog

使用 MetalLB 和 Envoy Gateway 实现多个域名对应多个公网 IP

· 1538 words · ~ 8 min read

Last modified:

本文介绍两种在 Kubernetes 中实现"多个域名对应多个公网 IP"的方案,均基于 MetalLB 和 Envoy Gateway,适用于裸金属或私有云环境。

目标

实现效果:

1
2
3
4
5
6
7
*.jimyagtest1.com A 192.0.2.10
*.jimyagtest1.com A 192.0.2.11
*.jimyagtest1.com A 192.0.2.12

*.jimyagtest2.com A 192.0.2.10
*.jimyagtest2.com A 192.0.2.11
*.jimyagtest2.com A 192.0.2.12

每个 IP 对外暴露 80/443。多个域名通过 DNS 多 A 记录同时指向这组 IP,客户端选择其中一个建连。

前置条件

  • 集群已安装 MetalLB,并配置包含目标 IP 的 IPAddressPool
  • 集群已安装 Envoy Gateway。
  • DNS 支持为同一域名配置多条 A 记录。
  • 目标 IP 在集群外部网络可达。

参考:MetalLB Configuration


方案一:多 Gateway,每 IP 一个 Envoy 实例

原理

每个公网 IP 对应一组独立资源:EnvoyProxy + Gateway + Envoy Gateway 自动生成的 LoadBalancer Service。多个域名作为 listener 聚合到每个 Gateway 上,每条 HTTPRoute 通过多个 parentRefs 同时绑定所有 Gateway。

架构

  flowchart TD
    Client["Client"]
    DNSA["DNS: *.jimyagtest1.com"]
    DNSB["DNS: *.jimyagtest2.com"]
    IP1["192.0.2.10"]
    IP2["192.0.2.11"]
    IP3["192.0.2.12"]

    SVC1["LoadBalancer Service\n192.0.2.10"]
    SVC2["LoadBalancer Service\n192.0.2.11"]
    SVC3["LoadBalancer Service\n192.0.2.12"]

    GW1["Gateway gw-192-0-2-10\nEnvoy 实例 A"]
    GW2["Gateway gw-192-0-2-11\nEnvoy 实例 B"]
    GW3["Gateway gw-192-0-2-12\nEnvoy 实例 C"]

    RouteA["HTTPRoute jimyagtest1-route\nparentRefs: GW1, GW2, GW3"]
    RouteB["HTTPRoute jimyagtest2-route\nparentRefs: GW1, GW2, GW3"]
    BackendA["Backend Service A"]
    BackendB["Backend Service B"]

    Client --> DNSA & DNSB
    DNSA & DNSB --> IP1 & IP2 & IP3
    IP1 --> SVC1 --> GW1
    IP2 --> SVC2 --> GW2
    IP3 --> SVC3 --> GW3
    GW1 & GW2 & GW3 --> RouteA --> BackendA
    GW1 & GW2 & GW3 --> RouteB --> BackendB

资源关系

每个 IP 一组(以三个 IP 为例):

资源 名称 说明
EnvoyProxy gw-192-0-2-10 控制 Envoy Service 的 IP 和 annotation
Gateway gw-192-0-2-10 引用对应 EnvoyProxy
Service (自动生成) LoadBalancer IP = 192.0.2.10

业务路由(所有 IP 共用):

资源 parentRefs
HTTPRoute jimyagtest1-route gw-192-0-2-10, gw-192-0-2-11, gw-192-0-2-12
HTTPRoute jimyagtest2-route gw-192-0-2-10, gw-192-0-2-11, gw-192-0-2-12

配置示例

EnvoyProxy(每个 IP 一个,以 192.0.2.10 为例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: gw-192-0-2-10
  namespace: gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        annotations:
          metallb.io/loadBalancerIPs: 192.0.2.10
        externalTrafficPolicy: Cluster

说明:新版 MetalLB 使用 metallb.io/* annotation;旧版集群需改为 metallb.universe.tf/*

Gateway(每个 IP 一个):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gw-192-0-2-10
  namespace: gateway-system
spec:
  gatewayClassName: envoy
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: gw-192-0-2-10
  addresses:
    - type: IPAddress
      value: 192.0.2.10
  listeners:
    - name: http-jimyagtest1
      hostname: "*.jimyagtest1.com"
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: https-jimyagtest1
      hostname: "*.jimyagtest1.com"
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: jimyagtest1-wildcard-tls
      allowedRoutes:
        namespaces:
          from: All
    - name: http-jimyagtest2
      hostname: "*.jimyagtest2.com"
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: https-jimyagtest2
      hostname: "*.jimyagtest2.com"
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: jimyagtest2-wildcard-tls
      allowedRoutes:
        namespaces:
          from: All

同理为 192.0.2.11192.0.2.12 各创建一组。参考:Envoy Gateway - Gateway Address

HTTPRoute(每个域名一条,绑定所有 Gateway):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: jimyagtest1-route
  namespace: app
  labels:
    gateway.example.com/ip-group: main
spec:
  parentRefs:
    - name: gw-192-0-2-10
      namespace: gateway-system
      sectionName: https-jimyagtest1
    - name: gw-192-0-2-11
      namespace: gateway-system
      sectionName: https-jimyagtest1
    - name: gw-192-0-2-12
      namespace: gateway-system
      sectionName: https-jimyagtest1
  hostnames:
    - app.jimyagtest1.com
  rules:
    - backendRefs:
        - name: jimyagtest1-svc
          port: 8080

jimyagtest2-route 结构相同,sectionName 改为 https-jimyagtest2

运维负担:parentRefs 批量更新

这是方案一的主要代价。每条 HTTPRoute 显式列出所有 Gateway,新增或删除一个 IP 时必须同步 patch 所有相关路由:

操作 步骤 patch 次数
新增 IP 创建 EnvoyProxy + Gateway → patch 所有 HTTPRoute 加一条 parentRef = HTTPRoute 数量
删除 IP patch 所有 HTTPRoute 删一条 parentRef → 删 Gateway + EnvoyProxy = HTTPRoute 数量

推荐将这组操作封装在管理层(控制器或 API),通过 label 批量查找需要更新的 HTTPRoute,避免人工逐条维护。

更新流程

新增 IP:

  sequenceDiagram
    participant Mgmt as 管理层
    participant K8s as Kubernetes API
    participant EG as Envoy Gateway
    participant ML as MetalLB
    participant DNS as DNS

    Mgmt->>K8s: Create EnvoyProxy for new IP
    Mgmt->>K8s: Create Gateway for new IP
    EG->>K8s: Reconcile Envoy data plane Service
    ML->>K8s: Assign LoadBalancer IP
    Mgmt->>K8s: List HTTPRoutes by label, patch all parentRefs
    Mgmt->>DNS: Add A record

删除 IP:

  1. 从 DNS 删除该 IP 的 A 记录。
  2. 等待至少一个 DNS TTL。
  3. 管理层通过 label 查出所有 HTTPRoute,批量删除对应 parentRef
  4. 删除对应 Gateway。
  5. 删除对应 EnvoyProxy。

方案一小结

优点:资源全部由 Envoy Gateway 控制器管辖,Gateway.status 准确反映外部 IP,行为可预期。

缺点:每个 IP 独立一套 Envoy 实例,CPU/内存冗余;新增/删除 IP 需要 O(n) 次 HTTPRoute patch。


方案二:单 Gateway + 手动 LoadBalancer Service

原理

只创建一个 Gateway 和一套 Envoy 数据面。将 EnvoyProxyenvoyService.type 设为 ClusterIP,Envoy Gateway 控制器只生成集群内部可达的 ClusterIP Service,不分配外部 IP。

为每个公网 IP 手动创建一个 type: LoadBalancer 的 Service,用 MetalLB annotation 绑定指定 IP,selector 指向 Envoy pods。所有外部流量经各自的 LoadBalancer Service 打到同一套 Envoy 数据面。

HTTPRoute 只引用这一个 Gateway,增删 IP 只操作 LoadBalancer Service,不涉及 HTTPRoute。

架构

  flowchart TD
    Client["Client"]
    DNSA["DNS: *.jimyagtest1.com"]
    DNSB["DNS: *.jimyagtest2.com"]
    IP1["192.0.2.10"]
    IP2["192.0.2.11"]
    IP3["192.0.2.12"]

    SVC1["LoadBalancer Service\n192.0.2.10(手动)"]
    SVC2["LoadBalancer Service\n192.0.2.11(手动)"]
    SVC3["LoadBalancer Service\n192.0.2.12(手动)"]

    GW["Gateway my-gateway\n单套 Envoy 实例"]

    RouteA["HTTPRoute jimyagtest1-route\nparentRefs: my-gateway"]
    RouteB["HTTPRoute jimyagtest2-route\nparentRefs: my-gateway"]
    BackendA["Backend Service A"]
    BackendB["Backend Service B"]

    Client --> DNSA & DNSB
    DNSA & DNSB --> IP1 & IP2 & IP3
    IP1 --> SVC1
    IP2 --> SVC2
    IP3 --> SVC3
    SVC1 & SVC2 & SVC3 --> GW
    GW --> RouteA --> BackendA
    GW --> RouteB --> BackendB

资源关系

控制器管理:

资源 名称 说明
EnvoyProxy my-proxy envoyService.type: ClusterIP
Gateway my-gateway 引用 my-proxy
Service (自动生成) ClusterIP,仅集群内可达
Deployment envoy-my-gateway Envoy 数据面

手动管理(每个 IP 一个):

资源 名称 说明
Service envoy-ip-192-0-2-10 type: LoadBalancer,selector → Envoy pods
Service envoy-ip-192-0-2-11 同上
Service envoy-ip-192-0-2-12 同上

业务路由(只引用一个 Gateway):

资源 parentRefs
HTTPRoute jimyagtest1-route my-gateway
HTTPRoute jimyagtest2-route my-gateway

Envoy Pod 的 Selector Labels

Envoy Gateway 生成的 Envoy pods 上带有以下标签,手动 Service 的 selector 使用这两个:

1
2
gateway.envoyproxy.io/owning-gateway-namespace: <gateway-namespace>
gateway.envoyproxy.io/owning-gateway-name: <gateway-name>

可以通过查看控制器生成的 ClusterIP Service 确认实际 selector 和 targetPort:

1
kubectl get svc -n gateway-system -o yaml | grep -A 10 selector

配置示例

EnvoyProxy(关闭控制器自动分配外部 IP):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: my-proxy
  namespace: gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: ClusterIP

Gateway

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
  namespace: gateway-system
spec:
  gatewayClassName: envoy
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: my-proxy
  listeners:
    - name: http-jimyagtest1
      hostname: "*.jimyagtest1.com"
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: https-jimyagtest1
      hostname: "*.jimyagtest1.com"
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: jimyagtest1-wildcard-tls
      allowedRoutes:
        namespaces:
          from: All
    - name: http-jimyagtest2
      hostname: "*.jimyagtest2.com"
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: https-jimyagtest2
      hostname: "*.jimyagtest2.com"
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: jimyagtest2-wildcard-tls
      allowedRoutes:
        namespaces:
          from: All

手动 LoadBalancer Service(每个 IP 一个,targetPort 需与实际 Envoy 容器端口对齐):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
  name: envoy-ip-192-0-2-10
  namespace: gateway-system
  annotations:
    metallb.io/loadBalancerIPs: 192.0.2.10
spec:
  type: LoadBalancer
  externalTrafficPolicy: Cluster
  selector:
    gateway.envoyproxy.io/owning-gateway-namespace: gateway-system
    gateway.envoyproxy.io/owning-gateway-name: my-gateway
  ports:
    - name: http
      port: 80
      targetPort: 80   # 以控制器生成的 ClusterIP Service 中的 targetPort 为准
    - name: https
      port: 443
      targetPort: 443

192.0.2.11192.0.2.12 各创建一个相同结构的 Service。

HTTPRoute(只引用一个 Gateway):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: jimyagtest1-route
  namespace: app
spec:
  parentRefs:
    - name: my-gateway
      namespace: gateway-system
      sectionName: https-jimyagtest1
  hostnames:
    - app.jimyagtest1.com
  rules:
    - backendRefs:
        - name: jimyagtest1-svc
          port: 8080

潜在问题

升级时静默失效(最高风险)

Envoy Gateway 升级后若变更了 pod label scheme 或容器端口,手动 Service 的 selector 或 targetPort 不再匹配,流量静默丢弃,没有控制器告警。每次升级 Envoy Gateway 后需要人工验证 selector 和 targetPort 是否仍然正确。

新增 listener 不自动同步

Gateway 新增 listener port 后,手动 Service 的 ports 不会自动跟进,需要同步 patch 所有 LoadBalancer Service。漏掉则该端口流量不通,且不报错。

Gateway status 不反映外部 IP

Gateway.status.addresses 只显示 ClusterIP,不包含手动 Service 的外部 IP。依赖 Gateway status 的工具(external-dns、告警)会读到错误结论。

生命周期解绑

删除 Gateway 时,控制器自动清理 ClusterIP Service 和 Envoy Deployment,但手动创建的 LoadBalancer Service 不受控制器管理,会孤立留在集群中,需要手动清理。

externalTrafficPolicy 影响源 IP

使用 Cluster 模式时 Envoy 看到的是 SNAT 后的 IP。要保留真实客户端 IP 需要改用 Local,但 Local 模式只有运行 Envoy pod 的节点接收流量,需要确认节点分布和故障切换行为。

更新流程

新增 IP:

  sequenceDiagram
    participant Mgmt as 管理层
    participant K8s as Kubernetes API
    participant ML as MetalLB
    participant DNS as DNS

    Mgmt->>K8s: Create LoadBalancer Service for new IP
    ML->>K8s: Assign LoadBalancer IP
    Mgmt->>DNS: Add A record

删除 IP:

  1. 从 DNS 删除该 IP 的 A 记录。
  2. 等待至少一个 DNS TTL。
  3. 删除对应的 LoadBalancer Service。

方案二小结

优点:一套 Envoy 实例,没有冗余部署;HTTPRoute 只引用一个 Gateway,增删 IP 不涉及 HTTPRoute。

缺点:手动 Service 游离于控制器管辖之外,升级时存在静默失效风险,Gateway status 不准确,生命周期需要自行维护。


方案对比

维度 方案一(多 Gateway) 方案二(单 Gateway)
Envoy 实例数 N 个(每 IP 一个) 1 个
HTTPRoute parentRefs N 条(引用所有 Gateway) 1 条
增删 IP 操作范围 EnvoyProxy + Gateway + patch 所有 HTTPRoute 1 个 LoadBalancer Service
新增 listener 影响 只改 Gateway 定义 需同步 patch 所有手动 Service
控制器可见性 全部受控 手动 Service 不受控
Gateway status 准确性 准确 仅显示 ClusterIP
升级风险 中(selector/targetPort 可能变)
适用场景 优先考虑,稳定可预期 资源受限或要求单 Envoy 实例时

社区现状

envoyproxy/gateway#7892 提议在 KubernetesProxyProvider 里增加 envoyServices 字段,允许单个 Gateway 原生对应多个 LoadBalancer IP,或由控制器在 spec.addresses 包含多个 IP 时自动拆分创建 Service。如果该特性落地,方案二的手动部分可以被控制器接管,两个方案的缺点都会消失。目前该 issue 状态为 open + stale,尚未开发。


关键点补充

为什么不是一个 Service 配多个 IPv4

MetalLB 的 metallb.io/loadBalancerIPs 支持逗号分隔,但主要用于 dual-stack 场景。“一个域名负载到多个 IPv4"的正确模型是多个 Service 各持一个 IP,再通过 DNS 多 A 记录分散。参考:MetalLB Usage - IPv6 and dual stack services

IP sharing

如果需要让其他 LoadBalancer Service 与 Envoy Service 共享同一个 IP,需满足 MetalLB IP sharing 条件:sharing key 相同、端口不冲突、都使用 externalTrafficPolicy: Cluster 或 selector 完全相同。参考:MetalLB Usage - IP address sharing


验证命令

查看 LoadBalancer Service 是否拿到指定 IP:

1
kubectl get svc -A | grep LoadBalancer

查看 Gateway 是否 Accepted:

1
2
3
4
# 方案一
kubectl get gateway -n gateway-system gw-192-0-2-10 -o yaml
# 方案二
kubectl get gateway -n gateway-system my-gateway -o yaml

查看 HTTPRoute 状态:

1
kubectl get httproute -n app jimyagtest1-route -o yaml

指定 IP 测试连通性:

1
2
3
4
for ip in 192.0.2.10 192.0.2.11 192.0.2.12; do
  curl -kfsS --resolve app.jimyagtest1.com:443:${ip} https://app.jimyagtest1.com/
  curl -kfsS --resolve app.jimyagtest2.com:443:${ip} https://app.jimyagtest2.com/
done

查看 DNS 返回:

1
2
dig +short app.jimyagtest1.com
dig +short app.jimyagtest2.com

#Kubernetes #MetalLB #Envoy Gateway #Gateway API #网络 #DNS