Bỏ qua đến nội dung
DevOps Lab

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.

Tối đa $0.02/GB

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.

Tối đa $0.06/GB!gấp 3x

So sánh flow: EC2 vs EKS

EC2 (1 hop)
ClientEC2$0.02
EKS (3+ hops)
PodCoreDNSNLBPod$0.06

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í.

Use Case 1

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.

Internet Client → NLB → EKS Pods
AZ-a
⚖️NLB Node
🖥️Worker Node
📦App Pod
AZ-b
⚖️NLB Node
🖥️Worker Node
📦App Pod
AZ-c
⚖️NLB Node
🖥️Worker Node
📦App Pod
⚠️ Before Optimization — Chi phí cao

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

Client→NLB: $0.02/GBNLB→Target: $0.02/GBTổng: $0.04/GB

📋 Cấu hình YAML — Service với NLB

service-nlb-external.yaml
"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 level
  • externalTrafficPolicy: 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 ConsumedLCUsActiveFlowCount
Use Case 2

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.

Pod → CoreDNS → NLB (Internal) → Target Pod
AZ-a
📦App Pod
🔍CoreDNS
⚖️NLB (Internal)
🖥️Target Pod
AZ-b
📦App Pod
🔍CoreDNS
⚖️NLB (Internal)
🖥️Target Pod
AZ-c
📦App Pod
🔍CoreDNS
⚖️NLB (Internal)
🖥️Target Pod
⚠️ Before — CoreDNS gây cross-AZ “đôi”

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!

Pod→CoreDNS: $0.02CoreDNS→NLB: $0.02NLB→Target: $0.02TỔNG: $0.06/GB

📋 Layer 1: CoreDNS — internalTrafficPolicy: Local

coredns-service.yaml
"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

service-internal-nlb-tar.yaml
"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ấy zone: 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.
Use Case 3

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.

gRPC / TCP Workloads: Instance Mode vs IP Mode
❌ Instance Mode (hiện tại)
⚖️NLB
🖥️Node NodePort :30080
🔀kube-proxy iptables
📦gRPC Pod (có thể khác AZ)
3 hops → latency cao + cross-zone risk
IP Mode (đề xuất)
⚖️NLB (IP Target)
nlb-target-type: ip
📦gRPC Pod (trực tiếp)
AWS VPC CNI
1 hop → latency thấp + direct connection

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

service-grpc-nlb-ip.yaml
"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=true trên aws-node daemonset
  • Security Group gán trực tiếp cho Pod (dùng SecurityGroupPolicy CRD)
  • Warm ENI: NLB cần target đăng ký trước khi traffic đến → dùng aws-load-balancer-target-group-config annotation
Use Case 4

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.

Pod Distribution: Skewed vs Balanced
AZ-a (5 pods)
⚖️NLB
📦
📦
📦
📦
📦
AZ-b (1 pods)
⚖️NLB
📦
AZ-c (2 pods)
⚖️NLB
📦
📦

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

deployment-topology-spread.yaml
"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ùng ScheduleAnyway cho 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 CaseTrước (Before)Sau (After)Config KeysĐộ khó
UC1: External Ingress$0.02-0.04/GB$0.00/GBcross-zone: false, externalTrafficPolicy: LocalDễ
UC2: CoreDNS + TAR$0.04-0.06/GB$0.00/GBinternalTrafficPolicy: Local, TAR: auto, cross-zone: falseTrung bình-Khó
UC3: gRPC IP Mode$0.02-0.04/GB$0.00/GBnlb-target-type: ip, externalTrafficPolicy: LocalTrung bình
UC4: Topology SpreadKhông đềuBalancedtopologySpreadConstraints, maxSkew: 1Dễ

Thứ tự triển khai khuyến nghị

1

UC4: Topology Spread

Phân bổ Pod đều trước → foundation cho mọi tối ưu khác

2

UC1: External Ingress

Dễ nhất, apply ngay, tiết kiệm nhanh

3

UC2: CoreDNS + TAR

Phức tạp nhất nhưng tiết kiệm nhiều nhất cho internal traffic

4

UC3: IP Mode

Tối ưu thêm cho gRPC/TCP workloads