| 元件 | 版本 |
|---|---|
| Kubernetes | v1.35 |
| kind 叢集 | gwapi-lab(Day 3) |
| Gateway API | v1.5(Experimental) |
| Envoy Gateway | v1.8.0 |
| Helm | 3.x |
Day 4 建立了 Gateway 與 HTTPRoute,兩者都停在 Accepted: Unknown、message: "Waiting for controller"。
今天把 controller 裝上,讓它們活過來。
本篇開始進入規格深潛,主題是 Gateway API 規格本身,而非任何單一產品的功能。因此實作要挑最「透明」的。
Envoy Gateway 符合三個條件:
IngressRoute、Istio 有 VirtualService,讀者容易分不清「這是規格還是產品功能」。Envoy Gateway 幾乎只吃 Gateway API以它為載體學到的規格內容,換到 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,一邊流量暴增不會波及另一邊。
kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
eg gateway.envoyproxy.io/gatewayclass-controller True 60s
ACCEPTED: True。
對照 Day 4 那個 nobody-home(ACCEPTED: Unknown)——差別就是「有沒有人服務這個 controllerName」。
Helm chart 預設會建立一個名為 eg 的 GatewayClass,本篇直接沿用它。
這一步是 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 即失效——換實作就得換一份對應的資料平面設定。
# 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 沒生效,流量會不通。
kubectl -n infra get gateway platform-gateway
NAME CLASS ADDRESS PROGRAMMED AGE
platform-gateway eg 10.96.x.x True 45s
PROGRAMMED: True,ADDRESS 也填上了。
對照 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」。狀態不只說「好了沒」,還說明了依據。
# 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 埋的三條線(extraPortMappings、ingress-ready 標籤、demo 應用)在這裡全部接起來了。
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 上被拒絕」是可以精確表達的。
後端引用錯誤是最常見的設定失誤之一。以下驗證規格層如何處理這種情況。
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: False、reason: BackendNotFound,message 直接指出是哪個 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"}]'
平台團隊撤銷授權,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 秒以上,狀態完全不變。
規則本身是實作對的,問題出在沒有被重新觸發。只要讓那條 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- 就收回了權限授權(貼標籤)會立刻生效,撤銷(拿掉標籤)不會。 這個不對稱很容易在事故檢討時才被發現。
實務上撤銷授權請多做一步,明確逼一次 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 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。格式不同,模式相同。