一直以來,我們的鐵人賽系列從最底層的 Pod Level 一路分析到 Service Level,範圍逐步向外擴張,從單一節點內部的網路互動,走向整個 Cluster 內的服務溝通。
今天這篇,我想帶大家繼續往「更外層」走一步 — 進入 Gateway API 的世界。
Gateway API 是 Kubernetes 用來取代傳統 Ingress 的新一代標準,它不只是名稱不同,而是徹底重構了整個 L7 流量管理的邏輯。
比較吸引我的是 Gateway API 引入了 Role-oriented 的概念,讓 Infra 團隊和開發團隊能清楚分工,感覺就很像以前我們在寫 Ingress 現在被拆解成多個 YAML 去寫。
這篇文章我們會從安裝開始,實際在 Cilium 環境中啟用 Gateway API,並體驗它在 North-South 流量(進出 Cluster 的流量)上的行為。
後面我們還會試著用 HTTPRoute 來實作 流量分流(Traffic Splitting),體驗看看 Gateway API 與 Cilium Envoy 整合後能做到的 L7 控制能力。

圖片取自:https://cilium.io/use-cases/gateway-api/
安裝 Gateway API CRD
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gateways.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_grpcroutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
安裝好之後,檢查 CRD 安裝成功:
$ kubectl get crd | grep networking.k8s.io
gatewayclasses.gateway.networking.k8s.io     2025-10-05T15:03:25Z
gateways.gateway.networking.k8s.io           2025-10-05T15:03:25Z
grpcroutes.gateway.networking.k8s.io         2025-10-05T15:03:26Z
httproutes.gateway.networking.k8s.io         2025-10-05T15:03:26Z
referencegrants.gateway.networking.k8s.io    2025-10-05T15:03:26Z
tlsroutes.gateway.networking.k8s.io          2025-10-05T15:03:27Z
然後你的 Cilium 有幾個設定要配置好,分別是:
kubeProxyReplacement=true
l7Proxy=true
開 l7proxy,但其實預設就是打開的:
$ helm get manifest -n kube-system cilium | grep -i l7
  # Enables L7 proxy for L7 policy enforcement and visibility
  enable-l7-proxy: "true"
$ helm upgrade cilium cilium/cilium --version 1.18.1 \
    --namespace kube-system \
    --reuse-values \
    --set l7Proxy=true \
    --set kubeProxyReplacement=true
$ kubectl -n kube-system rollout restart deployment/cilium-operator
$ kubectl -n kube-system rollout restart ds/cilium
至於 kubeProxyReplacement=true ,我們鐵人賽系列文本來右有開啟
假如讀者真的沒開啟,請執行以下指令:
原本沒啟用
kubeProxyReplacement的話,千萬不要在 Production 環境照做下方指令將其開啟,會停機!
$ helm upgrade cilium cilium/cilium --version 1.18.1 \
    --namespace kube-system \
    --reuse-values \
    --set l7Proxy=true \
    --set kubeProxyReplacement=true
$ kubectl -n kube-system rollout restart deployment/cilium-operator
$ kubectl -n kube-system rollout restart ds/cilium
如果上面的 Prerequisites 都已滿足,那請執行以下指令:
helm upgrade cilium cilium/cilium --version 1.18.1 \
  --namespace kube-system \
  --reuse-values \
  --set gatewayAPI.enabled=true
執行完之後,重啟 cilium-agent 和 cilium-operator:
$ kubectl -n kube-system rollout restart ds/cilium
$ kubectl -n kube-system rollout restart deployment/cilium-operator
啟用 GatewatAPI 之後,可以執行以下指令,會看到有 cilium GatewayClass 已經被自動創建:
$ kubectl get gatewayclass
NAME     CONTROLLER                     ACCEPTED   AGE
cilium   io.cilium/gateway-controller   True       22h
在剛才啟用了 Cilium Gateway API 後
我們可以 apply 一個很簡單的 Gateway 看看:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: cilium-gw
  namespace: default
spec:
  gatewayClassName: cilium
  listeners:
    - name: http
      protocol: HTTP
      port: 80
apply 後,我們現在來看看 K8s Cluster 裡面的 Gateway:
$ kubectl get gtw
NAME        CLASS    ADDRESS   PROGRAMMED   AGE
cilium-gw   cilium             True         21h
當 Gateway 被創建後,背後的具體實例是需要一個真實存在的 LoadBalancer,所以我們現在來看一下 Service 有哪些:
$ kubectl get svc
NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
bookstore                  ClusterIP      10.103.95.177    <none>        8080/TCP       23h
bookstore-v1               ClusterIP      10.103.64.148    <none>        8080/TCP       22h
bookstore-v2               ClusterIP      10.104.158.193   <none>        8080/TCP       22h
cilium-gateway-cilium-gw   LoadBalancer   10.104.14.27     <pending>     80:31917/TCP   21h
kubernetes                 ClusterIP      10.96.0.1        <none>        443/TCP        21d
nginx-service              ClusterIP      10.104.210.239   <none>        80/TCP         3d
注意到了嗎?在創建 Gateway cilium-gw 後,有一個 LoadBalancer Type 的 Service cilium-gateway-cilium-gw 被自動創建出來了,但是他還沒有 External IP,上面顯示 <pending>,原因是我們這裡沒有 Cloud Provier 的 Controller 去幫我們建立對應的 LoadBalancer (例如:AWS NLB)。
以我們目前使用 kubeadm 自建的 K8s Cluster,我們可以安裝 MetalLB:
$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml
$ kubectl -n metallb-system wait --for=condition=available deploy/controller --timeout=120s
接著要提供 MetalLB IP CIDR,讓 MetalLB 可以幫我們為 LoadBalancer 分配 IP,下方實際 addresses 請依照你自己 Node 所處 AWS VPC Subnet CIDR 決定:
cat <<'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: metallb-ip-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.13.1.200-10.13.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: metallb-l2-adv
  namespace: metallb-system
spec:
  ipAddressPools:
  - lan-pool
EOF
以上面為例,我 Node 所處 Subnet CIDR 是 10.13.1.0/24,所以我拿 10.13.1.200-10.13.1.250 給 MetalLB 使用。
apply 後,我們接著再次查看 cilium-gateway-cilium-gw Service:
$ kubectl get svc cilium-gateway-cilium-gw
NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
cilium-gateway-cilium-gw   LoadBalancer   10.104.14.27   10.13.1.200   80:31917/TCP   22h
可以注意到 EXTERNAL-IP 從之前的 <pending> 變成 10.13.1.200 ,這正是 MetalLB 幫我們分配的!
太好了,前面我們光是在「安裝」就花了不少篇幅在講解,現在終於把 Cilium Gateway API 啟用,也安裝了 MetalLB,並看到 LoadBalancer svc 已經可以正常被分配到 External IP。
接下來我們來講講,Cilium Gateway API 和傳統 Ingress Controller 的差異到底在哪裡。
先從傳統架構說起。像 NGINX Ingress Controller 或 AWS Load Balancer Controller,
這些 Controller 通常是以一個 Deployment(或 DaemonSet) 的形式跑在 Cluster 裡面,
並搭配一個 Service(通常是 type=LoadBalancer)對外暴露。
整個流量路徑大致如下:
Client -> LoadBalancer -> Ingress Pod (例如 NGINX) -> Service
在這種架構裡:
這種設計本質上是「Kubernetes 上跑的一個 user-space proxy」。

圖片取自:https://cilium.io/use-cases/gateway-api/
Cilium 的 Gateway API 實作方式完全不同。
它不是在 Cluster 裡再跑一個 Deployment 來代理流量,
而是直接在 CNI 層整合 Gateway 能力,讓整個 Gateway 功能變成 Cilium datapath 的一部分。
我們可以先看一下目前的 Deployment 和 DaemonSet:
kubectl -n kube-system get deploy,ds | grep envoy
你會發現:
cilium-envoy DaemonSet,這是每個 Node 上的本地 Envoy。Cilium 的流量路徑大致如下:
Client
-> LoadBalancer / NodePort
-> eBPF hook -> TPROXY -> per-node Envoy (DaemonSet)
-> Envoy 做 L7 routing (HTTPRoute/GRPCRoute)
-> Backend Pod
大部分應用都會需要知道發送請求的用戶 IP,例如:做防火牆、區域/國家分析、或是 rate limit。
在 Cilium 的 Gateway / Ingress 模式下,流量都會先經過 Envoy Proxy,那這樣後端 Pod 看到的 source IP 就不會是原本 client 的 IP,而是 Envoy 的 IP(通常是同 Node 上的 DaemonSet Pod IP):
那你要真的想要看到真實 Client IP 怎辦?Cilium 的 Envoy 會自動在 HTTP Header 裡幫你補上:
X-Forwarded-For
X-Envoy-External-Address
也就是說你不用特別設定啥,你在後端應用裡正常去拿 X-Forwarded-For 最左邊的 IP 就能拿到真實的 client IP。
Cilium 的 Gateway API 功能背後主要有兩個元件在運作:
這兩個角色加起來,決定你的 Gateway / HTTPRoute / GRPCRoute 最後怎麼被 Envoy 實際執行。
我們假設我們創建以下資源:
kubectl apply -f my-gateway.yaml
kubectl apply -f my-route.yaml
Kubernetes API Server 會把這些物件寫進 etcd。
Cilium Operator 會:
Accepted=True
然後 operator 會把這些 Gateway API 的設定「翻譯」成 Cilium 自家的內部格式,叫作 CiliumEnvoyConfig (CEC)
想像成 operator 幫你把 YAML 規格轉成 Envoy 能吃的設定。
每個 Node 上都有一個 Cilium Agent,它會監聽這些新的 CiliumEnvoyConfig(CEC)資源。
當 agent 偵測到新的設定:
流量真正進來(不管是北南向 Gateway 還是東西向 GAMMA)時,就由這些 Envoy 根據設定處理,
像是:
總結上面的內容,你可以把這整個機制想成:
K8s Gateway API YAML
       │
       ▼
Cilium Operator(解析+驗證)
       │
       ▼
CiliumEnvoyConfig (CEC)
       │
       ▼
Cilium Agent(下發設定)
       │
       ▼
Envoy(真正處理 HTTP / gRPC 流量)
昨天 (Day 22) 我們已經在 EnvoyConfig 完了流量分流,今天我們試著在 Gateway API 也嘗試實作流量分流 (流量分流),關於要部署 Application 我將參考這篇 Isovalent 的這篇部落格所提供的範例,此部落格裡面還示範了很多 Gateway API 的用法,像是 **TLS Termination 或是 HTTP Request Header Modification**,推薦讀者還有空閒時間可以去嘗試玩玩看
我們先來部署我們的 echo servers,我們會部署 echo-1 和 echo-2 當作我們的 Application
$ kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/main/echo-servers.yml
service/echo-1 created
deployment.apps/echo-1 created
service/echo-2 created
deployment.apps/echo-2 created
接著來建立 Gateway my-example-gateway 和 HTTPRoute example-route-1:
$ kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/main/gateway.yaml
gateway.gateway.networking.k8s.io/my-example-gateway created
$ kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/refs/heads/main/httpRoute.yml
httproute.gateway.networking.k8s.io/example-route-1 created
可以看一下現在配置的 HTTPRoute:
$ kubectl get httproute example-route-1 -o yaml
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: my-example-gateway
  rules:
  - backendRefs:
    - group: ""
      kind: Service
      name: echo-1
      port: 8080
      weight: 50
    - group: ""
      kind: Service
      name: echo-2
      port: 8090
      weight: 50
    matches:
    - path:
        type: PathPrefix
        value: /echo
接著我們要找出 Gateway IP,可以透過以下方式找到:
# 方式 1:直接找 Gateway 
$ kubectl get gtw
NAME                 CLASS    ADDRESS       PROGRAMMED   AGE
my-example-gateway   cilium   10.13.1.201   True         9m59s
# 方式 2:找 Service,然後看 EXTERNAL-IP
$ kubectl get svc cilium-gateway-my-example-gateway
NAME                                TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
cilium-gateway-my-example-gateway   LoadBalancer   10.96.117.73   10.13.1.201   80:30715/TCP   12m
所以我們知道 Gateway IP 是 10.13.1.201 了,現在可以來發出 HTTP 請求:
$ curl -s http://10.13.1.201/echo
Hostname: echo-1-6555b459d9-ms9sr
Pod Information:
	node name:	worker-2
	pod name:	echo-1-6555b459d9-ms9sr
	pod namespace:	default
	pod IP:	10.244.1.59
Server values:
	server_version=nginx: 1.12.2 - lua: 10010
Request Information:
	client_address=10.244.0.31
	method=GET
	real path=/echo
	query=
	request_version=1.1
	request_scheme=http
	request_uri=http://10.13.1.201:8080/echo
Request Headers:
	accept=*/*  
	host=10.13.1.201  
	user-agent=curl/8.5.0  
	x-envoy-internal=true  
	x-forwarded-for=10.13.1.243  
	x-forwarded-proto=http  
	x-request-id=ded297ae-3da0-421d-ba79-5f03ac96bc28  
Request Body:
	-no body in request-
這裡可以從 Hostname 知道是 echo-2 處理我的請求
可以試著多發送幾次請求,會看到 echo-1 也會處請求,比例會是 50:50:
for i in {1..20}; do
  kubectl exec netshoot -- curl -s http://10.13.1.201/echo | head -n 3
done
現在我們來調整權重,改成 90:10:
kubectl edit httproute example-route-1
可以依照以下配置修改:
  - backendRefs:
    - group: ""
      kind: Service
      name: echo-1
      port: 8080
      weight: 90 # 改這裡
    - group: ""
      kind: Service
      name: echo-2
      port: 8090
      weight: 10 # 改這裡
    matches:
    - path:
        type: PathPrefix
        value: /echo
接著我們再次執行一次指令:
for i in {1..20}; do
  kubectl exec netshoot -- curl -s http://10.13.1.201/echo | head -n 3
done
會注意到 echo-1: echo-2 確實是 90:10
今天這篇主要帶大家體驗了 Cilium Gateway API 的安裝流程,並實際看到了它與傳統 Ingress Controller 完全不同的運作模式。
傳統的 Ingress Controller(像 NGINX 或 AWS Load Balancer Controller)是跑在 user space 的 proxy,所有流量都會經過一層額外的 Pod;而 Cilium 的 Gateway API 則是直接在 CNI 層整合 Envoy,利用 eBPF + TPROXY 把封包導入每個 Node 上的 Envoy DaemonSet,讓 Gateway 功能成為 datapath 的一部分。
這樣的架構不但讓 L7 流量管理更貼近網路層,也能結合 Cilium 的 NetworkPolicy 做更細緻的控制。
另外也提到,因為流量會先經過 Envoy,所以後端 Pod 拿到的 source IP 不會是原本的 client IP,不過 Cilium 會自動在 HTTP Header 裡補上 X-Forwarded-For 和 X-Envoy-External-Address,這樣 backend application 還是能取得真實使用者的 IP。
最後我們也透過 HTTPRoute 實作了最經典的流量分流(Traffic Splitting)範例,從 50/50 到 90/10,觀察 Cilium Gateway API 如何讓 Envoy 根據權重分配流量。