iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Kubernetes

Kubernetes ingress架構系列 第 7

# Day 7|Filters:改寫、重導、標頭、鏡像

  • 分享至 

  • xImage
  •  

本篇環境

元件 版本
Kubernetes v1.35
Gateway API v1.5(Experimental channel)
Envoy Gateway v1.8.0
測試後端 traefik/whoami:v1.10(鏡像端加開 --verbose

沿用 Day 5 的 platform-gateway 與 Day 6 的 echo-a / echo-b / echo-d 後端。


今天要解決什麼

Day 6 解決的是「請求該去哪」。今天處理的是**「請求在路上被動了什麼手腳」**——路徑改寫、標頭注入、重導、流量鏡像。

這些在 Ingress 時代全部是 annotation。想把 /api/users 改寫成 /users,nginx 要寫 nginx.ingress.kubernetes.io/rewrite-target: /$2 外加一段正則 path;換 Traefik 要改成 Middleware CRD;換 HAProxy 又是另一套。同一個需求,三種寫法,零可攜性,而且寫錯了要等到打流量才知道。

Gateway API 把這些行為收進 filters 這個型別化欄位。今天要驗證的是三件事:

  1. 四種 Core Filter 各自做什麼、怎麼串
  2. 串起來時執行順序由誰決定
  3. 寫出互斥組合時,錯誤在哪一層被擋下

Filter 是規則層的加工站

filters 掛在 rules 底下,與 backendRefs 平行——先加工,再轉發

rules:
  - matches: [...]      # 誰進來
    filters: [...]      # 進來之後做什麼加工
    backendRefs: [...]  # 加工完送去哪

規格定義的 Core Filter 有四種,所有實作都必須支援:

Filter 作用 是否轉給後端
RequestHeaderModifier 改請求標頭
ResponseHeaderModifier 改回應標頭
URLRewrite 改路徑與 Host
RequestRedirect 直接回 3xx 否,終結型
RequestMirror 複製一份到別的後端 是(鏡像回應丟棄)

kubectl explain 裡有兩條相容性規則要先記住:

kubectl explain httproute.spec.rules.filters
Specifying the same filter multiple times is not supported unless explicitly
indicated in the filter.

All filters are expected to be compatible with each other except for the
URLRewrite and RequestRedirect filters, which may not be combined.

同型別不能重複寫兩次;URLRewriteRequestRedirect 互斥。後者稍後會實測「互斥」到底是誰在把關。


實作一:一條規則串四個 Filter

# day07/route-filters.yaml(節錄)
    - matches:
        - path: { type: PathPrefix, value: /shop }
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            set:
              - { name: X-Tenant, value: acme }    # 有就覆蓋、沒有就建立
            add:
              - { name: X-Trace, value: day07 }    # 已存在時附加成多值
            remove:
              - X-Drop-Me                          # 直接拿掉
        - type: ResponseHeaderModifier
          responseHeaderModifier:
            set:
              - { name: X-Served-By, value: gateway-api }
            add:
              - { name: Cache-Control, value: no-store }
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /                # /shop/items → /items
        - type: RequestMirror
          requestMirror:
            backendRef: { name: echo-mirror, port: 80 }
      backendRefs:
        - name: echo-a
          port: 80

setadd 的差別值得停一下:set 是覆蓋(用戶端送什麼都不算數),add 是附加(同名標頭會變成多值)。要防止用戶端偽造內部標頭,必須用 setremove,不能用 add

驗證:請求端

whoami 會把收到的請求整包回吐,所以「Gateway 到底改了什麼」看回應就知道。刻意送一個假的 X-Tenant 和一個該被拿掉的 X-Drop-Me

curl -s -H "Host: filters.localhost" \
     -H "X-Tenant: wrong-value" -H "X-Drop-Me: secret" \
     127.0.0.1/shop/items
Name: echo-a
RemoteAddr: 10.244.0.3:40262
GET /items HTTP/1.1
Host: filters.localhost
User-Agent: curl/8.18.0
X-Envoy-External-Address: 172.18.0.1
X-Forwarded-For: 172.18.0.1
X-Forwarded-Proto: http
X-Request-Id: e2f46486-e9f5-4eee-af80-74abd6cccdab
X-Tenant: acme
X-Trace: day07

三件事同時成立:

  • GET /items HTTP/1.1——用戶端打的是 /shop/items,後端只看到 /itemsReplacePrefixMatch 換掉的是matches 匹配到的那段前綴,不是字串取代。
  • X-Tenant: acme——用戶端送的 wrong-valueset 蓋掉了。
  • X-Drop-Me 不見了——remove 生效,後端完全看不到。

驗證:回應端

curl -si -H "Host: filters.localhost" 127.0.0.1/shop/items | head -6
HTTP/1.1 200 OK
date: Sat, 08 Aug 2026 16:09:14 GMT
content-length: 405
content-type: text/plain; charset=utf-8
x-served-by: gateway-api
cache-control: no-store

whoami 本身不會送這兩個標頭,它們是 ResponseHeaderModifier 加上去的。set / add / remove 三個動作在請求與回應兩側是對稱的;這裡只示範了前兩者,因為 whoami 的回應沒有可拿掉的標頭。


ReplaceFullPath 與 Host 改寫

ReplacePrefixMatch 只換前綴,ReplaceFullPath 整段換掉。順便把 Host 也改了:

    - matches:
        - path: { type: PathPrefix, value: /legacy }
      filters:
        - type: URLRewrite
          urlRewrite:
            hostname: internal.example.com
            path:
              type: ReplaceFullPath
              replaceFullPath: /new-home
      backendRefs: [{ name: echo-b, port: 80 }]
curl -s -H "Host: filters.localhost" 127.0.0.1/legacy/anything/deep | sed -n '1p;8,9p'
Name: echo-b
GET /new-home HTTP/1.1
Host: internal.example.com

/legacy/anything/deep 後面那一長串整個消失了——這是與 ReplacePrefixMatch 最大的差別,用錯會靜默丟掉路徑資訊。hostname 改寫則常用在後端服務要求特定 Host 的場景(虛擬主機、憑證 SNI 比對)。


RequestRedirect:終結型 Filter

    - matches:
        - path: { type: PathPrefix, value: /old }
      filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            hostname: new.example.com
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /fresh
            statusCode: 301
      # 沒有 backendRefs —— 它不會轉給任何後端
curl -si -H "Host: filters.localhost" 127.0.0.1/old/page | head -4
HTTP/1.1 301 Moved Permanently
location: https://new.example.com/fresh/page
date: Sat, 08 Aug 2026 16:09:04 GMT
content-length: 0

一次換掉 scheme、hostname 與 path 前綴,而且 /page 這段被保留下來接在新前綴後面。整條規則不需要 backendRefs——請求根本沒進到叢集內部,Gateway 自己回完就結束。HTTP→HTTPS 的全站導向就是這個 Filter 加一條 scheme: https


RequestMirror:把正式流量複製一份

鏡像的用途是拿真實流量壓測新版,但不影響使用者——鏡像端的回應會被丟棄,慢了、掛了、回 500 都不會傳回用戶端。

問題是:怎麼證明它真的送到了?鏡像端刻意開了 --verbose,讓它把每一筆請求寫進 stdout:

curl -s -o /dev/null -H "Host: filters.localhost" 127.0.0.1/shop/items
kubectl -n demo logs deploy/echo-mirror --tail=2
2026/08/08 16:08:43 10.244.0.3:51962 - - [08/Aug/2026:16:08:43 +0000] "GET /items HTTP/1.1" - -
2026/08/08 16:08:47 10.244.0.3:51974 - - [08/Aug/2026:16:08:47 +0000] "GET /items HTTP/1.1" - -

鏡像端收到了,而主線仍然是乾淨的 200。注意日誌裡是 /items——鏡像收到的是改寫後的路徑。這件事接下來會變成一個實驗。


實作二:Filter 的執行順序由誰決定

規格對順序的用字很微妙:

Wherever possible, implementations SHOULD implement filters in the order
they are specified.

Implementations MAY choose to implement this ordering strictly, rejecting
any combination or order of filters that cannot be supported. If
implementations choose a strict interpretation of filter ordering, they
MUST clearly document that behavior.

SHOULD 而不是 MUST 也就是說,列表順序是建議,實作有裁量空間。這跟 Day 6 的裁決階梯(MUST)是完全不同層級的約束,值得實測。

設計方式與 Day 6 相同——兩條規則的 Filter 內容一模一樣,只把順序對調:

# day07/route-order.yaml
    - matches: [{ path: { type: PathPrefix, value: /ord-a } }]
      filters:
        - type: URLRewrite          # 先改寫
          urlRewrite: { path: { type: ReplaceFullPath, replaceFullPath: /rewritten-a } }
        - type: RequestMirror       # 後鏡像 → 鏡像端應收到 /rewritten-a
          requestMirror: { backendRef: { name: echo-mirror, port: 80 } }
      backendRefs: [{ name: echo-a, port: 80 }]

    - matches: [{ path: { type: PathPrefix, value: /ord-b } }]
      filters:
        - type: RequestMirror       # 先鏡像 → 鏡像端「應該」收到原始的 /ord-b
          requestMirror: { backendRef: { name: echo-mirror, port: 80 } }
        - type: URLRewrite          # 後改寫
          urlRewrite: { path: { type: ReplaceFullPath, replaceFullPath: /rewritten-b } }
      backendRefs: [{ name: echo-b, port: 80 }]
curl -s -o /dev/null -H "Host: order.localhost" 127.0.0.1/ord-a
curl -s -o /dev/null -H "Host: order.localhost" 127.0.0.1/ord-b
kubectl -n demo logs deploy/echo-mirror --tail=2
2026/08/08 16:08:51 10.244.0.3:59352 - - [.../Aug/2026:16:08:51 +0000] "GET /rewritten-a HTTP/1.1" - -
2026/08/08 16:08:51 10.244.0.3:59368 - - [.../Aug/2026:16:08:51 +0000] "GET /rewritten-b HTTP/1.1" - -

兩題都是改寫後的路徑。 /ord-bRequestMirror 寫在前面,鏡像端仍然收到 /rewritten-b

結論:Envoy Gateway v1.8.0 對這組 Filter 採固定順序——URLRewrite 恆先於 RequestMirror,不隨列表順序改變。這不是 bug,規格在這裡用的是 SHOULD,實作有裁量權。

但對使用者的意義很實際:如果你的鏡像端是用來比對「使用者原本打了什麼」,這個設定在 Envoy Gateway 上拿不到原始路徑。 想要原始路徑,得靠別的方式保存(例如在改寫前用 RequestHeaderModifier 把原始資訊寫進標頭),而不是靠調整 Filter 的排列順序。

這也是 Day 1 提的取捨在 Filter 層的具體樣貌:核心行為可攜,執行細節不保證。


實作三:互斥組合是誰擋下的

規格說 URLRewriteRequestRedirect 不能併用,並建議實作用 IncompatibleFilters 這個 reason 回報。那就寫一份違規的:

# day07/route-invalid.yaml
      filters:
        - type: URLRewrite
          urlRewrite: { path: { type: ReplaceFullPath, replaceFullPath: /x } }
        - type: RequestRedirect
          requestRedirect: { statusCode: 302 }
kubectl apply -f day07/route-invalid.yaml
The HTTPRoute "bad-filters" is invalid: spec.rules[0].filters: Invalid value:
May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite,
but not both

根本沒進到叢集。 這不是 controller 事後在 status 回報 Accepted=False,而是 CRD 上的 CEL 驗證規則在 API Server 收下請求的當下就拒絕了寫入

kubectl -n demo get httproute bad-filters
Error from server (NotFound): httproutes.gateway.networking.k8s.io "bad-filters" not found

這正是 Day 1 講「把路由行為定義成結構化欄位」的第一項收益——API Server 在套用當下就能擋下錯誤——在 Filter 層的實證。錯誤攔截點的差別很實際:

攔截層 何時知道 資源狀態
CEL 驗證(API Server) kubectl apply 當下 沒有被建立
controller 狀態回報 幾秒後查 status 已存在,但不生效
打流量才發現 上線後 已存在且行為錯誤

規格建議的 IncompatibleFilters 是給第二層用的。這一組因為能在 schema 層面表達,所以被推到了更前面的第一層——能靜態擋下的就不要留到執行期


上一篇
# Day 6|HTTPRoute 匹配全解:path、header、query、method
系列文
Kubernetes ingress架構7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言