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

## 目标

实现效果：

```text
*.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](https://metallb.io/configuration/)。

---

## 方案一：多 Gateway，每 IP 一个 Envoy 实例

### 原理

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

### 架构

```mermaid
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` 为例）：

```yaml
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 一个）：

```yaml
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.11`、`192.0.2.12` 各创建一组。参考：[Envoy Gateway - Gateway Address](https://gateway.envoyproxy.io/docs/tasks/traffic/gateway-address/)。

**HTTPRoute**（每个域名一条，绑定所有 Gateway）：

```yaml
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：**

```mermaid
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 数据面。将 `EnvoyProxy` 的 `envoyService.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。

### 架构

```mermaid
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 使用这两个：

```text
gateway.envoyproxy.io/owning-gateway-namespace: <gateway-namespace>
gateway.envoyproxy.io/owning-gateway-name: <gateway-name>
```

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

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

### 配置示例

**EnvoyProxy**（关闭控制器自动分配外部 IP）：

```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: my-proxy
  namespace: gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: ClusterIP
```

**Gateway**：

```yaml
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 容器端口对齐）：

```yaml
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.11`、`192.0.2.12` 各创建一个相同结构的 Service。

**HTTPRoute**（只引用一个 Gateway）：

```yaml
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：**

```mermaid
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](https://github.com/envoyproxy/gateway/issues/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](https://metallb.universe.tf/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](https://metallb.universe.tf/usage/#ip-address-sharing)。

---

## 验证命令

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

```bash
kubectl get svc -A | grep LoadBalancer
```

查看 Gateway 是否 Accepted：

```bash
# 方案一
kubectl get gateway -n gateway-system gw-192-0-2-10 -o yaml
# 方案二
kubectl get gateway -n gateway-system my-gateway -o yaml
```

查看 HTTPRoute 状态：

```bash
kubectl get httproute -n app jimyagtest1-route -o yaml
```

指定 IP 测试连通性：

```bash
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 返回：

```bash
dig +short app.jimyagtest1.com
dig +short app.jimyagtest2.com
```

