| 元件 | 版本 |
|---|---|
| Kubernetes | v1.35 |
| kind 叢集 | gwapi-lab(Day 3) |
| Gateway API | v1.5(Experimental channel) |
| 實作 | 刻意不裝 |
今天做一件反直覺的事:建立 Gateway 和 HTTPRoute,但不裝任何實作。
目的是驗證 Day 2 提出的第六項要求——失效時能說明原因——在規格層是如何落實的:資源建立了、實作卻不存在時,狀態欄位會明確指出原因,而不是留下一片空白。
接著拆解三層資源模型、Route 家族,以及 Policy Attachment 機制——這三樣是後續 26 天的地基。
Gateway API 把入口設定拆成三個資源,各有各的擁有者:
┌─────────────────────────────────────────────────────────┐
│ GatewayClass 基礎設施供應商 / 叢集管理員
│ 「這個叢集有哪幾種 Gateway 可以開」
│ ─ 誰來實作、用什麼參數
└────────────────────────┬────────────────────────────────┘
│ 被引用
┌────────────────────────▼────────────────────────────────┐
│ Gateway 平台團隊
│ 「開一個對外入口」
│ ─ 監聽哪些埠、什麼協定、哪張憑證
│ ─ 哪些 namespace 的 Route 可以綁上來 ← 權限邊界在這
└────────────────────────┬────────────────────────────────┘
│ 被綁定
┌────────────────────────▼────────────────────────────────┐
│ HTTPRoute 應用團隊
│ 「服務怎麼路由」
│ ─ 匹配什麼路徑、送到哪個 Service、要不要改寫
└─────────────────────────────────────────────────────────┘
Day 2 談過第四項要求:權限邊界必須落在資源邊界上。
若把 TLS 憑證、對外網域、路徑規則全部放進同一個資源,就會出現這種結構:
spec:
tls: # ← 平台團隊負責
- secretName: shop-tls
rules:
- host: shop.example.com # ← 平台團隊負責(網域是公司資產)
http:
paths:
- path: /api # ← 應用團隊負責
此時 RBAC 幫不上忙,因為它的最小授權單位是「資源」,而衝突發生在「同一個資源的不同欄位」。
拆成三層之後,權限邊界正好落在資源邊界上:
Gateway 的寫入權限,應用團隊沒有HTTPRoute 的寫入權限kubectl apply 自己的檔案,互不干擾
RBAC 終於管得動了。
這個邊界可以用三份各自受限的 kubeconfig 實測,驗證權限確實切開,而非僅止於宣稱。
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
# 選配:實作專屬的參數
# parametersRef:
# group: gateway.envoyproxy.io
# kind: EnvoyProxy
# name: custom-proxy-config
# namespace: envoy-gateway-system
這是叢集層級(cluster-scoped)資源,沒有 namespace。
| 欄位 | 意義 |
|---|---|
controllerName |
誰來實作。每個實作宣告自己負責哪個字串,只處理匹配的 GatewayClass |
parametersRef |
指向實作專屬的 CRD,用來客製資料平面(副本數、資源、Service 型別) |
parametersRef 是規格刻意留下的擴充點——資料平面要開幾個副本、用多少資源,規格不介入,交由實作定義。各實作以自己的 CRD 承接這部分設定,Envoy Gateway 用的是 EnvoyProxy。
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: platform-gateway
namespace: infra # ← 平台團隊的 namespace
spec:
gatewayClassName: envoy-gateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
listeners:一個 Gateway 可以有多個監聽器每個 listener 是一組「埠 + 協定 + 主機名 + TLS 設定 + 誰能綁上來」。
listener 是逐一設定的,因此同一個 Gateway 可以表達「80 埠開放給所有 namespace、443 埠只給通過核可的團隊」這種不對稱的授權結構。TLS 設定也綁在各自的 listener 上,而非整個資源共用一份。
protocol 的選項為 HTTP、HTTPS、TLS、TCP、UDP。L4 是一等公民,這正是 Day 2 第五項要求的落實。
allowedRoutes:權限邊界這是整個角色分離模型的樞紐。三種模式:
allowedRoutes:
namespaces:
from: Same # 只有同 namespace 的 Route 能綁(最嚴格)
---
allowedRoutes:
namespaces:
from: All # 任何 namespace 都能綁(最寬鬆)
---
allowedRoutes:
namespaces:
from: Selector # 只有帶特定標籤的 namespace 能綁(推薦)
selector:
matchLabels:
gateway-access: "true"
Selector 是實務上最實用的:平台團隊幫核准過的 namespace 貼上標籤,該團隊就能自助上線路由,不用開工單。
還可以限制 Route 的種類:
allowedRoutes:
kinds:
- kind: HTTPRoute # 只允許 HTTPRoute,不允許 TCPRoute
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
tls.mode| 模式 | 行為 |
|---|---|
Terminate |
Gateway 解密,用明文送到後端(最常見) |
Passthrough |
不解密,依 SNI 直接把加密流量轉給後端 |
Passthrough 適用於後端自行處理 TLS 的場景,例如 mTLS 需端到端貫通,或後端本身即為 TLS server、不應在中間解密。
Gateway 到後端那一段要加密怎麼辦?那是
BackendTLSPolicy(v1.4 引入)的守備範圍。
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: whoami-route
namespace: demo # ← 應用團隊的 namespace
spec:
parentRefs:
- name: platform-gateway
namespace: infra # ← 綁到平台團隊的 Gateway
sectionName: http # ← 綁到哪個 listener(選配)
hostnames:
- "whoami.localhost"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-v1
port: 80
parentRefs:綁定對象綁定關係是直接指名的:Route 明確寫出要綁哪一個 Gateway,而非透過某個 class 名稱間接媒合。
sectionName 可以精確綁到某個 listener(例如只綁 HTTPS 不綁 HTTP)。
注意方向:是 Route 主動綁 Gateway,不是 Gateway 列出誰能用。這叫雙向同意:
parentRefs 宣告「要綁定哪個 Gateway」allowedRoutes 宣告「接受哪些 namespace」兩邊都同意才成立。 這個設計讓應用團隊不能亂綁,平台團隊也不用維護一份長長的白名單。
hostnames支援精確網域與一層萬用字元。它會跟 listener 的 hostname 取交集:
| Listener hostname | Route hostnames | 實際生效 |
|---|---|---|
*.example.com |
shop.example.com |
shop.example.com |
*.example.com |
shop.other.com |
無交集,這條 Route 不生效 |
| (未設定) | shop.example.com |
shop.example.com |
這是另一道安全機制——平台團隊在 listener 上限制 *.example.com,應用團隊就搶不走 bank.com。
rules每條 rule 三個部分:
rules:
- matches: # 什麼條件下觸發(path / header / query / method)
- path:
type: PathPrefix
value: /api
filters: # 觸發後要做什麼加工(改寫、加標頭、重導、鏡像)
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs: # 最後送到哪(可多個,帶權重)
- name: api-v1
port: 8080
weight: 90
- name: api-v2
port: 8080
weight: 10
matches、filters、backendRefs 的權重各自都是獨立的題目,今天先建立整體印象。
注意 weight——金絲雀分流是規格的原生能力,以型別化欄位表達,不需要任何額外機制。
Day 2 談過第五項要求:L4 必須是一等公民。Gateway API 的解法是把 Route 做成一個家族:
| 資源 | 用途 | 通道 |
|---|---|---|
HTTPRoute |
HTTP/HTTPS 路由 | Standard |
GRPCRoute |
gRPC,懂 service/method 語意 | Standard |
TLSRoute |
依 SNI 路由,不解密 | Experimental |
TCPRoute |
任意 TCP | Experimental |
UDPRoute |
任意 UDP | Experimental |
它們全部綁到同一個 Gateway,共用同一套 listener 與權限模型。對外開放一個 Redis 不需要學習另一套資源模型。
Day 3 安裝的是 Experimental channel,因此五種 Route 皆已具備:
kubectl get crd | grep routes.gateway.networking
grpcroutes.gateway.networking.k8s.io
httproutes.gateway.networking.k8s.io
tcproutes.gateway.networking.k8s.io
tlsroutes.gateway.networking.k8s.io
udproutes.gateway.networking.k8s.io
規格不可能涵蓋所有功能。限流、JWT 驗證、熔斷、WAF——這些都是實作各自的強項。
Gateway API 的解法是 Policy Attachment:定義一套「政策如何附著到資源上」的通用機制,讓各實作自己定義 Policy CRD。
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: rate-limit
namespace: demo
spec:
targetRefs: # ← 附著到哪個資源上
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: whoami-route
rateLimit:
type: Local
local:
rules:
- limit:
requests: 10
unit: Second
關鍵是 targetRefs——政策可以掛在 Gateway 上(影響所有 Route)或掛在單一 HTTPRoute 上。
Day 2 談的第二項要求是「設定必須結構化且可驗證」,第三項是「擴充機制必須與核心規格解耦」。Policy CRD 完整滿足第二項,但不解決可攜性:
| 自由字串設定 | Policy CRD | |
|---|---|---|
| 型別檢查 | ✗ | ✓ |
| schema 驗證 | ✗ | ✓ |
kubectl explain |
✗ | ✓ |
| IDE 補全 | ✗ | ✓ |
| 跨實作可攜 | ✗ | ✗(仍然不可攜) |
Policy 依然綁死實作。 Envoy Gateway 的 BackendTrafficPolicy 在 Traefik 上不能用。
但這是刻意的取捨——規格層保持精簡與可攜,擴充層允許各實作競爭創新。核心路由邏輯(HTTPRoute)可攜,進階功能(Policy)不可攜。
這條界線也正是「同一份 HTTPRoute 搬家」這類實驗真正要測的東西:HTTPRoute 搬得動,Policy 搬不動。
三大 Policy(Envoy Gateway):
| Policy | 管什麼 |
|---|---|
ClientTrafficPolicy |
Envoy 對下游客戶端的行為(連線、逾時、HTTP 選項) |
BackendTrafficPolicy |
Envoy 對上游後端的行為(限流、熔斷、重試、負載平衡) |
SecurityPolicy |
認證與授權(JWT、OIDC、CORS、extAuth) |
理論講完,來看實際行為。
kubectl get gatewayclass
No resources found
沒有任何 GatewayClass——因為沒裝實作。
# day04/no-implementation.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: nobody-home
spec:
controllerName: example.com/does-not-exist
---
apiVersion: v1
kind: Namespace
metadata:
name: infra
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: test-gateway
namespace: infra
spec:
gatewayClassName: nobody-home
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: test-route
namespace: demo
spec:
parentRefs:
- name: test-gateway
namespace: infra
hostnames:
- "test.localhost"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-v1
port: 80
kubectl apply -f day04/no-implementation.yaml
gatewayclass.gateway.networking.k8s.io/nobody-home created
namespace/infra created
gateway.gateway.networking.k8s.io/test-gateway created
httproute.gateway.networking.k8s.io/test-route created
全部建立成功。
kubectl get gatewayclass nobody-home
NAME CONTROLLER ACCEPTED AGE
nobody-home example.com/does-not-exist Unknown 20s
ACCEPTED: Unknown。
kubectl get gateway -n infra
NAME CLASS ADDRESS PROGRAMMED AGE
test-gateway nobody-home Unknown 20s
PROGRAMMED: Unknown。
深入看 conditions:
kubectl -n infra get gateway test-gateway -o jsonpath='{.status.conditions}' | jq
[
{
"lastTransitionTime": "1970-01-01T00:00:00Z",
"message": "Waiting for controller",
"reason": "Pending",
"status": "Unknown",
"type": "Accepted"
},
{
"lastTransitionTime": "1970-01-01T00:00:00Z",
"message": "Waiting for controller",
"reason": "Pending",
"status": "Unknown",
"type": "Programmed"
}
]
lastTransitionTime1970-01-01T00:00:00Z——Unix 紀元零點。這個時間戳是條線索:沒有任何程式在執行期寫過這段狀態。
它來自 CRD 本身。Gateway API 在 gateways 這個 CRD 的 OpenAPI schema 裡,替 status.conditions 設了 default:
kubectl get crd gateways.gateway.networking.k8s.io -o jsonpath='{.spec.versions[0].schema.openAPIV3Schema.properties.status.properties.conditions.default}' | jq
因此在 kubectl apply 的當下,API Server 就依 schema 預設值把這兩條 condition 填好了,不需要有任何 controller 存在。gatewayclasses 也有同樣的預設(只有 Accepted 一條)。
這一點讓 Day 2 的第六項要求成立得更徹底:狀態回報不是「某個實作額外提供的功能」,而是規格層的保證——只要 CRD 安裝了,即使叢集裡一個 controller 都沒有,查詢結果也不會是空白,而是明確的 Pending / Waiting for controller。
| 觀察點 | 沒有實作時的回報 |
|---|---|
kubectl get gatewayclass |
ACCEPTED: Unknown |
kubectl get gateway |
PROGRAMMED: Unknown、ADDRESS 空白 |
status.conditions[].reason |
Pending |
status.conditions[].message |
Waiting for controller |
「為什麼不生效」在這套規格裡是一等公民。 正式環境排查時,「明確的狀態」與「一片空白」的差別,直接決定定位問題所需的時間量級。
這三個 condition 會貫穿後續所有章節:
| Condition | 意義 | False 時常見原因 |
|---|---|---|
Accepted |
這個資源的設定語意上有效,實作願意接手 | GatewayClass 不存在、listener 設定衝突、hostname 無交集 |
Programmed |
設定已經實際生效到資料平面 | 資料平面 Pod 還沒起、憑證 Secret 不存在 |
ResolvedRefs(Route 上) |
所有引用(backendRefs、certificateRefs)都解析成功 | Service 不存在、跨 namespace 少了 ReferenceGrant |
看 HTTPRoute 的狀態:
kubectl -n demo get httproute test-route -o jsonpath='{.status}' | jq
什麼都沒有——連 {} 都不會印出來。
這裡跟 Gateway 形成有趣的對比:httproutes 的 CRD 沒有替 status 設 schema 預設值,所以在沒有 controller 認領的情況下,它的 status 是真正的空。
理由也很合理:Gateway 的狀態描述的是「這個 Gateway 自身的情況」,講得出通用的預設;而 HTTPRoute 的狀態是 status.parents——每個 parent 各自一份,在還沒有任何 controller 決定認領哪些 parent 之前,規格無法預先填入任何內容。
因此 Route 的「為什麼不生效」要從它所綁定的 Gateway 那一端查起,這是本系列固定的排查路徑。
ResolvedRefs的價值在於把後端引用的正確性提前到資源狀態上呈現:後端 Service 不存在時,不必等到執行期收到 5xx 才發現,資源狀態就會直接標示解析失敗。
三層資源各自負責什麼、對應到哪些欄位,整理成一張隨身表:
| 關注點 | 資源與欄位 | 擁有者 |
|---|---|---|
| 叢集提供哪幾種入口、由誰實作 | GatewayClass.spec.controllerName |
叢集管理員 |
| 資料平面的部署參數 | GatewayClass.spec.parametersRef → 實作專屬 CRD |
叢集管理員 |
| 對外埠號與協定 | Gateway.spec.listeners[].port / .protocol |
平台團隊 |
| TLS 憑證與終止模式 | Gateway.spec.listeners[].tls |
平台團隊 |
| 網域邊界 | Gateway.spec.listeners[].hostname |
平台團隊 |
| 誰能綁上來 | Gateway.spec.listeners[].allowedRoutes |
平台團隊 |
| 綁定對象 | HTTPRoute.spec.parentRefs |
應用團隊 |
| 服務網域 | HTTPRoute.spec.hostnames(與 listener 取交集) |
應用團隊 |
| 匹配條件 | rules[].matches(path / header / query / method) |
應用團隊 |
| 請求加工 | rules[].filters(改寫、標頭、重導、鏡像) |
應用團隊 |
| 後端與權重 | rules[].backendRefs[].weight |
應用團隊 |
| 跨 namespace 引用授權 | ReferenceGrant |
被引用方 |
| 進階功能(限流、認證、熔斷) | 實作專屬 Policy CRD + targetRefs |
依功能而定 |
| 生效狀態與失敗原因 | status.conditions |
由實作回報 |
這張表的縱向分組就是權限邊界。 平台團隊那一組全部落在 Gateway 資源上,應用團隊那一組全部落在 HTTPRoute 上,因此 RBAC 可以直接依資源授權。