| 元件 | 版本 |
|---|---|
| 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 這個型別化欄位。今天要驗證的是三件事:
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.
同型別不能重複寫兩次;URLRewrite 與 RequestRedirect 互斥。後者稍後會實測「互斥」到底是誰在把關。
# 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
set 與 add 的差別值得停一下:set 是覆蓋(用戶端送什麼都不算數),add 是附加(同名標頭會變成多值)。要防止用戶端偽造內部標頭,必須用 set 或 remove,不能用 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,後端只看到 /items。ReplacePrefixMatch 換掉的是被 matches 匹配到的那段前綴,不是字串取代。X-Tenant: acme——用戶端送的 wrong-value 被 set 蓋掉了。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 的回應沒有可拿掉的標頭。
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 比對)。
- 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。
鏡像的用途是拿真實流量壓測新版,但不影響使用者——鏡像端的回應會被丟棄,慢了、掛了、回 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——鏡像收到的是改寫後的路徑。這件事接下來會變成一個實驗。
規格對順序的用字很微妙:
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-b 把 RequestMirror 寫在前面,鏡像端仍然收到 /rewritten-b。
結論:Envoy Gateway v1.8.0 對這組 Filter 採固定順序——URLRewrite 恆先於 RequestMirror,不隨列表順序改變。這不是 bug,規格在這裡用的是 SHOULD,實作有裁量權。
但對使用者的意義很實際:如果你的鏡像端是用來比對「使用者原本打了什麼」,這個設定在 Envoy Gateway 上拿不到原始路徑。 想要原始路徑,得靠別的方式保存(例如在改寫前用 RequestHeaderModifier 把原始資訊寫進標頭),而不是靠調整 Filter 的排列順序。
這也是 Day 1 提的取捨在 Filter 層的具體樣貌:核心行為可攜,執行細節不保證。
規格說 URLRewrite 與 RequestRedirect 不能併用,並建議實作用 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 層面表達,所以被推到了更前面的第一層——能靜態擋下的就不要留到執行期。