iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Kubernetes

Kubernetes ingress架構系列 第 5

# Day 5|第一條路由:裝上 Envoy Gateway,打通 Gateway + HTTPRoute

  • 分享至 

  • xImage
  •  

本篇環境

元件 版本
Kubernetes v1.35
kind 叢集 gwapi-lab(Day 3)
Gateway API v1.5(Experimental)
Envoy Gateway v1.8.0
Helm 3.x

今天要解決什麼

Day 4 建立了 GatewayHTTPRoute,兩者都停在 Accepted: Unknownmessage: "Waiting for controller"

今天把 controller 裝上,讓它們活過來。


為什麼用 Envoy Gateway 開場

本篇開始進入規格深潛,主題是 Gateway API 規格本身,而非任何單一產品的功能。因此實作要挑最「透明」的。

Envoy Gateway 符合三個條件:

  1. CNCF 專案,Envoy 官方團隊維護。 Envoy 是全世界最主流的 L7 代理,Istio、Cilium、kgateway、Consul 的資料平面全都是它。學 Envoy Gateway 等於學到底層
  2. 沒有自家方言。 Traefik 有 IngressRoute、Istio 有 VirtualService,讀者容易分不清「這是規格還是產品功能」。Envoy Gateway 幾乎只吃 Gateway API
  3. 穩定度與效能都在前段。 2026 年的一致性測試中,Envoy 在 100 輪測試零失敗;L7 效能與 Istio Ambient 實質打平

以它為載體學到的規格內容,換到 Traefik 或 Istio 上同樣適用。 這是刻意的安排。


安裝

source versions.env

helm install envoy-gateway \
  oci://docker.io/envoyproxy/gateway-helm \
  --version $ENVOY_GATEWAY_VERSION \
  --namespace envoy-gateway-system \
  --create-namespace \
  --wait

Envoy Gateway 的 chart 是 OCI registry 格式,不用 helm repo add,直接 oci:// 開頭。這是 Helm 3.8+ 的功能。

等它起來:

kubectl -n envoy-gateway-system get pods
NAME                             READY   STATUS    RESTARTS   AGE
envoy-gateway-8657b9f4d5-lz7kq   1/1     Running   0          52s

只有一個 Pod。注意:這是控制平面,不是資料平面。

Envoy Gateway 採用控制平面與資料平面分離的架構——這正是 Day 2 談的第二組分離:

┌────────────────────────────────────────────────┐
│  控制平面(現在裝好的這一個 Pod)                 │
│  envoy-gateway                                 │
│  ─ watch Gateway API 資源                       │
│  ─ 翻譯成 Envoy 的 xDS 設定                     │
│  ─ 動態建立資料平面 Deployment                   │
└─────────────────┬──────────────────────────────┘
                  │ 每建一個 Gateway 資源
                  ▼
┌────────────────────────────────────────────────┐
│  資料平面(等一下建 Gateway 才會出現)            │
│  envoy-<gateway-namespace>-<gateway-name>-xxx  │
│  ─ 真正跑 Envoy proxy 的 Pod                    │
│  ─ 實際處理流量                                  │
└────────────────────────────────────────────────┘

控制平面本身不處理任何流量,它只負責 watch 資源、翻譯設定、管理資料平面的生命週期。一個 Gateway 資源會生出一組獨立的 Envoy Deployment。

這個設計帶來兩項好處:控制平面重啟不中斷既有流量,以及故障隔離——兩個團隊的 Gateway 各自運行獨立的 Envoy,一邊流量暴增不會波及另一邊。

確認 GatewayClass 出現了

kubectl get gatewayclass
NAME  CONTROLLER                                      ACCEPTED  AGE
eg    gateway.envoyproxy.io/gatewayclass-controller   True      60s

ACCEPTED: True

對照 Day 4 那個 nobody-homeACCEPTED: Unknown)——差別就是「有沒有人服務這個 controllerName」。

Helm chart 預設會建立一個名為 eg 的 GatewayClass,本篇直接沿用它。


為 kind 準備資料平面設定

這一步是 kind 環境的關鍵,跳過會不通。

Envoy Gateway 預設會把資料平面的 Service 開成 type: LoadBalancer。在 kind 上沒有雲端 LB,會卡在 <pending>(Day 3 講過的坑)。

解法是以 EnvoyProxy CRD 客製資料平面——這正是 Day 4 談的 GatewayClass.parametersRef 擴充點。

ClusterIP + 節點埠的組合在 kind 上還需要一步:讓 Envoy 容器綁到節點的 80/443。Envoy Gateway 支援用 patch 直接改 Deployment:

# day05/envoy-proxy-config.yaml(完整版)
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: kind-proxy-config
  namespace: envoy-gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyDeployment:
        replicas: 1
        pod:
          nodeSelector:
            ingress-ready: "true"
          tolerations:
            - key: node-role.kubernetes.io/control-plane
              operator: Equal
              effect: NoSchedule
        container:
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
        patch:
          type: StrategicMerge
          value:
            spec:
              template:
                spec:
                  containers:
                    - name: envoy
                      ports:
                        - name: http
                          containerPort: 10080
                          hostPort: 80
                          protocol: TCP
                        - name: https
                          containerPort: 10443
                          hostPort: 443
                          protocol: TCP
      envoyService:
        type: ClusterIP

為什麼 containerPort 是 10080 不是 80? Envoy 以非 root 執行,不能綁 1024 以下的埠。Envoy Gateway 的慣例是把 listener port 80 映射到容器內的 10080、443 映射到 10443,再由 hostPort 接回節點的 80/443。

把它綁到 GatewayClass 上:

# day05/gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy
    name: kind-proxy-config
    namespace: envoy-gateway-system
kubectl apply -f day05/envoy-proxy-config.yaml
kubectl apply -f day05/gatewayclass.yaml

EnvoyProxy 是 Envoy Gateway 自己的 CRD,不是 Gateway API 規格的一部分。這正是 Day 4 談的擴充點——資料平面要開幾個副本、要不要 hostPort,規格不介入,交由實作定義。換言之,這份 YAML 換到 Traefik 即失效——換實作就得換一份對應的資料平面設定。


建立 Gateway

# day05/gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: infra
  labels:
    # 標記為平台團隊的 namespace
    role: platform
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: platform-gateway
  namespace: infra
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: "true"

允許帶 gateway-access: "true" 標籤的 namespace 綁上來。先幫 demo 貼標籤:

kubectl apply -f day05/gateway.yaml
kubectl label namespace demo gateway-access=true

觀察資料平面被建出來

kubectl -n envoy-gateway-system get pods -w
NAME                                        READY   STATUS    RESTARTS   AGE
envoy-gateway-8657b9f4d5-lz7kq              1/1     Running   0          5m
envoy-infra-platform-gateway-a1b2c3d4-...   0/2     Pending   0          3s
envoy-infra-platform-gateway-a1b2c3d4-...   2/2     Running   0          25s

一個新的 Pod 出現了。 命名規則是 envoy-<gateway-namespace>-<gateway-name>-<hash>

這就是 Day 4 講的:Gateway 資源不只是設定,它會實際生出一組資料平面

確認它在正確的節點上:

kubectl -n envoy-gateway-system get pods -o wide | grep platform-gateway

NODE 欄位必須是 gwapi-lab-control-plane。如果跑到 worker 上,nodeSelector 沒生效,流量會不通。

看 Gateway 的狀態

kubectl -n infra get gateway platform-gateway
NAME               CLASS   ADDRESS      PROGRAMMED   AGE
platform-gateway   eg      10.96.x.x    True         45s

PROGRAMMED: TrueADDRESS 也填上了。

對照 Day 4 那個永遠 Unknown 的:

kubectl -n infra get gateway platform-gateway -o jsonpath='{.status.conditions}' | jq
[
  {
    "type": "Accepted",
    "status": "True",
    "reason": "Accepted",
    "message": "The Gateway has been scheduled by Envoy Gateway"
  },
  {
    "type": "Programmed",
    "status": "True",
    "reason": "Programmed",
    "message": "Address assigned to the Gateway, 1/1 envoy replicas available"
  }
]

message 具體到可以直接判讀資料平面的就緒狀況——「1/1 envoy replicas available」。狀態不只說「好了沒」,還說明了依據。


建立 HTTPRoute

# day05/httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: whoami-route
  namespace: demo
spec:
  parentRefs:
    - name: platform-gateway
      namespace: infra
      sectionName: http
  hostnames:
    - "whoami.localhost"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: whoami-v1
          port: 80
kubectl apply -f day05/httproute.yaml

驗證

curl -s whoami.localhost/ | head -5
Hostname: whoami-v1-6b4d8c9f7-mn2kp
IP: 127.0.0.1
IP: 10.244.1.5
RemoteAddr: 10.244.0.8:41234
Name: whoami-v1

第一條路由通了。

完整路徑:

curl whoami.localhost
   │
   ▼ DNS:*.localhost → 127.0.0.1
主機 :80
   │
   ▼ kind extraPortMappings(Day 3)
control-plane 容器 :80
   │
   ▼ hostPort(EnvoyProxy CRD 設定)
Envoy 資料平面 Pod(:10080)
   │
   ▼ 讀 Host header = "whoami.localhost"
   ▼ 比對 HTTPRoute → 找到 whoami-v1
whoami-v1 Pod(在 worker 節點上)

Day 3 埋的三條線(extraPortMappingsingress-ready 標籤、demo 應用)在這裡全部接起來了。


讀懂 Route 的狀態

kubectl -n demo get httproute whoami-route -o jsonpath='{.status.parents}' | jq
[
  {
    "parentRef": {
      "group": "gateway.networking.k8s.io",
      "kind": "Gateway",
      "name": "platform-gateway",
      "namespace": "infra",
      "sectionName": "http"
    },
    "controllerName": "gateway.envoyproxy.io/gatewayclass-controller",
    "conditions": [
      {
        "type": "Accepted",
        "status": "True",
        "reason": "Accepted",
        "message": "Route is accepted"
      },
      {
        "type": "ResolvedRefs",
        "status": "True",
        "reason": "ResolvedRefs",
        "message": "Resolved all the Object references for the Route"
      }
    ]
  }
]

注意 status.parents陣列——一條 Route 可以同時綁定多個 Gateway,每個 parent 各自回報狀態。因此「這條 Route 在 A Gateway 上生效、在 B Gateway 上被拒絕」是可以精確表達的。

實驗一:故意指向不存在的 Service

後端引用錯誤是最常見的設定失誤之一。以下驗證規格層如何處理這種情況。

kubectl -n demo patch httproute whoami-route --type=json \
  -p='[{"op":"replace","path":"/spec/rules/0/backendRefs/0/name","value":"does-not-exist"}]'

sleep 2
kubectl -n demo get httproute whoami-route \
  -o jsonpath='{.status.parents[0].conditions}' | jq
[
  {
    "type": "Accepted",
    "status": "True",
    "reason": "Accepted",
    "message": "Route is accepted"
  },
  {
    "type": "ResolvedRefs",
    "status": "False",
    "reason": "BackendNotFound",
    "message": "Failed to process route rule 0 backendRef 0: service demo/does-not-exist not found."
  }
]

ResolvedRefs: Falsereason: BackendNotFoundmessage 直接指出是哪個 Service 找不到。

這就是 Day 4 講的「Gateway API 把『為什麼不動』變成一等公民」。

打打看:

curl -s -o /dev/null -w "%{http_code}\n" whoami.localhost/
500

流量確實會失敗(規格規定 backend 解析失敗時回 5xx),但失敗原因不需要推測——資源狀態上已完整記載。

改回來:

kubectl -n demo patch httproute whoami-route --type=json \
  -p='[{"op":"replace","path":"/spec/rules/0/backendRefs/0/name","value":"whoami-v1"}]'

實驗二:拿掉 namespace 標籤

平台團隊撤銷授權,Route 應該被擋下:

kubectl label namespace demo gateway-access-
sleep 30
kubectl -n demo get httproute whoami-route \
  -o jsonpath='{.status.parents[0].conditions}' | jq -c '.[] | {type,status,reason}'
curl -s -o /dev/null -w "%{http_code}\n" whoami.localhost/
{"type":"Accepted","status":"True","reason":"Accepted"}
{"type":"ResolvedRefs","status":"True","reason":"ResolvedRefs"}
200

沒有被擋下。 標籤已經拿掉了,Route 仍然是 Accepted: True,流量照樣通。

這不是等不夠久——實測放到 60 秒以上,狀態完全不變。

這是 Envoy Gateway v1.8.0 的一個 reconcile 缺口

規則本身是實作對的,問題出在沒有被重新觸發。只要讓那條 HTTPRoute 重新 reconcile 一次,判斷立刻就正確了:

# 隨便改個 annotation,逼它重新 reconcile
kubectl -n demo annotate httproute whoami-route poke="$(date +%s)" --overwrite
sleep 10
kubectl -n demo get httproute whoami-route \
  -o jsonpath='{.status.parents[0].conditions}' | jq -c '.[] | {type,status,reason}'
curl -s -o /dev/null -w "%{http_code}\n" whoami.localhost/
{"type":"Accepted","status":"False","reason":"NotAllowedByListeners"}
{"type":"ResolvedRefs","status":"True","reason":"ResolvedRefs"}
404

這下對了。 重啟控制平面(kubectl -n envoy-gateway-system rollout restart deploy/envoy-gateway)做全量 resync 也會得到同樣結果。

所以結論是:Envoy Gateway v1.8.0 不會因為 Namespace 標籤變動,去重新評估受影響的 HTTPRoute。 它只在 Route 自己變動、或控制平面重啟時才重算 allowedRoutes

為什麼這一點值得重視

這是安全相關的行為差異。allowedRoutes 是 Day 4 講的整個角色分離模型的樞紐,而「撤銷授權」正是它最關鍵的一半:

  • 平台團隊以為 kubectl label namespace demo gateway-access- 就收回了權限
  • 實際上那條 Route 繼續對外服務,直到有人剛好動了它

授權(貼標籤)會立刻生效,撤銷(拿掉標籤)不會。 這個不對稱很容易在事故檢討時才被發現。

實務上撤銷授權請多做一步,明確逼一次 reconcile:

kubectl label namespace demo gateway-access-
# 別只做上面這行 —— 再逼受影響的 Route 重新評估
kubectl -n demo annotate httproute --all reconcile="$(date +%s)" --overwrite

這是 v1.8.0 上的實測行為,並非規格規定。Gateway API 規格只定義「不符合 allowedRoutes 的 Route 應回報 NotAllowedByListeners」,未規定實作需在多久內偵測到變動。把同一個實驗換到其他實作上重跑,是一個相當有效的實作差異探針。

貼回去(同樣要逼一次 reconcile):

kubectl label namespace demo gateway-access=true
kubectl -n demo annotate httproute --all reconcile="$(date +%s)" --overwrite

動態更新有多快

新增一條路由,看看多久生效:

cat <<'EOF' | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: v2-route
  namespace: demo
spec:
  parentRefs:
    - name: platform-gateway
      namespace: infra
  hostnames:
    - "v2.localhost"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: whoami-v2
          port: 80
EOF

sleep 2
curl -s v2.localhost/ | grep "^Name:"
Name: whoami-v2

兩秒內生效,沒有任何服務中斷。

順便驗證負載平衡:

for i in $(seq 1 10); do
  curl -s whoami.localhost/ | grep "^Hostname:"
done | sort | uniq -c
   5 Hostname: whoami-v1-6b4d8c9f7-mn2kp
   5 Hostname: whoami-v1-6b4d8c9f7-qr8vt

看一眼 Envoy 產生的設定

這一步能建立最深的直覺。

Envoy Gateway 提供 egctl 工具可以導出資料平面的設定。沒裝的話,用 Envoy 的 admin 介面也行:

ENVOY_POD=$(kubectl -n envoy-gateway-system get pods \
  -l gateway.envoyproxy.io/owning-gateway-name=platform-gateway \
  -o jsonpath='{.items[0].metadata.name}')

kubectl -n envoy-gateway-system port-forward $ENVOY_POD 19000:19000 &
sleep 2

# 看它認得哪些 route
curl -s localhost:19000/config_dump | \
  jq '.configs[] | select(.["@type"] | contains("RoutesConfigDump"))' | head -60

輸出中可以看到 whoami.localhost 被翻譯成 Envoy 的 virtual_hosts 結構。

# 看叢集(後端)狀態
curl -s localhost:19000/clusters | grep whoami | head -5
httproute/demo/whoami-route/rule/0::10.244.1.5:80::health_flags::healthy
httproute/demo/whoami-route/rule/0::10.244.2.7:80::health_flags::healthy

兩個 Endpoint,皆為健康狀態。 命名規則直接對應 HTTPRoute 的 namespace、名稱與 rule 索引。

kill %1  # 關掉 port-forward

Gateway API 實作做的事永遠是這三步——watch API → 翻譯成資料平面的原生設定 → 熱套用。Envoy Gateway 翻成 xDS、Traefik 翻成自家動態設定、Istio 也翻成 xDS。格式不同,模式相同。


上一篇
# Day 4|Gateway API 的核心模型:三層資源與角色分離
下一篇
# Day 6|HTTPRoute 匹配全解:path、header、query、method
系列文
Kubernetes ingress架構7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言