LB IPAM và Traffic Policy

K
Kai··6 min read

Bài 48 phơi Ingress qua NodePort, bài 49 phơi Gateway cũng qua NodePort, và cả hai để lại cùng một vết: Service kiểu LoadBalancer treo EXTERNAL-IP <pending>, Gateway báo Programmed=False / AddressNotAssigned. Lý do đã nói ở bài 49 — cụm EC2 tự dựng không có cloud controller nào cấp địa chỉ cho Service LoadBalancer. Bài này dùng LB IPAM của Cilium để tự cấp, rồi xem externalTrafficPolicy đổi cách lưu lượng vào đi tới pod.

Service LoadBalancer treo pending

Dựng một Service kiểu LoadBalancer để thấy lại trạng thái đó:

kubectl create namespace lb-demo
kubectl -n lb-demo create deployment web --image=hashicorp/http-echo:1.0 -- /http-echo -text=hello-lb -listen=:8080
kubectl -n lb-demo expose deployment web --type=LoadBalancer --port=80 --target-port=8080
kubectl -n lb-demo get svc web
NAME   TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
web    LoadBalancer   10.32.0.76   <pending>     80:32303/TCP   1s

Service đòi một địa chỉ ngoài nhưng không ai cấp, nên nó chờ mãi. NodePort 32303 vẫn được cấp tự động (mọi Service LoadBalancer kèm một NodePort), đó là lý do bài 48–49 vẫn test được qua NodePort dù external-IP treo.

CiliumLoadBalancerIPPool cấp địa chỉ

LB IPAM luôn bật nhưng nằm im cho tới khi có pool đầu tiên. Một CiliumLoadBalancerIPPool khai một dải IP để Cilium rút ra gán cho các Service LoadBalancer. Khai dải bằng cidr, hoặc bằng start/stop:

apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata:
  name: demo-pool
spec:
  blocks:
  - start: "192.0.2.10"
    stop: "192.0.2.50"

192.0.2.0/24 là dải tài liệu (TEST-NET, RFC 5737) — ở đây chỉ cần một dải không đụng IP thật trong VPC. Áp pool vào, Service web nhận địa chỉ ngay:

kubectl apply -f demo-pool.yaml
kubectl get ciliumloadbalancerippool
kubectl -n lb-demo get svc web
kubectl -n lb-demo get svc web -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'
NAME        DISABLED   CONFLICTING   IPS AVAILABLE   AGE
demo-pool   false      False         40              6s

NAME   TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
web    LoadBalancer   10.32.0.159   192.0.2.10    80:30648/TCP   6s

cilium.io/IPAMRequestSatisfied=True

Pool báo còn 40 IP (dải 192.0.2.10–.50 có 41 địa chỉ, một cái vừa gán cho web), Service chuyển từ <pending> sang 192.0.2.10, và điều kiện IPAMRequestSatisfied=True. Pool không khai serviceSelector nên cấp cho mọi Service LoadBalancer; thêm serviceSelector.matchLabels thì pool chỉ cấp cho Service mang nhãn khớp, dùng khi muốn tách dải IP theo môi trường.

Gateway của bài 49 chuyển sang Programmed

Cơ chế này lấp luôn chỗ treo của Gateway. Tạo lại một Gateway như bài 49, lần này pool đã có:

kubectl -n lb-demo get gateway gw
NAME   CLASS    ADDRESS      PROGRAMMED   AGE
gw     cilium   192.0.2.11   True         7s

ADDRESS 192.0.2.11, PROGRAMMED True. Gateway tạo một Service LoadBalancer phía sau (cilium-gateway-gw), Service đó được LB IPAM cấp IP, và điều kiện AddressNotAssigned của bài 49 biến mất. Cùng một LB IPAM phục vụ cả Service thường lẫn Gateway.

Cấp IP khác với quảng bá IP

Có IP rồi, thử gọi thẳng từ một node trong cụm:

# từ worker-1
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://192.0.2.10/
HTTP 200

Gọi được, nhưng cần hiểu đúng tại sao. Cilium kube-proxy-less (bài 46) nạp VIP của LoadBalancer vào datapath eBPF trên mọi node; khi một node trong cụm gửi gói tới 192.0.2.10, eBPF trên chính node đó bắt lấy và DNAT về backend trước khi gói kịp ra ngoài. Xem bảng service eBPF:

kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list | grep 192.0.2
35   192.0.2.10:80/TCP   LoadBalancer   1 => 10.200.0.90:8080/TCP (active)
39   192.0.2.11:80/TCP   LoadBalancer

Vậy IP dùng được từ trong cụm. Từ một client ngoài cụm thì chưa, và đây là ranh giới doc nhấn: LB IPAM chỉ cấp địa chỉ, không quảng bá nó. Một router ở ngoài không biết 192.0.2.10 nằm ở đâu cho tới khi có thứ rao nó ra: L2 Announcement (CiliumL2AnnouncementPolicy, dùng cho mạng L2 nội bộ, node trả lời ARP cho VIP) hoặc BGP (Cilium peer với router, quảng bá route). Trên AWS, mạng VPC là software-defined nên ARP-based L2 không định tuyến được như on-prem; cách thực dụng là BGP hoặc dùng thẳng AWS Load Balancer Controller. Đó cũng là lý do EKS không dùng LB IPAM mà gắn vào NLB/ALB của AWS. Cụm tự dựng này dừng ở mức cấp IP và xác nhận datapath eBPF chạy; phần quảng bá ra ngoài phụ thuộc hạ tầng mạng cụ thể.

externalTrafficPolicy: Cluster hay Local

Trường externalTrafficPolicy của Service quyết định lưu lượng vào (qua NodePort hoặc LoadBalancer) được xử lý ra sao. Để thấy khác biệt, đặt một pod echo (agnhost, endpoint /clientip in ra địa chỉ nguồn nó quan sát) ghim trên worker-0, Service NodePort 31888. Client là node lb-0 (10.0.1.10), nơi không có pod nào.

Với externalTrafficPolicy: Cluster (mặc định), gọi vào NodePort của worker-1 — nơi không có pod echo:

# từ lb-0 (10.0.1.10), gọi NodePort của worker-1
curl -s http://10.0.1.21:31888/clientip
10.0.1.21:39672

Pod thấy nguồn là 10.0.1.21 — IP của worker-1, không phải IP client thật 10.0.1.10. Worker-1 nhận gói, thấy không có pod local, nên chuyển tiếp sang worker-0 và SNAT địa chỉ nguồn thành IP của chính nó. Client thật bị che. Đổi sang Local:

kubectl -n lb-demo patch svc echo -p '{"spec":{"externalTrafficPolicy":"Local"}}'

Rồi gọi lại từ lb-0, tới hai node khác nhau:

curl -s -m5 -o /dev/null -w "HTTP %{http_code}\n" http://10.0.1.21:31888/clientip   # worker-1: không pod
curl -s http://10.0.1.20:31888/clientip                                            # worker-0: có pod
HTTP 000          # worker-1 không có endpoint local -> drop
10.0.1.10:53068   # worker-0 có pod -> giữ nguyên IP client thật

Local chỉ chuyển tới pod trên cùng node nhận gói: gọi vào worker-1 (không endpoint) thì rớt, gọi vào worker-0 (có pod) thì pod thấy đúng 10.0.1.10 của client. Đánh đổi rõ ràng: Cluster rải đều mọi node nhưng thêm một hop và che IP nguồn; Local giữ IP nguồn và bỏ hop thừa nhưng đòi cân bằng tải bên ngoài phải biết node nào có endpoint. Khi cần log IP thật của người dùng hoặc áp policy theo IP nguồn, chọn Local.

Một họ hàng gần là trafficDistribution: PreferClose trên Service — gợi ý ưu tiên gửi tới endpoint cùng zone để giảm lưu lượng chéo vùng (topology-aware routing). Còn dual-stack (IPv4 + IPv6 song song) thì cụm này không bật vì dựng thuần IPv4 từ đầu (pod CIDR 10.200.0.0/16); bật dual-stack phải khai cả CIDR IPv6 từ khâu dựng control plane.

🧹 Dọn dẹp

kubectl delete namespace lb-demo                          # web, echo, gateway gw
kubectl delete ciliumloadbalancerippool demo-pool

Xóa cả pool vì không còn Service LoadBalancer nào cần địa chỉ. Cilium giữ nguyên cấu hình; LB IPAM trở về trạng thái nằm im cho tới pool kế tiếp. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 50-lb-ipam.

Tổng kết

LB IPAM lấp chỗ treo <pending> mà bài 48–49 để lại: một CiliumLoadBalancerIPPool khai dải IP (cidr hoặc start/stop), Cilium gán cho Service LoadBalancer (điều kiện IPAMRequestSatisfied=True) và cho cả Service phía sau Gateway, khiến Gateway bài 49 chuyển sang Programmed=True với địa chỉ thật. Ranh giới quan trọng: LB IPAM cấp IP, không quảng bá — IP dùng được từ trong cụm nhờ eBPF nạp VIP trên mọi node, nhưng để client ngoài tới được cần L2 Announcement (mạng L2 nội bộ) hoặc BGP, mà trên AWS thì thực dụng nhất là BGP hoặc AWS Load Balancer Controller. externalTrafficPolicy quyết định địa chỉ nguồn: Cluster (mặc định) rải mọi node nhưng SNAT che IP client; Local chỉ phục vụ endpoint cùng node, giữ IP client thật, đổi lại node không có endpoint sẽ drop. Đến đây Part X khép lại — từ mô hình mạng phẳng bài 13–14, qua Cilium eBPF, NetworkPolicy, Ingress, Gateway API, tới cấp phát và định tuyến địa chỉ vào.

Bài 51 mở Part XI về bảo mật, bắt đầu từ đường vào API server: một request kubectl đi qua những chặng xác thực (authentication) nào trước khi tới được dữ liệu — certificate, token, service account — và cụm tự dựng của ta đã cấu hình từng chặng đó ở đâu.

Related Posts