5 minute read

Kubernetes 外部流量接入实战:MetalLB + Envoy Gateway + Gateway API

前言

在 Kubernetes 中,如何让集群外部的流量访问到集群内的服务,是一个核心问题。

云环境(AWS、阿里云、腾讯云):创建 LoadBalancer 类型的 Service,云厂商自动创建 LB 并分配公网 IP,开箱即用。

裸金属 / 自建集群(包括 Kind):没有云厂商的 LB 支持,type: LoadBalancer 的 Service 永远卡在 Pending。需要自己搭建流量入口。

而这个需求,恰好引出了 Kubernetes 生态中两个重要组件:

  • MetalLB — 裸金属负载均衡器,为 LoadBalancer Service 分配 IP
  • Gateway API — Kubernetes 官方推出的下一代流量管理 API(Ingress 的继任者)

本文将完整记录如何搭建一套生产可用的流量接入方案:

  1. 部署 MetalLB,让 LoadBalancer 类型 Service 能分配到 IP
  2. 部署 Envoy Gateway(Gateway API 的官方参考实现)
  3. 通过 Gateway API 的 HTTPRoute 将流量路由到后端服务
  4. 宿主机端口转发,打通外部访问

这套方案不只是 Kind 玩具,裸金属 K8s 集群、边缘计算、本地机房,都在用同样的架构。


环境说明

项目 配置
服务器 腾讯云轻量云,40GB 磁盘
OS OpenCloudOS 9.4
Kind 集群 5 节点(1 control-plane + 4 worker→后缩减为 2 节点)
K8s 版本 v1.33.1
Kind 版本 v0.29.0
Docker 网络 172.21.0.0/16

第一章:裸金属集群的网络困境

云环境 vs 裸金属

在云环境(AWS EKS、阿里云 ACK、腾讯云 TKE)中,创建一个 LoadBalancer Service 时:

apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: LoadBalancer  # 云厂商自动创建 LB

云厂商的控制器会自动创建负载均衡器、分配公网 IP、配置健康检查,一切自动完成。

但在裸金属或自建集群中,没有云厂商 API 可以用。type: LoadBalancer 的 Service 永远卡在 <pending> 状态。

网络隔离问题

以 Kind 集群为例,实际生产裸金属集群的原理类似:

外部机器(笔记本)
    │
    ├─ 宿主机网卡 (81.69.8.26) ← 真实物理 IP
    │   │
    │   ├─ 容器/VM 网络: 172.21.0.0/16
    │   │   ├─ K8s 节点 (172.21.0.2 ~ 172.21.0.6)
    │   │   │   ├─ Pod IP 段: 10.244.0.0/16
    │   │   │   │   └─ 应用 Pod (10.244.1.2:8080)
    │   │   │   └─ Service ClusterIP: 10.96.0.0/16
    │   │   └─ 这些 IP 段外部无法直接访问
    │   │
    │   └─ 对外 IP

有三个隔离的 IP 段:

IP 段 谁在用 谁能访问
10.244.0.0/16 Pod 仅集群内部
10.96.0.0/16 Service ClusterIP 仅集群内部
172.21.0.0/16 节点 / MetalLB 宿主机 + 内部网络
81.69.8.26 宿主机网卡 外部互联网

裸金属生产环境也是类似的:服务器有物理网卡 IP,Pod 在独立的网络命名空间里,外部不能直接访问。

解决方案架构

打通外部到 Pod 的访问,需要四层组件:

外部请求
  ↓
① 宿主机端口转发(Nginx / iptables DNAT / 物理 LB)
  ↓  宿主机 80 端口 → MetalLB IP
② MetalLB(裸金属 LB)
  ↓  将 IP 分配给 LoadBalancer Service
③ Envoy Gateway(Gateway API 控制器)
  ↓  解析 HTTPRoute 路由规则
④ 后端 Service(ClusterIP)
  ↓
⑤ Pod

第二章:部署 MetalLB

MetalLB 是什么?

MetalLB 是一个裸金属 Kubernetes 的负载均衡器实现。它让你在没有云厂商 LB 的环境下,也能给 Service 分配 External IP。

它有两种工作模式:

  • Layer2 模式:用 ARP/NDP 协议宣告 IP,节点间故障转移
  • BGP 模式:通过 BGP 协议与路由器交换路由

本文将使用 Layer2 模式。

安装 MetalLB

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

⚠️ 国内网络问题: 如果拉取 quay.io 镜像慢,可以先把镜像拉到宿主机,再用 kind load docker-image 导入到集群节点:

docker pull quay.io/metallb/controller:v0.14.9
docker pull quay.io/metallb/speaker:v0.14.9
kind --name my-5-node-cluster load docker-image quay.io/metallb/controller:v0.14.9 quay.io/metallb/speaker:v0.14.9

配置 IP 地址池

MetalLB 需要知道哪些 IP 可以分配。这些 IP 必须在 Docker 桥接网络所在的子网内。

先查看 Docker 网络信息:

docker network inspect kind --format ' - '

输出:172.21.0.0/16 - 172.21.0.1

选择一个不与其他容器冲突的地址段,创建 IPAddressPool 和 L2Advertisement:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: my-pool
  namespace: metallb-system
spec:
  addresses:
  - 172.21.0.100-172.21.0.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: my-l2-advertisement
  namespace: metallb-system
spec:
  ipAddressPools:
  - my-pool

验证 MetalLB 安装

kubectl get pods -n metallb-system

所有 Pod 状态为 Running 即表示安装成功。

给 Service 分配 IP

将一个已有的 Service 改为 LoadBalancer 类型:

kubectl patch svc test-service -p '{"spec":{"type":"LoadBalancer"}}'

查看 External-IP:

kubectl get svc test-service
# NAME           TYPE           CLUSTER-IP    EXTERNAL-IP
# test-service   LoadBalancer   10.96.79.78   172.21.0.100

MetalLB 从池中自动分配了 172.21.0.100 给这个 Service。


第三章:部署 Envoy Gateway

为什么选 Envoy Gateway?

Kubernetes 官方已宣布 Ingress 进入冻结状态,不再增加新功能。Gateway API 是其正式继任者,提供了:

  • 更精细的路由规则(按 Header、权重等)
  • 跨命名空间路由
  • 面向角色设计的资源模型(基础设施提供者、集群运维、应用开发者)

Envoy Gateway 是 Gateway API 的官方参考实现,由 Envoy 核心团队维护。

安装 Gateway API CRD

curl -sL -o /tmp/gateway-crd.yaml https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
kubectl apply -f /tmp/gateway-crd.yaml

安装了以下 CRD:

  • gatewayclasses.gateway.networking.k8s.io
  • gateways.gateway.networking.k8s.io
  • httproutes.gateway.networking.k8s.io
  • grpcroutes.gateway.networking.k8s.io
  • referencegrants.gateway.networking.k8s.io

安装 Envoy Gateway

curl -sL -o /tmp/envoy-gateway-install.yaml https://github.com/envoyproxy/gateway/releases/download/v1.3.0/install.yaml
kubectl apply -f /tmp/envoy-gateway-install.yaml

⚠️ 镜像拉取问题: 同样需要手动拉镜像并导入 Kind 节点:

docker pull envoyproxy/gateway:v1.3.0
docker pull envoyproxy/envoy:distroless-v1.33.0
kind --name my-5-node-cluster load docker-image envoyproxy/gateway:v1.3.0
kind --name my-5-node-cluster load docker-image envoyproxy/envoy:distroless-v1.33.0

了解 Gateway API 三层模型

Gateway API 引入了三个核心角色:

  1. GatewayClass — 定义网关的类型(类似 StorageClass),由基础设施提供者创建
  2. Gateway — 网关实例的声明,指定监听端口和协议,由集群运维创建
  3. HTTPRoute — 定义路由规则,将流量导向后端 Service,由应用开发者创建

第四章:创建 Gateway API 资源

创建 GatewayClass

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller

controllerName 告诉 Kubernetes 哪个控制器应当处理这个 GatewayClass 的实例。

创建 Gateway

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
  namespace: default
spec:
  gatewayClassName: eg
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All

创建 Gateway 后,Envoy Gateway 会自动做两件事:

  1. 创建 Envoy Proxy Pod — 一个运行 Envoy 的数据面代理
  2. 创建 LoadBalancer Service — MetalLB 分配 External IP

查看结果:

kubectl get gateway my-gateway -o wide
# NAME         CLASS   ADDRESS        PROGRAMMED   AGE
# my-gateway   eg      172.21.0.101   True         25m

kubectl get svc -n envoy-gateway-system
# NAME                                TYPE           CLUSTER-IP     EXTERNAL-IP
# envoy-default-my-gateway-xxxxx      LoadBalancer   10.96.245.18   172.21.0.101

创建 HTTPRoute

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: test-app-route
  namespace: default
spec:
  parentRefs:
  - name: my-gateway
  rules:
  - backendRefs:
    - name: test-service
      port: 80

关键字段说明:

  • parentRefs — 关联到哪个 Gateway
  • backendRefs — 转发到哪个 Service(不需要 LoadBalancer 类型,ClusterIP 即可)

测试内部连通性

在宿主机上 curl MetalLB 分配的 IP:

curl http://172.21.0.101
# <html><body><h1>tenten</h1></body></html>

第五章:宿主机反向代理(打通外部访问)

为什么还需要这一步?

MetalLB 分配的 IP(如 172.21.0.101)在 Docker 内部网络,外部机器无法直接访问。我们需要在宿主机上运行 Nginx,将宿主机端口转发到 Gateway 的 IP。

用 Docker 运行 Nginx:

docker run -d --name nginx-gateway --restart=always --network kind -p 80:80 nginx:alpine

配置 Nginx 反向代理:

server {
    listen 80;
    location / {
        proxy_pass http://172.21.0.101:80;
    }
}

为什么 Nginx 容器要用 --network kind 这样才能直接访问 Docker 桥接网络内的 IP(即 MetalLB 分配的地址)。

开放防火墙

腾讯云安全组和宿主机防火墙都需要开放对应端口:

firewall-cmd --add-port=80/tcp --permanent
firewall-cmd --reload

然后在腾讯云控制台安全组加一条入站规则:TCP 80 端口。

最终验证

# 从宿主机测试
curl http://127.0.0.1:80
# <html><body><h1>tenten</h1></body></html>

# 从外部机器访问
curl http://81.69.8.26:80
# <html><body><h1>tenten</h1></body></html>

第六章:完整架构图

                         ┌─────────────────────────┐
                         │  你的电脑                 │
                         │  http://81.69.8.26       │
                         └─────────────┬───────────┘
                                       │
                         ┌─────────────▼───────────┐
                         │  腾讯云安全组(80 端口)  │
                         └─────────────┬───────────┘
                                       │
                         ┌─────────────▼───────────┐
                         │  宿主机 firewalld(80)   │
                         └─────────────┬───────────┘
                                       │
                         ┌─────────────▼───────────┐
                         │  Nginx 容器 (port 80)    │
                         │  proxy_pass ↓           │
                         └─────────────┬───────────┘
                                       │
                         ┌─────────────▼───────────┐
                         │  Envoy Gateway           │
                         │  172.21.0.101:80         │
                         │  (MetalLB 分配)          │
                         └─────────────┬───────────┘
                                       │
                    ┌──────────────────┼──────────────────┐
                    │                  │                  │
         ┌──────────▼─────────┐  ┌────▼─────────┐  ┌────▼─────────┐
         │  HTTPRoute         │  │  HTTPRoute   │  │  HTTPRoute   │
         │  test-app-route    │  │  my-app-route│  │  (你的新服务) │
         └──────────┬─────────┘  └──────┬───────┘  └──────┬───────┘
                    │                   │                  │
         ┌──────────▼─────────┐  ┌──────▼───────┐  ┌──────▼───────┐
         │  test-service      │  │  my-app-svc  │  │  新 Service   │
         │  (ClusterIP)       │  │  (ClusterIP) │  │  (ClusterIP)  │
         └────────────────────┘  └──────────────┘  └──────────────┘

第七章:如何添加新服务

以后每加一个新应用,只需要三步:

步骤 1:部署应用

kubectl create deployment my-app --image=my-image
kubectl expose deployment my-app --port=80 --name=my-app-svc

注意:不需要 --type=LoadBalancer,默认 ClusterIP 即可。

步骤 2:创建 HTTPRoute

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app-route
spec:
  parentRefs:
  - name: my-gateway
  rules:
  - backendRefs:
    - name: my-app-svc
      port: 80
EOF

步骤 3:验证

curl http://81.69.8.26

不需要修改 Nginx,不需要改防火墙,不需要碰 MetalLB 配置 —— Envoy Gateway 会自动热加载路由规则。


踩坑记录

1. 国内网络慢

所有 Docker 镜像(quay.io, Docker Hub, GitHub raw content)在国内下载都很慢。解决方式:

  • 使用中科大 USTC 镜像源
  • 先用 docker pull 拉到宿主机,再用 kind load docker-image 导入集群
  • 部分容器设置 imagePullPolicy: IfNotPresent

2. 磁盘空间不足

40GB 磁盘 + 5 节点 Kind 集群非常紧张。Kind 节点使用 Docker overlayfs,每个节点都有独立的镜像存储。

解决方案:

  • 缩减 worker 节点数量(5→2)
  • 定期清理 Docker 未使用的镜像和卷
  • 清理历史日志

3. Firewalld 重置 iptables 规则

firewall-cmd --reload 会清空手工添加的 iptables DNAT 规则。替代方案:

  • 使用 firewalld 的富规则或端口转发功能
  • 或直接用 Nginx/Docker 暴露端口(推荐)

4. Docker 重启需要恢复网络

重启 Docker 守护进程后:

  • Kind 节点容器需要重新启动
  • 需要等待 Kubernetes 组件恢复(约 30 秒)
  • Nginx 容器可能需要重新启动

附录:常用命令速查

# MetalLB 相关
kubectl get pods -n metallb-system
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system

# Gateway API 相关
kubectl get gatewayclass
kubectl get gateway
kubectl get httproute
kubectl get svc -n envoy-gateway-system

# 调试
kubectl logs -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-namespace=default
curl -v http://172.21.0.101

# 宿主机 Nginx
docker logs nginx-gateway
docker exec nginx-gateway nginx -s reload

总结

架构总览

外部请求 → 宿主机端口转发 → MetalLB → Envoy Gateway → HTTPRoute → Service → Pod
                                                      ↑
                                              Gateway API 标准

各组件定位

组件 作用 生产替代方案
MetalLB 裸金属 LB,分配 External IP 同方案,或物理 F5 / HAProxy
Envoy Gateway Gateway API 控制器,路由流量 Istio、Contour、Kong 均可
Gateway API CRDs 声明式路由规则(GatewayClass → Gateway → HTTPRoute) 标准 API,厂商无关
Nginx 宿主机端口转发 物理 LB、iptables DNAT

这套方案的优势

  • 标准化 — Gateway API 是 Kubernetes 官方标准,Ingress 的正式继任者
  • 厂商无关 — 不管用 Envoy Gateway、Istio 还是 Contour,上层的 HTTPRoute 资源写法完全一样
  • 声明式 — 加新服务只需 kubectl apply -f httproute.yaml,不改任何基础设施
  • 无侵入 — 所有流量管理在集群内完成,宿主机只需一个端口转发
  • 多维度路由 — 支持按域名、路径前缀、Header、权重等多维度分发

这套方案用在哪些地方

这套架构不是 Kind 的玩具方案,而是生产环境的通用模式

  • 裸金属机房 — 物理服务器上自建 K8s,用 MetalLB + Gateway API 做流量入口
  • 边缘计算 — 无云环境的边缘节点,用同样的方案暴露服务
  • 混合云 — 部分在云上、部分在本地,统一使用 Gateway API 管理路由
  • 开发测试 — Kind 集群完全模拟生产环境的流量路径,本地验证后再上线

Categories:

Updated: