EKS + NLB: Tối ưu chi phí Cross-AZ Data Transfer
Deep dive cho DevOps — Hiểu tại sao EKS tốn tiền cross-AZ hơn EC2, cách CoreDNS gây ra chi phí ẩn, và 4 use-case tối ưu với YAML cấu hình sẵn.
🔥 Tại sao EKS cross-AZ cost lại CAO HƠN EC2?
EC2 thuần
1 request = 1 lần cross-AZ tối đa (Client → Server). Bạn kiểm soát hoàn toàn vị trí của server.
EKS + NLB
1 request có thể cross-AZ 3 lần: Pod → CoreDNS → NLB → Target Pod. Mỗi hop đều có thể sang AZ khác.
So sánh flow: EC2 vs EKS
Core Concepts cần hiểu trước
Inter-AZ Data Transfer
Mỗi lần data đi qua boundary giữa 2 AZ, AWS tính phí $0.01/GB ở sender và $0.01/GB ở receiver → tổng $0.02/GB. Trong EKS, 1 request có thể cross-AZ nhiều lần qua các hop khác nhau.
CoreDNS = "Client" trong K8s
Khi Pod cần gọi Service khác, nó đầu tiên gọi CoreDNS để resolve DNS. CoreDNS chạy dưới dạng Deployment với nhiều pod trải trên nhiều AZ. Nếu Pod gọi CoreDNS ở AZ khác → cross-AZ ngay ở bước đầu tiên!
NLB Zonal Affinity
Khi bật 100% zonal affinity, Route 53 ưu tiên trả NLB IP cùng AZ với client. Điều này giữ traffic Client→NLB trong cùng AZ → miễn phí. Nếu không bật, client có thể bị route đến NLB ở bất kỳ AZ nào.
Topology-Aware Routing (TAR)
Kubernetes feature giúp Service trả về endpoint IPs ưu tiên cùng zone với caller. Khi bật TAR, kube-proxy/EndpointSlice chỉ trả IP của Pod cùng AZ → tránh cross-AZ ngay từ bước DNS resolution.
externalTrafficPolicy: Local
Khi set "Local", traffic từ NLB chỉ được forward đến Pod chạy trên cùng Node với NLB target. Bỏ qua kube-proxy cross-node routing → giảm 1 cross-zone hop. Lưu ý: cần đảm bảo mỗi AZ đều có Pod chạy.
internalTrafficPolicy: Local
Giống externalTrafficPolicy nhưng cho internal cluster traffic. Khi set "Local" trên CoreDNS Service, Pod chỉ gọi CoreDNS trên cùng Node → giữ DNS lookup trong cùng AZ → miễn phí.
External Ingress via NLB
Ứng dụng phổ biến nhất: Client từ Internet truy cập EKS cluster qua NLB. Đây là pattern cơ bản nhất nhưng cũng dễ gây cross-AZ cost nhất nếu không cấu hình đúng. Phù hợp cho web app, API gateway, hoặc bất kỳ service nào cần expose ra Internet.
1. Client từ Internet → Route 53 trả NLB IP ngẫu nhiên (có thể khác AZ với target)
2. NLB nhận traffic ở AZ-b → forward đến Target Node ở AZ-a (cross-zone!)
3. Cross-zone load balancing BẬT → traffic phân tán đều sang các AZ khác
📋 Cấu hình YAML — Service với NLB
"comment"># UC1: External Ingress via NLB — Tối ưu với Zonal Affinity + externalTrafficPolicy: Local"key">apiVersion: v1"key">kind: Service"key">metadata:"key">name: my-app-nlb"key">namespace: default"key">annotations:"comment"># NLB type (sử dụng AWS Load Balancer Controller v2)"annotation">service.beta.kubernetes.io/aws-load-balancer-type: "external""comment"># TẮT cross-zone load balancing → tránh phí cross-AZ"annotation">service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "false""comment"># Internet-facing (hoặc "internal" cho internal service)"annotation">service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing""comment"># Instance target type (mặc định)"annotation">service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "instance""key">spec:"key">type: LoadBalancer"key">externalTrafficPolicy: Local"key">selector:"key">app: my-app"key">ports:- port: 80"key">targetPort: 8080"key">protocol: TCP
Điểm mấu chốt
cross-zone-load-balancing-enabled: "false"— Tắt cross-zone ở NLB levelexternalTrafficPolicy: Local— Pod chỉ nhận traffic trên node có NLB ENI- Lưu ý: Cần đảm bảo mỗi AZ đều có Pod running → dùng HPA + Pod Topology Spread
- Monitor: CloudWatch metric
ConsumedLCUsvàActiveFlowCount
CoreDNS + Topology-Aware Routing (Quan trọng nhất!)
Đây là use-case quan trọng nhất cho EKS. Trong Kubernetes, mọi inter-service communication đều đi qua CoreDNS. Nếu không tối ưu, mỗi DNS lookup có thể cross-AZ → cộng dồn thành chi phí khổng lồ. Bạn cần tối ưu 3 layer cùng lúc: CoreDNS Service + Target Service + NLB.
1. App Pod (AZ-a) gọi CoreDNS → kube-proxy chọn ngẫu nhiên CoreDNS Pod → có thể sang AZ-b!
2. CoreDNS (AZ-b) resolve Service DNS → trả NLB IP → có thể ở AZ-c!
3. NLB (AZ-b) forward đến Target Pod → có thể cross-zone sang AZ-c
→ Nhiều lần cross-zone trong 1 request: Pod→CoreDNS + CoreDNS→NLB + NLB→Target = tối đa $0.06/GB!
📋 Layer 1: CoreDNS — internalTrafficPolicy: Local
"comment"># UC2 Layer 1: CoreDNS Service — internalTrafficPolicy: Local"comment"># File: /etc/kubernetes/manifests/coredns.yaml (hoặc qua kube-system ConfigMap)"key">apiVersion: v1"key">kind: Service"key">metadata:"key">name: kube-dns"key">namespace: kube-system"key">spec:"comment"># KEY: Pod chỉ gọi CoreDNS trên cùng Node/AZ"comment"># → Loại bỏ cross-AZ hop Pod → CoreDNS"key">internalTrafficPolicy: Local"key">selector:"key">k8s-app: kube-dns"key">ports:- name: dns"key">port: 53"key">protocol: UDP"key">targetPort: 53- name: dns-tcp"key">port: 53"key">protocol: TCP"key">targetPort: 53
Cách apply trên EKS managed CoreDNS:
Edit ConfigMap kube-system/coredns hoặc patch trực tiếp Service:kubectl patch svc kube-dns -n kube-system -p '{"spec":{"internalTrafficPolicy":"Local"}}'
📋 Layer 2+3: Internal NLB Service + TAR
"comment"># UC2 Layer 2+3: Internal NLB Service với Topology-Aware + Zonal Affinity"key">apiVersion: v1"key">kind: Service"key">metadata:"key">name: my-internal-svc"key">namespace: default"key">annotations:"comment"># NLB internal — chỉ dùng trong VPC"annotation">service.beta.kubernetes.io/aws-load-balancer-type: "external""annotation">service.beta.kubernetes.io/aws-load-balancer-scheme: "internal""annotation">service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip""comment"># TẮT cross-zone → traffic chỉ đi trong cùng AZ"annotation">service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "false""key">labels:"comment"># Bật Topology-Aware Hints (cần kube-proxy --topology-aware-hints=true)service.kubernetes.io/topology-aware-hints: "auto""key">spec:"key">type: LoadBalancer"comment"># KEY: internalTrafficPolicy: Local"comment"># → NLB target chỉ là Pod trên node cùng AZ với NLB"key">internalTrafficPolicy: Local"key">selector:"key">app: my-app"key">ports:- port: 80"key">targetPort: 8080"key">protocol: TCP
Kiểm tra Topology-Aware Hints đã bật
- Check kube-proxy arguments:
kubectl get ds kube-proxy -n kube-system -o yaml | grep topology-aware - Nếu chưa bật: edit kube-proxy ConfigMap, thêm
--topology-aware-hints=true - Check EndpointSlice hints:
kubectl get endpointslices -o yaml | grep hints— nên thấyzone:hints - Lưu ý: TAR cần ít nhất 3 endpoint trở lên mới hoạt động. Với 2 endpoints, K8s sẽ fallback về round-robin.
gRPC / TCP Workloads với IP Mode
NLB hỗ trợ TCP/UDP natively — lý tưởng cho gRPC, game servers, IoT protocols, hoặc bất kỳ non-HTTP workload nào. Với IP target mode, NLB giao tiếp trực tiếp với Pod IP thay vì qua NodePort, loại bỏ hoàn toàn kube-proxy hop. Giảm latency và giảm thiểu cross-AZ risk.
Vấn đề Instance Mode: NLB target là Node IP (NodePort). Khi NLB nhận traffic, nó forward đến Node → kube-proxy (iptables) route đến Pod. Nếu Pod không chạy trên node đó → thêm 1 lần cross-zone.
Trong EKS, Cluster Autoscaler có thể schedule Pod sang node khác AZ bất kỳ → cross-zone xảy ra rất thường xuyên.
📋 Cấu hình NLB IP Mode cho gRPC
"comment"># UC3: gRPC/TCP với IP Mode — Bỏ qua kube-proxy"key">apiVersion: v1"key">kind: Service"key">metadata:"key">name: grpc-service"key">namespace: default"key">annotations:"annotation">service.beta.kubernetes.io/aws-load-balancer-type: "external""annotation">service.beta.kubernetes.io/aws-load-balancer-scheme: "internal""comment"># KEY: IP target type → NLB target trực tiếp Pod IP"comment"># → Không qua NodePort + kube-proxy"annotation">service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip""comment"># TẮT cross-zone"annotation">service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "false""comment"># Health check config cho gRPC"annotation">service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "TCP""annotation">service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "50051""key">spec:"key">type: LoadBalancer"comment"># KEY: Local → target Pod trên node cùng AZ với NLB node"key">externalTrafficPolicy: Local"key">selector:"key">app: grpc-app"key">ports:- port: 443"key">targetPort: 50051"key">protocol: TCP---"comment"># SecurityGroupPolicy — Gán SG trực tiếp cho Pod (yêu cầu cho IP mode)"key">apiVersion: v1"key">kind: Service"key">metadata:"key">name: grpc-sg-policy"key">annotations:"annotation">service.beta.kubernetes.io/aws-load-balancer-security-groups: "sg-12345678"---"comment"># Yêu cầu: Cài AWS VPC CNI với prefix delegation enabled"comment"># kubectl set env daemonset aws-node -n kube-system # ENABLE_PREFIX_DELEGATION=true
Yêu cầu cho IP Mode
- AWS VPC CNI (mặc định) — không dùng Calico/Canal cho IP target mode
- Enable
ENABLE_PREFIX_DELEGATION=truetrên aws-node daemonset - Security Group gán trực tiếp cho Pod (dùng
SecurityGroupPolicyCRD) - Warm ENI: NLB cần target đăng ký trước khi traffic đến → dùng
aws-load-balancer-target-group-configannotation
Topology Spread Constraints — Phân bổ Pod đều
Tất cả các tối ưu ở trên đều phụ thuộc vào việc pods được phân bổ đều trên các AZ. Nếu AZ-a có 5 pod còn AZ-b chỉ có 1 pod → cross-zone load balancing (nếu bật) hoặc overload (nếu tắt). Topology Spread Constraints đảm bảo Kubernetes scheduler phân bổ pod đều.
Vấn đề: Pods phân bổ không đều (5-1-2). AZ-a quá tải, AZ-b thiếu pod. NLB ở AZ-a phải cross-zone forward sang AZ-b, AZ-c → phí $0.02-0.04/GB cho mỗi request.
Nguyên nhân: Default scheduler không cân bằng theo zone. Cluster Autoscaler chỉ scale nodes, không phân bổ pod đều.
📋 Cấu hình Topology Spread Constraints
"comment"># UC4: Topology Spread Constraints — Phân bổ Pod đều trên các AZ"key">apiVersion: apps/v1"key">kind: Deployment"key">metadata:"key">name: my-app"key">namespace: default"key">spec:"key">replicas: 9 "comment"># Nhiều replica để thấy rõ hiệu quả"key">selector:"key">matchLabels:"key">app: my-app"key">template:"key">metadata:"key">labels:"key">app: my-app"key">spec:"comment"># KEY: topologySpreadConstraints đảm bảo mỗi AZ chênh lệch tối đa 1 pod"key">topologySpreadConstraints:- maxSkew: 1 "comment"># Chênh lệch tối đa giữa các AZ"key">topologyKey: topology.kubernetes.io/zone "comment"># Phân bổ theo AZ"key">whenUnsatisfiable: DoNotSchedule "comment"># Không schedule nếu vi phạm"key">labelSelector:"key">matchLabels:"key">app: my-app"comment"># (Tùy chọn) Cũng cân bằng theo Node- maxSkew: 1"key">topologyKey: kubernetes.io/hostname"key">whenUnsatisfiable: ScheduleAnyway "comment"># Node spread là "soft" constraint"key">labelSelector:"key">matchLabels:"key">app: my-app"key">containers:- name: my-app"key">image: my-app:latest"key">ports:- containerPort: 8080
Lưu ý khi sử dụng
maxSkew: 1→ mỗi AZ chênh lệch tối đa 1 pod. Có thể tăng lên 2-3 nếu cần linh hoạt hơn.whenUnsatisfiable: DoNotSchedule→ cứng, không schedule nếu vi phạm. DùngScheduleAnywaycho soft constraint.- Tích hợp với Cluster Autoscaler: Nếu node không đủ ở 1 AZ, pod sẽ Pending → CA sẽ tạo node mới ở AZ đó.
- Kết hợp với
podAntiAffinityđể tránh nhiều pod của cùng Deployment trên 1 node.
Tổng kết so sánh
| Use Case | Trước (Before) | Sau (After) | Config Keys | Độ khó |
|---|---|---|---|---|
| UC1: External Ingress | $0.02-0.04/GB | $0.00/GB | cross-zone: false, externalTrafficPolicy: Local | Dễ |
| UC2: CoreDNS + TAR | $0.04-0.06/GB | $0.00/GB | internalTrafficPolicy: Local, TAR: auto, cross-zone: false | Trung bình-Khó |
| UC3: gRPC IP Mode | $0.02-0.04/GB | $0.00/GB | nlb-target-type: ip, externalTrafficPolicy: Local | Trung bình |
| UC4: Topology Spread | Không đều | Balanced | topologySpreadConstraints, maxSkew: 1 | Dễ |
Thứ tự triển khai khuyến nghị
UC4: Topology Spread
Phân bổ Pod đều trước → foundation cho mọi tối ưu khác
UC1: External Ingress
Dễ nhất, apply ngay, tiết kiệm nhanh
UC2: CoreDNS + TAR
Phức tạp nhất nhưng tiết kiệm nhiều nhất cho internal traffic
UC3: IP Mode
Tối ưu thêm cho gRPC/TCP workloads