Gateway API: Kế Nhiệm Của Ingress

K
Kai··6 min read

Bài 48 dừng ở chỗ Ingress đã đóng băng: định tuyến theo header, chia lưu lượng theo phần trăm, hay tách quyền giữa người quản hạ tầng và người viết ứng dụng — những thứ này sẽ không bao giờ vào được Ingress. Gateway API là nơi chúng được làm. Đây là API kế nhiệm mà SIG Network khuyến nghị, vẫn chạy trên cùng Cilium ta đã dùng cho Ingress ở bài trước.

Ba object thay cho một

Ingress nhồi mọi thứ vào một object. Gateway API tách thành ba, theo đúng ba vai trò khác nhau trong một tổ chức:

   GatewayClass   ── ai cài controller (như Cilium) đăng ký; giống IngressClass
        │
        ▼
   Gateway        ── cluster operator mở một "cổng": listener port 80/443, host nào
        │             được phép gắn route. Đây là nơi cấp địa chỉ + chứng chỉ TLS.
        ▼
   HTTPRoute      ── app team gắn route vào Gateway: host, path, header → backend Service.
                     Mỗi team quản route của mình mà không đụng Gateway chung.

Sự tách này giải quyết một vấn đề thật của Ingress: trong Ingress, người viết ứng dụng phải sửa cùng một object với người quản TLS và địa chỉ. Gateway API cho phép người quản hạ tầng giữ Gateway, còn các team chỉ tạo HTTPRoute trỏ vào đó.

Bật Gateway API trên Cilium

Khác Ingress, Gateway API không có CRD sẵn trong Kubernetes — phải cài riêng. Cilium 1.19 cần CRD chuẩn (standard channel) v1.4.1:

BASE=https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.4.1/config/crd/standard
for r in gatewayclasses gateways httproutes referencegrants grpcroutes; do
  kubectl apply -f $BASE/gateway.networking.k8s.io_${r}.yaml
done

Rồi bật trên Cilium (vẫn --reuse-values để giữ cấu hình kube-proxy-less + ingress của các bài trước), và restart operator + agent như bài 48 — helm upgrade chỉ sửa ConfigMap, pod không tự nạp lại:

helm upgrade cilium cilium/cilium --version 1.19.4 -n kube-system --reuse-values \
  --set gatewayAPI.enabled=true
kubectl -n kube-system rollout restart deploy/cilium-operator ds/cilium

Cilium tự đăng ký một GatewayClass tên cilium:

kubectl get gatewayclass
NAME     CONTROLLER                     ACCEPTED   AGE
cilium   io.cilium/gateway-controller   True       23s

ACCEPTED True nghĩa là controller nhận class này. Gateway API yêu cầu kubeProxyReplacement=true và L7 proxy Envoy — cả hai đã có từ bài 46.

Gateway và HTTPRoute

Tạo một Gateway mở listener HTTP cổng 80, chỉ cho route trong cùng namespace gắn vào:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web-gw
  namespace: gw-demo
spec:
  gatewayClassName: cilium
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Same

Rồi một HTTPRoute gắn vào Gateway đó (parentRefs), định tuyến theo host apps.kkloud.local và path:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: fanout
  namespace: gw-demo
spec:
  parentRefs:
  - name: web-gw
  hostnames:
  - apps.kkloud.local
  rules:
  - matches:
    - path: {type: PathPrefix, value: /foo}
    backendRefs:
    - {name: foo, port: 80}
  - matches:
    - path: {type: PathPrefix, value: /bar}
    backendRefs:
    - {name: bar, port: 80}

Backend là hai Service foobar (mỗi cái một Deployment http-echo trả về chuỗi FOO/BAR, giống bài 48). Khi áp xong, Cilium tạo một Service cho Gateway và một listener Envoy:

kubectl -n gw-demo get gateway web-gw
kubectl -n gw-demo get svc | grep gateway
NAME     CLASS    ADDRESS   PROGRAMMED   AGE
web-gw   cilium             False        52s

NAME                    TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
cilium-gateway-web-gw   LoadBalancer   10.32.0.205   <pending>     80:30731/TCP   12s

Vì sao Gateway chưa Programmed

PROGRAMMED FalseEXTERNAL-IP <pending> không phải lỗi cấu hình — đó là hệ quả của chạy baremetal. Xem lý do cụ thể:

kubectl -n gw-demo get gateway web-gw -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=True Accepted
Programmed=False AddressNotAssigned

Gateway tạo một Service kiểu LoadBalancer và chờ được cấp địa chỉ ngoài. Cụm EC2 tự dựng không có cloud controller gán IP, cũng chưa bật LB IPAM của Cilium (để dành bài 50), nên Service mãi <pending> và Gateway báo AddressNotAssigned. Nhưng data plane đã sẵn sàng — listener Envoy đã lập:

kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg envoy admin listeners | grep gateway
gw-demo/cilium-gateway-web-gw/listener::127.0.0.1:17803

Service LoadBalancer luôn được cấp kèm một NodePort (ở đây 30731), nên ta test qua đó. Nhớ bài 48: NodePort của Cilium không nghe trên loopback, phải dùng IP node.

Định tuyến và chia lưu lượng

Test từ một worker, gửi request với Host header:

IP=10.0.1.20 ; NP=30731
curl -s -H "Host: apps.kkloud.local" $IP:$NP/foo
curl -s -H "Host: apps.kkloud.local" $IP:$NP/bar
curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Host: apps.kkloud.local" $IP:$NP/baz
FOO          # /foo -> Service foo
BAR          # /bar -> Service bar
HTTP 404     # path không khớp rule

Đến đây Gateway API làm đúng việc của Ingress. Phần Ingress không làm được nằm ở đây: một HTTPRoute thứ hai chia lưu lượng giữa hai backend theo trọng số, không cần annotation riêng của từng controller:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: split
  namespace: gw-demo
spec:
  parentRefs:
  - name: web-gw
  hostnames:
  - split.kkloud.local
  rules:
  - backendRefs:
    - {name: foo, port: 80, weight: 80}
    - {name: bar, port: 80, weight: 20}

Gọi 20 lần rồi đếm:

for i in $(seq 20); do curl -s -H "Host: split.kkloud.local" $IP:$NP/; done | sort | uniq -c
   3 BAR
  17 FOO

17/3 trên 20 lần, bám theo tỉ lệ 80/20 đã khai. Đây là cách triển khai canary thẳng trong API chuẩn: hạ trọng số phiên bản cũ, nâng dần phiên bản mới, không phụ thuộc annotation của một controller cụ thể — vốn là điểm khiến cấu hình Ingress khó chuyển giữa các controller.

🧹 Dọn dẹp

kubectl delete namespace gw-demo        # xóa Gateway, 2 HTTPRoute, foo, bar

Service cilium-gateway-web-gw thuộc Gateway nên bị xóa theo namespace. Giữ lại Gateway API CRD và gatewayAPI.enabled trên Cilium — bài 50 dùng tiếp khi gán địa chỉ thật cho LoadBalancer. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 49-gateway-api.

Tổng kết

Gateway API tách lưu lượng vào thành ba object theo ba vai trò: GatewayClass (controller đăng ký, Cilium tạo class cilium), Gateway (mở listener, cấp địa chỉ + TLS), HTTPRoute (team gắn route theo host/path/header vào Gateway). Ta cài CRD v1.4.1, bật gatewayAPI.enabled trên Cilium (nhớ restart operator + agent), rồi dựng một Gateway HTTP cổng 80 với hai HTTPRoute: fanout theo path (/foo→foo, /bar→bar, path lạ → 404) và chia lưu lượng theo trọng số (80/20 → đếm thật ra 17/3). Trên baremetal, Gateway dừng ở Programmed=False / AddressNotAssigned vì Service LoadBalancer chưa có IP ngoài, nhưng listener Envoy đã chạy nên ta test qua NodePort kèm theo. So với Ingress: cùng định tuyến host/path, nhưng Gateway API thêm traffic splitting và tách vai trò ngay trong API chuẩn, không qua annotation riêng từng controller.

Còn một mảnh dở dang: Gateway và cả Service LoadBalancer của bài 48 đều chờ một địa chỉ IP ngoài mà cụm baremetal chưa cấp được. Bài 50 lấp chỗ đó — LB IPAM của Cilium gán IP cho Service LoadBalancer, cùng các lựa chọn định tuyến lưu lượng vào (externalTrafficPolicy, topology-aware routing) để hoàn thiện đường vào cụm.