Sau khi rời ingress-nginx, bài toán lớn nhất của Gateway API trên một cluster nhiều website là chứng chỉ: nhiều site cùng ghi vào một Gateway thì ghi đè nhau. NGINX Gateway Fabric cùng ListenerSet giải quyết đúng chỗ đó. Bài này mô tả thiết kế một Gateway, một load balancer cho mọi website trên LKE, mỗi site tự khai hostname và chứng chỉ bằng ListenerSet, cert-manager 1.21 cấp chứng chỉ qua listener HTTP dùng chung, cùng các lỗi hay gặp khi triển khai. Đây là bài kết Series 2, nên phần cuối nhìn lại những gì series đã đi qua.

1. Vì sao NGINX Gateway Fabric và ListenerSet

Bài trước, ingress-nginx nghỉ hưu: pilot Traefik, Gateway API và rollback, dừng ở vấn đề certificateRefs: danh sách chứng chỉ của một listener là trường atomic, nhiều site cùng ghi vào một Gateway thì ghi đè nhau.

ListenerSet (GEP-1713 trong Gateway API) cho phép một namespace khai thêm listener cho một Gateway có sẵn mà không sửa Gateway đó. Mỗi site có một ListenerSet trong namespace của nó, chứa hostname và Secret TLS cùng namespace. Không còn ai cùng ghi một danh sách, cũng không cần ReferenceGrant để Gateway đọc Secret ở namespace khác.

NGINX Gateway Fabric có lõi là nginx, gần với kinh nghiệm của đội đã vận hành Nginx trên VPS nhiều năm. Các phiên bản cần khớp nhau:

Thành phầnPhiên bảnLý do
NGINX Gateway Fabric2.8 trở lênCó ListenerSet, yêu cầu Kubernetes từ 1.32
CRD Gateway API1.6.3, kênh standardCần CRD listenersets.gateway.networking.k8s.io
cert-manager1.21 trở lênHỗ trợ ListenerSet từ 1.20, annotation http01-parentreffallback từ 1.21

Trong cert-manager, ListenerSet vẫn nằm sau feature gate ListenerSets và mặc định tắt. Phải bật cả config.gatewayAPI.enableListenerSet lẫn config.featureGates.ListenerSets khi cài, theo hướng dẫn cert-manager: Annotated Gateway resource.

2. Kiến trúc NGINX Gateway Fabric: một Gateway, một load balancer

Internet
  → NodeBalancer (TCP 80 + TCP 443, không terminate TLS)
    → Gateway public-gateway (namespace nginx-gateway)
        :80   HTTP dùng chung: redirect và challenge ACME HTTP-01
        :443  HTTPS placeholder (placeholder.invalid)
      → ListenerSet của từng site (namespace site): HTTPS :443, hostname + Secret TLS
        → HTTPRoute → Service → Pod

TLS được terminate tại NGINX Gateway Fabric. Đội hạ tầng giữ Gateway, ClusterIssuer và Secret placeholder, tạo một lần cho cả cluster. Chart web tạo ListenerSet, HTTPRoute, Deployment, Service trong namespace của site. Thêm hay gỡ một site không đụng vào Gateway.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public-gateway
  namespace: nginx-gateway
spec:
  gatewayClassName: nginx
  infrastructure:
    annotations:
      service.beta.kubernetes.io/linode-loadbalancer-default-protocol: tcp
  allowedListeners:
    namespaces: {from: All}          # ListenerSet ở namespace site gắn được
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes: {namespaces: {from: All}}
    - name: https
      protocol: HTTPS
      port: 443
      hostname: placeholder.invalid  # chỉ để load balancer mở cổng 443
      tls:
        mode: Terminate
        certificateRefs: [{kind: Secret, name: placeholder-tls}]
      allowedRoutes: {namespaces: {from: Same}}

Listener :443 với hostname .invalid là mẹo cần thiết trên LKE: NodeBalancer lấy danh sách cổng từ listener khai trên chính Gateway. Nếu Gateway chỉ có :80, Service và NodeBalancer chỉ mở cổng 80, HTTPS không vào được dù ListenerSet của site có listener 443. Hostname .invalid bảo đảm placeholder không bao giờ trùng domain thật khi chọn chứng chỉ theo SNI.

3. Mỗi site trên NGINX Gateway Fabric: ListenerSet, ba HTTPRoute và hai policy

apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
  name: site-a-example
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    acme.cert-manager.io/http01-parentreffallback: "true"
spec:
  parentRef: {name: public-gateway, namespace: nginx-gateway, kind: Gateway}
  listeners:
    - name: https
      hostname: "site-a.example"
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs: [{kind: Secret, name: site-a-example-tls}]
    - name: https-www               # chỉ có khi bật wwwRedirect
      hostname: "www.site-a.example"
      ...

cert-manager đọc annotation trên ListenerSet và tự tạo Certificate trong cùng namespace, NGINX Gateway Fabric nạp Secret khi nó sẵn sàng. ListenerSet chỉ có listener HTTPS, nên annotation http01-parentreffallback cho phép challenge HTTP-01 đi qua listener :80 của Gateway. Thiếu annotation này, challenge báo NoReadyListeners.

Route tách thành ba HTTPRoute: route chính đưa HTTPS của domain tới Service, route https-redirect chuyển HTTP sang HTTPS (chỉ khi bật TLS), route www-redirect chuyển www về domain chính. Site không bật TLS gắn route thẳng vào listener HTTP của Gateway, không có ListenerSet, nên không gặp lỗi 404 do redirect toàn cục. Route challenge /.well-known/acme-challenge/ loại Exact được ưu tiên hơn redirect PathPrefix /, nên gia hạn chứng chỉ vẫn chạy khi đã bật redirect.

Các annotation ingress-nginx còn lại được thay bằng policy riêng của NGINX Gateway Fabric gắn vào route chính: ClientSettingsPolicy giới hạn body (ví dụ 128m), ProxySettingsPolicy đặt timeout connect, read, send. Lưu ý thực tế: route chính nên chỉ có một rule, vì nhiều rule có thể làm NGINX Gateway Fabric sinh trùng chỉ thị client_max_body_size. So với thiết kế dùng script gom chứng chỉ, chart bỏ hẳn template Certificate, Middleware, ReferenceGrant và CronJob đồng bộ.

4. Lỗi hay gặp khi triển khai NGINX Gateway Fabric

  • Helm chỉ cài control plane. Data plane của NGINX Gateway Fabric (pod nginx và Service LoadBalancer) chỉ xuất hiện khi có object Gateway. Mỗi Gateway sinh một load balancer riêng. Muốn nhiều site chung một IP thì chỉ được có đúng một Gateway.
  • Listener mặc định chỉ nhận route cùng namespace. HTTPRoute ở namespace khác báo The Route is not allowed by any listener cho tới khi listener có allowedRoutes.namespaces.from: All.
  • Giữ IP bằng cách patch Service cũ vẫn chạy, với điều kiện Gateway nằm cùng namespace với Service ingress-nginx cũ, vì Service chỉ chọn được pod cùng namespace. Khác namespace thì có 0 endpoint.
  • Dùng annotation của Linode để giữ IP. linode-loadbalancer-preserve: "true" giữ lại NodeBalancer khi Service bị xoá. linode-loadbalancer-nodebalancer-id gắn một Service mới vào NodeBalancer có sẵn và nhận lại đúng IP cũ.
  • Đổi kiểu Service không phải cách gỡ load balancer. Patch Service của Gateway từ LoadBalancer sang ClusterIP thì NGINX Gateway Fabric lập tức đặt lại LoadBalancer, Cloud Controller Manager cấp một NodeBalancer mới, còn cái cũ bị bỏ lại và vẫn tính phí. Sau mỗi buổi thử, đối chiếu danh sách load balancer trên tài khoản với Service trong cluster.
  • Gỡ và cài lại cert-manager an toàn cho dữ liệu nếu CRD được giữ bằng resource policy keep: Certificate và Secret còn nguyên, controller mới tiếp tục quản lý mà không cấp lại.

5. Checklist trước khi đưa NGINX Gateway Fabric lên production

  • Test luồng cấp chứng chỉ thật cho một domain thật qua ListenerSet và cert-manager, ghi lại kết quả để chạy lại được.
  • Lên kế hoạch chạy song song. NGINX Gateway Fabric không xử lý object Ingress, nên giai đoạn chuyển tiếp phải chạy ingress-nginx và NGINX Gateway Fabric như hai controller riêng, chuyển hostname theo từng đợt.
  • Có template cho mọi loại site, kể cả site chỉ redirect nguyên domain.
  • Không để secret trong values mặc định của chart. Giá trị nào đã từng nằm trong values thì coi như đã lộ, rotate rồi chuyển sang Secret.
  • Đồng bộ phiên bản CRD Gateway API với yêu cầu của controller, gỡ CRD của controller đã thử trước đó.
  • Dọn hạ tầng thử nghiệm: cluster test, load balancer bị bỏ lại.

6. Nhìn lại Series 2

Series đi theo đúng thứ tự một đội PHP chuyển hàng trăm website Laravel lên Kubernetes:

Giai đoạnNội dungBài đại diện
Nền tảngCluster đầu tiên, ingress, cert-manager, biến môi trường trong containerDựng cluster LKE đầu tiên: node pool, kubeconfig và lựa chọn ban đầu
CI/CDGiám sát, requests và limits, pipeline deploy đầu tiênCI/CD đầu tiên với Bitbucket Pipelines, Docker Hub và kubectl
Đóng góiHelm chart, cầu nối sang GitLab CI, chart dùng chungHelm chart là gì: đóng gói website Laravel để deploy lặp lại
Chuyển nhàDatabase ngoài cluster, NFS, migrate hàng loạt, backupMigrate website lên Kubernetes: quy trình chuyển hàng loạt an toàn
Mở rộngPod giữ chỗ, cluster thứ hai, stack giám sát mớikube-prometheus-stack: thay monitoring cũ và cấu hình sai âm thầm
Vận hành và đổi controllerJenkins làm form cho GitLab CI, Gateway APIJenkins và GitLab CI: vận hành Website AI trên Kubernetes

Ba điều đúng suốt series: thứ chạy được thì phải lặp lại được, nhờ chart dùng chung và pipeline; dữ liệu đặt ngoài cluster giúp mọi cuộc chuyển nhà nhẹ đi; còn những gì "cài một lần rồi để đó", như ingress controller hay cert-manager không nâng cấp, sớm muộn cũng thành món nợ phải trả cùng lúc. Lên lịch nâng cấp các thành phần hạ tầng của cluster như một việc định kỳ, đừng chờ tới khi dự án ngừng vá.

Phía Cloud VPS cũng có bài nhìn lại riêng: Nhìn lại mô hình Cloud VPS: điểm yếu và phương án gộp VPS. Mọi bài của các series nằm trong chuyên mục blog kỹ thuật của Sudo.

Kết series

Cảm ơn bạn đã theo dõi 30 bài về Kubernetes và CI/CD trên Linode. Con đường không thẳng: có quyết định đúng từ sớm, có thiết kế sai mà mãi sau mới thấy, có pilot phải gỡ. Nếu bạn đang chuẩn bị rời ingress-nginx sang NGINX Gateway Fabric hay đưa nhiều website Laravel lên Kubernetes, hy vọng các lệnh, cấu hình mẫu và sai lầm trong series giúp bạn đi bớt đường vòng.

Phần còn lại là vận hành: sự cố, cách điều tra và những thứ phải bảo trì định kỳ. Mời bạn đọc tiếp series Vận hành thực chiến, bắt đầu từ bài Điều tra sự cố nhiều tầng: từ balancer tới database và Kubernetes.