Ingress และ Gateway API
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Ingress อธิบาย rule การ route HTTP(S) แบบ Layer-7 เพื่อพา traffic ภายนอกไปหา Service ภายใน แต่ rule พวกนั้นจะไม่ทำงานเลยจนกว่าจะมี Ingress controller รันอยู่จริงเพื่อ implement rule พวกนั้น
Ingress: rule การ route ไม่ใช่ router
หัวข้อที่มีชื่อว่า “Ingress: rule การ route ไม่ใช่ router”LoadBalancer Service ใช้ได้ดีกับแอปเดียว แต่การ provision cloud load balancer แยกต่อแอปนั้นแพง แถมยัง route ตาม hostname หรือ URL path ไม่ได้ด้วย Ingress แก้ปัญหานี้ที่ Layer 7 จุดเข้าเดียวที่ตรวจ HTTP request แล้ว route ตาม host กับ path
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: 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: 80manifest นั้นแสดงสองอย่างพร้อมกัน host-based routing (มีแค่ request ที่ยิงไปหา shop.example.com เท่านั้นที่ match rule นี้) และ path-based routing (/api ไปที่ Service หนึ่ง ที่เหลือทั้งหมดไปอีก Service หนึ่ง)
Ingress object เดี่ยว ๆ ไม่ทำอะไรเลย
หัวข้อที่มีชื่อว่า “Ingress object เดี่ยว ๆ ไม่ทำอะไรเลย”ตรงนี้คือจุดที่คนสับสนกันบ่อย การสร้าง Ingress object เฉย ๆ ไม่ทำให้ traffic ถูก route ไปไหนเลยด้วยตัวเอง Ingress เป็นแค่การประกาศ intent เท่านั้น ต้องพึ่ง Ingress controller ที่เป็น software แยกต่างหากที่รันอยู่จริงใน cluster (เช่น controller ที่ใช้ nginx, controller ของ cloud provider หรือตัวอื่น ๆ) คอย watch Ingress object แล้วเขียน proxy หรือ load balancer จริงให้ตรงตามนั้น ถ้าไม่มี controller รันอยู่ Ingress resource ก็แค่นอนเฉย ๆ ไม่มีผลอะไร
# An Ingress with no controller watching it accomplishes nothingkubectl get ingress shopkubectl 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/v1kind: IngressClassmetadata: name: nginxspec: controller: k8s.io/ingress-nginxGateway API: ตัวต่อยอดที่ยืดหยุ่นกว่า
หัวข้อที่มีชื่อว่า “Gateway API: ตัวต่อยอดที่ยืดหยุ่นกว่า”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/v1kind: Gatewaymetadata: name: shop-gatewayspec: gatewayClassName: example-gateway-class listeners: - name: http protocol: HTTP port: 80---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-routespec: 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 gatewayclassNAME CONTROLLER ACCEPTED AGEnginx gateway.nginx.org/nginx-gateway-controller True 16sapiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: demo-gatewayspec: gatewayClassName: nginx listeners: - name: http protocol: HTTP port: 80$ kubectl get gateway demo-gatewayNAME CLASS ADDRESS PROGRAMMED AGEdemo-gateway nginx 10.96.71.123 True 18sPROGRAMMED: True พร้อม ADDRESS จริง — NGF สร้าง Deployment กับ Service ของ data plane nginx จริงเบื้องหลัง Gateway นี้ ต่อสาย weighted backendRefs กับ header filter:
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-routespec: 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: 20weighted 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 v244/6 จาก 50 (88%/12%) ใกล้เคียง 80/20 ที่ config ไว้ แต่ไม่เป๊ะ — สาธิตจริง ๆ อย่างตรงไปตรงมาว่า weight ตั้ง อัตราส่วน ที่ implementation สุ่มตาม ไม่ใช่การรับประกัน split แม่นเป๊ะที่ request volume ต่ำ ตัวอย่างที่ใหญ่กว่านี้มากจะเข้าใกล้ 80/20 มากขึ้น นี่คือความต่างจริงระหว่าง “weight” กับ “percentage” ที่ตาราง ข้อแลกเปลี่ยน ด้านล่างของบทนี้สร้างขึ้นจาก data จริงนี้เลย
filter RequestRedirect พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “filter RequestRedirect พิสูจน์แล้ว”# 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/new301 จริงพร้อม header Location ที่ถูกเขียนใหม่ตรงตาม ReplacePrefixMatch: /new เป๊ะ — พิสูจน์ว่า filter ทำงานตามที่เอกสารบอกจริง ไม่ต้องมี backend เกี่ยวข้องกับ rule นี้เลย
RequestRedirect กับ backendRefs พร้อมกัน: reject จริงทันที
หัวข้อที่มีชื่อว่า “RequestRedirect กับ backendRefs พร้อมกัน: reject จริงทันที”ความพยายามครั้งแรกของ 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 rejectionapiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-route-badspec: 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.yamlThe HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefsInvalid rejection จริงทันที — validation ของ Gateway API เอง (ValidatingAdmissionPolicy ตาม CRD ที่ติดตั้งไว้ด้านบน) จับ shape mismatch นี้ได้ตอน apply เลย ซึ่งเป็นข้อผิดพลาดแบบที่โค้ด AI สร้างมักเจอพอดี: rule แบบ redirect ไม่ route ไปหา backend เลย ดังนั้น backendRefs ที่ copy มาจาก rule ข้าง ๆ ไม่มีเหตุผลเลย และ API server บอกตรง ๆ แทนที่จะเงียบ ๆ เก็บไว้ (หรือเงียบ ๆ ทิ้ง) ตัวใดตัวหนึ่ง
ReferenceGrant: จริง ข้าม namespace
หัวข้อที่มีชื่อว่า “ReferenceGrant: จริง ข้าม namespace”apiVersion: gateway.networking.k8s.io/v1beta1kind: ReferenceGrantmetadata: name: allow-gateway-to-other-ns namespace: backend-nsspec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: default to: - group: '' kind: Service$ kubectl apply -f referencegrant.yamlreferencegrant.gateway.networking.k8s.io/allow-gateway-to-other-ns createdobject จริงนี้ที่ apply สำเร็จคือสิ่งที่จะให้ HTTPRoute ใน default reference Service ที่อยู่ใน backend-ns ได้ — ถ้าไม่มีอันนี้ Gateway API จะ reject backendRef ข้าม namespace ตรง ๆ ต่างจาก Ingress (ที่ backend reference ข้าม namespace ได้แบบ implicit ตามที่ controller อนุญาต) Gateway API บังคับให้ opt-in ชัดเจนแบบนี้จากฝั่ง namespace ปลายทาง — เป็นการรัดความปลอดภัยที่ตั้งใจทำ
GRPCRoute dry-run จริง
หัวข้อที่มีชื่อว่า “GRPCRoute dry-run จริง”$ kubectl apply --dry-run=server -f - <<'EOF'apiVersion: gateway.networking.k8s.io/v1kind: GRPCRoutemetadata: name: demo-grpcspec: parentRefs: - name: demo-gateway rules: - matches: - method: service: demo.v1.DemoService method: Ping backendRefs: - name: api-v1 port: 80EOFgrpcroute.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
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนรีวิว 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.numberAPI version และรูปแบบ field เก่าถูกถอดออกไปนานแล้ว cluster จริงจะ reject - filter
RequestRedirectที่มีbackendRefsเหลือติดมาใน rule เดียวกัน — พิสูจน์ด้านบนแล้วว่า reject จริงทันที (RequestRedirect filter must not be used together with backendRefs) backendRefsweight ที่คิดว่าเป็นเปอร์เซ็นต์รับประกันทุก 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 ในโค้ดนี้
apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:name: shop-legacyspec: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 แทนที่จะแก้ในที่เดิม)
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 — พิสูจน์ด้านบนด้วยตัวเลขจริงที่ไม่เป๊ะแต่เข้าใกล้
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| Ingress | object เดียว ง่าย รองรับกว้างขวางวันนี้ | API แช่แข็งแล้ว (ไม่มีฟีเจอร์ใหม่) controller ที่ใช้มากสุดกำลัง retire มีนาคม 2026 |
| Gateway API | ยืดหยุ่น (weight, header/path match, filter, GRPCRoute พิสูจน์ด้านบน) แยก role ชัดเจน | object เยอะกว่าต้องคิดตาม; cross-namespace ต้องมี ReferenceGrant ชัดเจน พิสูจน์ด้านบน |
backendRefs weight | split traffic จริงสำหรับ canary โดยไม่ต้องมี service mesh พิสูจน์ด้านบน | เข้าใกล้อัตราส่วนที่ config ไว้แค่ตอน request volume สูงพอ พิสูจน์ด้านบนด้วยตัวอย่างจริง 88/12 จาก config 80/20 |
filter RequestRedirect | 301 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"]