ข้ามไปยังเนื้อหา

Ingress และ Gateway API

Ingress อธิบาย rule การ route HTTP(S) แบบ Layer-7 เพื่อพา traffic ภายนอกไปหา Service ภายใน แต่ rule พวกนั้นจะไม่ทำงานเลยจนกว่าจะมี Ingress controller รันอยู่จริงเพื่อ implement rule พวกนั้น

LoadBalancer Service ใช้ได้ดีกับแอปเดียว แต่การ provision cloud load balancer แยกต่อแอปนั้นแพง แถมยัง route ตาม hostname หรือ URL path ไม่ได้ด้วย Ingress แก้ปัญหานี้ที่ Layer 7 จุดเข้าเดียวที่ตรวจ HTTP request แล้ว route ตาม host กับ path

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-svc
port:
number: 80

manifest นั้นแสดงสองอย่างพร้อมกัน host-based routing (มีแค่ request ที่ยิงไปหา shop.example.com เท่านั้นที่ match rule นี้) และ path-based routing (/api ไปที่ Service หนึ่ง ที่เหลือทั้งหมดไปอีก Service หนึ่ง)

ตรงนี้คือจุดที่คนสับสนกันบ่อย การสร้าง Ingress object เฉย ๆ ไม่ทำให้ traffic ถูก route ไปไหนเลยด้วยตัวเอง Ingress เป็นแค่การประกาศ intent เท่านั้น ต้องพึ่ง Ingress controller ที่เป็น software แยกต่างหากที่รันอยู่จริงใน cluster (เช่น controller ที่ใช้ nginx, controller ของ cloud provider หรือตัวอื่น ๆ) คอย watch Ingress object แล้วเขียน proxy หรือ load balancer จริงให้ตรงตามนั้น ถ้าไม่มี controller รันอยู่ Ingress resource ก็แค่นอนเฉย ๆ ไม่มีผลอะไร

Terminal window
# An Ingress with no controller watching it accomplishes nothing
kubectl get ingress shop
kubectl get pods -n ingress-nginx # this is what actually does the routing

เวลามี controller ติดตั้งมากกว่าหนึ่งตัวใน cluster ingressClassName (ที่อิงกับ IngressClass object) จะบอก Kubernetes ว่า controller ตัวไหนควร implement Ingress ตัวนั้น

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
spec:
controller: k8s.io/ingress-nginx

Gateway API เป็นโมเดลใหม่กว่าที่แก้ปัญหาเดียวกัน แต่แยก role ชัดเจน สร้างจากสาม resource แทนที่จะเป็นตัวเดียว

  • GatewayClass implementation คล้าย ๆ กับ IngressClass (มาจาก vendor ของ controller)
  • Gateway listener จุดเข้าจริง (protocol, port, hostname) ปกติดูแลโดยทีม platform/infrastructure
  • HTTPRoute (และ GRPCRoute) rule การ route ที่ผูกกับ Gateway ปกติดูแลโดยทีม application
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gateway
spec:
gatewayClassName: example-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-svc
port: 80

การแยกแบบนี้มีประโยชน์จริง ทีม platform เป็นเจ้าของและคุม Gateway ได้ (ว่าเปิด port ไหน TLS certificate ไหน) ส่วนแต่ละทีม application เป็นเจ้าของ HTTPRoute ของตัวเองได้อย่างปลอดภัย โดยไม่ต้องมีสิทธิ์เข้าถึง listener ที่ใช้ร่วมกันเลย Gateway, GatewayClass และ HTTPRoute (พร้อมกับ GRPCRoute) เข้าสู่ General Availability แล้วที่ gateway.networking.k8s.io/v1 และนี่ไม่ใช่แค่เทรนด์ของ ecosystem แต่เป็นคำแนะนำอย่างเป็นทางการจากโปรเจกต์ Kubernetes เอง — Ingress API ถูก “แช่แข็ง” แล้ว (ยังเป็น GA ใช้ได้ ไม่ได้ถูกถอดออก แต่จะไม่มีฟีเจอร์ใหม่เพิ่มอีก) และเอกสารทางการก็บอกตรง ๆ ให้ใช้ Gateway API แทน Ingress สำหรับงานใหม่

คำแนะนำนี้ไม่ใช่แค่พูดลอย ๆ ingress-nginx ซึ่งเป็น Ingress controller ที่ถูกใช้แพร่หลายที่สุด ประกาศ retire ตัวเองไปเมื่อพฤศจิกายน 2025 จะดูแลแบบ best-effort ถึงแค่ มีนาคม 2026 เท่านั้น หลังจากนั้นจะไม่มี release, bugfix หรือแพตช์ความปลอดภัยใหม่อีก การติดตั้งเดิมยังทำงานต่อได้ และ Helm chart/image ยังอยู่ครบ แต่ cluster ใหม่วันนี้ควรเลือก Gateway API implementation แทนการติดตั้ง ingress-nginx

Gateway API พิสูจน์จริงตั้งแต่ต้นจนจบ: GatewayClass, HTTPRoute แบบ weighted, filters

หัวข้อที่มีชื่อว่า “Gateway API พิสูจน์จริงตั้งแต่ต้นจนจบ: GatewayClass, HTTPRoute แบบ weighted, filters”

Gateway API controller ตัวจริง — NGINX Gateway Fabric — ถูกติดตั้งบน kind cluster ของโมดูลนี้เองผ่าน Helm chart ทางการ (helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginx.service.type=NodePort ตาม เอกสารติดตั้ง NGINX Gateway Fabric) บน Gateway API standard channel CRD v1.6.2 GatewayClass จริงโผล่มาทันที:

$ kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
nginx gateway.nginx.org/nginx-gateway-controller True 16s
k8s/ingress-and-gateway-api/gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: demo-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
$ kubectl get gateway demo-gateway
NAME CLASS ADDRESS PROGRAMMED AGE
demo-gateway nginx 10.96.71.123 True 18s

PROGRAMMED: True พร้อม ADDRESS จริง — NGF สร้าง Deployment กับ Service ของ data plane nginx จริงเบื้องหลัง Gateway นี้ ต่อสาย weighted backendRefs กับ header filter:

k8s/ingress-and-gateway-api/httproute-weighted.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
spec:
parentRefs:
- name: demo-gateway
hostnames:
- 'shop.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /api
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: X-Routed-Via
value: gateway-api
backendRefs:
- name: api-v1
port: 80
weight: 80
- name: api-v2
port: 80
weight: 20

weighted split จริง จากในตัว cluster เอง curl ไปที่ Service IP ของ Gateway ตรง ๆ พร้อม header Host ที่ route ถูก — 50 request:

$ for i in $(seq 1 50); do wget -q -T 3 -O- --header="Host: shop.example.com" http://10.96.71.123/api; echo; done | sort | uniq -c
44 v1
6 v2

44/6 จาก 50 (88%/12%) ใกล้เคียง 80/20 ที่ config ไว้ แต่ไม่เป๊ะ — สาธิตจริง ๆ อย่างตรงไปตรงมาว่า weight ตั้ง อัตราส่วน ที่ implementation สุ่มตาม ไม่ใช่การรับประกัน split แม่นเป๊ะที่ request volume ต่ำ ตัวอย่างที่ใหญ่กว่านี้มากจะเข้าใกล้ 80/20 มากขึ้น นี่คือความต่างจริงระหว่าง “weight” กับ “percentage” ที่ตาราง ข้อแลกเปลี่ยน ด้านล่างของบทนี้สร้างขึ้นจาก data จริงนี้เลย

# excerpt: second rule of k8s/ingress-and-gateway-api/httproute-weighted.yaml above
- matches:
- path:
type: PathPrefix
value: /old
filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new
statusCode: 301
$ wget -T 3 -S --header="Host: shop.example.com" http://10.96.71.123/old
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: http://shop.example.com:80/new

301 จริงพร้อม header Location ที่ถูกเขียนใหม่ตรงตาม ReplacePrefixMatch: /new เป๊ะ — พิสูจน์ว่า filter ทำงานตามที่เอกสารบอกจริง ไม่ต้องมี backend เกี่ยวข้องกับ rule นี้เลย

ความพยายามครั้งแรกของ rule redirect ด้านบนยังมี backendRefs เหลือติดมา (copy มาจาก rule แบบ weighted) — API server reject ตรง ๆ ทันที ไม่ได้เพิกเฉยต่อ conflict นี้เลย:

# k8s/ingress-and-gateway-api/httproute-bad-redirect.yaml — @expect-error RequestRedirect filter combined with backendRefs in the same rule; real API server rejection
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route-bad
spec:
parentRefs:
- name: demo-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /old
filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new
statusCode: 301
backendRefs:
- name: api-v1
port: 80
$ kubectl apply -f httproute-bad-redirect.yaml
The HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefs

Invalid rejection จริงทันที — validation ของ Gateway API เอง (ValidatingAdmissionPolicy ตาม CRD ที่ติดตั้งไว้ด้านบน) จับ shape mismatch นี้ได้ตอน apply เลย ซึ่งเป็นข้อผิดพลาดแบบที่โค้ด AI สร้างมักเจอพอดี: rule แบบ redirect ไม่ route ไปหา backend เลย ดังนั้น backendRefs ที่ copy มาจาก rule ข้าง ๆ ไม่มีเหตุผลเลย และ API server บอกตรง ๆ แทนที่จะเงียบ ๆ เก็บไว้ (หรือเงียบ ๆ ทิ้ง) ตัวใดตัวหนึ่ง

k8s/ingress-and-gateway-api/referencegrant.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-other-ns
namespace: backend-ns
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: default
to:
- group: ''
kind: Service
$ kubectl apply -f referencegrant.yaml
referencegrant.gateway.networking.k8s.io/allow-gateway-to-other-ns created

object จริงนี้ที่ apply สำเร็จคือสิ่งที่จะให้ HTTPRoute ใน default reference Service ที่อยู่ใน backend-ns ได้ — ถ้าไม่มีอันนี้ Gateway API จะ reject backendRef ข้าม namespace ตรง ๆ ต่างจาก Ingress (ที่ backend reference ข้าม namespace ได้แบบ implicit ตามที่ controller อนุญาต) Gateway API บังคับให้ opt-in ชัดเจนแบบนี้จากฝั่ง namespace ปลายทาง — เป็นการรัดความปลอดภัยที่ตั้งใจทำ

$ kubectl apply --dry-run=server -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: demo-grpc
spec:
parentRefs:
- name: demo-gateway
rules:
- matches:
- method:
service: demo.v1.DemoService
method: Ping
backendRefs:
- name: api-v1
port: 80
EOF
grpcroute.gateway.networking.k8s.io/demo-grpc created (server dry run)

GRPCRoute เป็นของจริงและ GA แล้วบน Gateway API v1.6.2 ของ cluster นี้ — Gateway/GatewayClass ตัวเดียวกันรองรับ routing ทั้ง HTTP และ gRPC เลย แค่ match ด้วย service/method แทน path

ตอนรีวิว manifest ของ Ingress/Gateway API ที่ AI สร้าง ให้เช็ค

  • Ingress แบบ networking.k8s.io/v1beta1 ที่ใช้ serviceName/servicePort — เป็นรูปแบบก่อน 1.19 Ingress ปัจจุบันคือ networking.k8s.io/v1 พร้อม backend.service.name/backend.service.port.number API version และรูปแบบ field เก่าถูกถอดออกไปนานแล้ว cluster จริงจะ reject
  • filter RequestRedirect ที่มี backendRefs เหลือติดมาใน rule เดียวกัน — พิสูจน์ด้านบนแล้วว่า reject จริงทันที (RequestRedirect filter must not be used together with backendRefs)
  • backendRefs weight ที่คิดว่าเป็นเปอร์เซ็นต์รับประกันทุก request — พิสูจน์ด้านบนแล้วว่าตัวอย่างจริง 50 request ได้ 88/12 จาก config 80/20 ไม่เป๊ะ weight คืออัตราส่วนที่ implementation สุ่มตาม
  • backendRef ข้าม namespace ที่ไม่มี ReferenceGrant คู่กันใน namespace ปลายทาง คิดว่า “ก็น่าจะใช้ได้” เหมือน Ingress backend ใน namespace เดียวกัน — Gateway API บังคับ opt-in ชัดเจนตามที่พิสูจน์ด้านบน
  • cluster ใหม่ที่ตั้งค่า Ingress เอื้อมไปหา ingress-nginx ตามความเคยชิน โดยไม่รู้เรื่องการ retire มีนาคม 2026 (คุยไว้แล้วด้านบน)

🐛 หา bug ในโค้ดนี้

k8s/ingress-and-gateway-api/legacy-ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shop-legacy
spec:
rules:
- host: shop.example.com
http:
paths:
- path: /api
backend:
serviceName: api-svc
servicePort: 80
เฉลย

manifest นี้ใช้ apiVersion: networking.k8s.io/v1beta1 พร้อม backend.serviceName/backend.servicePort — รูปแบบ Ingress ก่อน Kubernetes 1.19 API version นั้นถูกถอดออกไปหลายปีแล้ว apply เข้า cluster ปัจจุบัน (1.22+) จะได้ error จริง no matches for kind "Ingress" in version "networking.k8s.io/v1beta1" เพราะ API server ไม่ serve group/version นั้นเลยแล้ว — นี่ไม่ใช่แค่ deprecation warning แต่คือการถอดออกแบบเด็ดขาด ทางแก้: ใช้ networking.k8s.io/v1 ที่ต้องมี pathType และเขียน backend เป็น backend.service.name/backend.service.port.number (หรือตามคำแนะนำของบทนี้เอง เมื่อ Ingress API แช่แข็งแล้วและ ingress-nginx กำลัง retire ให้ย้ายไป Gateway API HTTPRoute แทนที่จะแก้ในที่เดิม)

Gateway API ตั้งใจแยกสิ่งที่ Ingress รวมไว้เป็น object เดียวออกจากกัน: ใครเป็นเจ้าของ listener ที่ share กัน (Gateway ทีม platform) กลายเป็นคนละ object กับ RBAC คนละชุด จาก ใครเป็นเจ้าของ routing rule (HTTPRoute ทีม application) — และ cross-namespace reference ต้อง opt-in ผ่าน ReferenceGrant (พิสูจน์ด้านบน) แทนที่จะ implicit เป็นการรัดความปลอดภัยที่ตั้งใจทำเทียบกับโมเดลที่หลวมกว่าของ Ingress weight บน backendRef คืออัตราส่วนสุ่มที่ data plane ทำตามความน่าจะเป็น ไม่ใช่การรับประกันเปอร์เซ็นต์ต่อ request — พิสูจน์ด้านบนด้วยตัวเลขจริงที่ไม่เป๊ะแต่เข้าใกล้

ทางเลือกข้อดีต้นทุน
Ingressobject เดียว ง่าย รองรับกว้างขวางวันนี้API แช่แข็งแล้ว (ไม่มีฟีเจอร์ใหม่) controller ที่ใช้มากสุดกำลัง retire มีนาคม 2026
Gateway APIยืดหยุ่น (weight, header/path match, filter, GRPCRoute พิสูจน์ด้านบน) แยก role ชัดเจนobject เยอะกว่าต้องคิดตาม; cross-namespace ต้องมี ReferenceGrant ชัดเจน พิสูจน์ด้านบน
backendRefs weightsplit traffic จริงสำหรับ canary โดยไม่ต้องมี service mesh พิสูจน์ด้านบนเข้าใกล้อัตราส่วนที่ config ไว้แค่ตอน request volume สูงพอ พิสูจน์ด้านบนด้วยตัวอย่างจริง 88/12 จาก config 80/20
filter RequestRedirect301 redirect จริงพร้อม path rewrite พิสูจน์ด้านบน ไม่ต้องมี backendรวมกับ backendRefs ใน rule เดียวกันไม่ได้ — reject จริงทันที พิสูจน์ด้านบน
  • “Ingress กับ Gateway API ใช้แทนกันได้ เลือกอันไหนก็ได้” โปรเจกต์ Kubernetes เองแนะนำ Gateway API สำหรับงานใหม่แล้ว (Ingress แช่แข็ง, ingress-nginx retire) — อ้างอิงไว้แล้วในบทนี้
  • “HTTPRoute ใน namespace A reference Service ใน namespace B ได้เหมือน namespace เดียวกัน” พิสูจน์ด้านบนแล้ว — ต้องมี ReferenceGrant ชัดเจนใน namespace ปลายทาง ไม่งั้น reference ถูก reject
  • “ค่า weight ของ backendRefs คือเปอร์เซ็นต์แม่นเป๊ะ” พิสูจน์ด้านบนด้วยตัวอย่างจริง — เป็น weight สัมพัทธ์ที่สุ่มตามความน่าจะเป็น จำนวน request น้อยอาจทำให้ผลต่างจาก config ได้ชัดเจน
  • “filter RequestRedirect ชี้ไป backend ได้ด้วยเผื่อเป็น fallback” พิสูจน์ด้านบนแล้ว — API server reject RequestRedirect ที่รวมกับ backendRefs ใน rule เดียวกันตรง ๆ
flowchart LR
  ext["External client"] --> ctrl["Ingress controller\n(e.g. nginx)"]
  ctrl -->|"host/path rules"| ing["Ingress object"]
  ing --> svcA["Service: api-svc"] --> podA["Pods"]
  ing --> svcB["Service: frontend-svc"] --> podB["Pods"]
Traffic ภายนอกผ่าน Ingress controller ไปยัง Service และ Pod ภายใน
ทำไมการสร้าง Ingress object เดี่ยว ๆ ถึงไม่ทำให้ traffic ถูก route
host-based routing กับ path-based routing ใน Ingress rule ต่างกันยังไง
ใน Gateway API HTTPRoute ทำหน้าที่อะไร
GatewayClass, Gateway และ HTTPRoute สัมพันธ์กันยังไง
นี่คือ rejection จริงจาก API server เกิดจากอะไร
The HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefs
นี่คือผลจริงจาก HTTPRoute ที่มี backendRefs weight: 80 (api-v1) กับ weight: 20 (api-v2) การได้ 44/6 (88%/12%) แทนที่จะเป๊ะ 40/10 (80%/20%) พิสูจน์อะไร
$ for i in $(seq 1 50); do wget ...; done | sort | uniq -c
     44 v1
      6 v2