iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Kubernetes

Kubernetes ingress架構系列 第 4

# Day 4|Gateway API 的核心模型:三層資源與角色分離

  • 分享至 

  • xImage
  •  

本篇環境

元件 版本
Kubernetes v1.35
kind 叢集 gwapi-lab(Day 3)
Gateway API v1.5(Experimental channel)
實作 刻意不裝

今天要解決什麼

今天做一件反直覺的事:建立 GatewayHTTPRoute,但不裝任何實作。

目的是驗證 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 的寫入權限,應用團隊沒有
  • 應用團隊有自己 namespace 內 HTTPRoute 的寫入權限
  • 兩邊各自 kubectl apply 自己的檔案,互不干擾

RBAC 終於管得動了。

這個邊界可以用三份各自受限的 kubeconfig 實測,驗證權限確實切開,而非僅止於宣稱。


GatewayClass

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


Gateway

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 的選項為 HTTPHTTPSTLSTCPUDPL4 是一等公民,這正是 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 引入)的守備範圍。


HTTPRoute

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 列出誰能用。這叫雙向同意

  • Route 以 parentRefs 宣告「要綁定哪個 Gateway」
  • 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

matchesfiltersbackendRefs 的權重各自都是獨立的題目,今天先建立整體印象。

注意 weight——金絲雀分流是規格的原生能力,以型別化欄位表達,不需要任何額外機制。


Route 家族全覽

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

Policy Attachment:規格的擴充點

規格不可能涵蓋所有功能。限流、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 上。

Policy 解決了什麼、沒解決什麼

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"
  }
]

注意那個 lastTransitionTime

1970-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: UnknownADDRESS 空白
status.conditions[].reason Pending
status.conditions[].message Waiting for controller

「為什麼不生效」在這套規格裡是一等公民。 正式環境排查時,「明確的狀態」與「一片空白」的差別,直接決定定位問題所需的時間量級。

三個核心 condition

這三個 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 可以直接依資源授權。


上一篇
# Day 3|建置 kind 實驗環境 + 裝上 Gateway API
下一篇
# Day 5|第一條路由:裝上 Envoy Gateway,打通 Gateway + HTTPRoute
系列文
Kubernetes ingress架構7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言