← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202620 phút đọc17 min read

Services and NetworkingServices and Networking

Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.

Tổng quan

Networking là nơi Kubernetes không còn là “Docker kèm scheduler” nữa mà trở thành một hệ phân tán thực thụ. Pod vốn ephemeral — chúng sinh ra rồi mất đi, bị reschedule sang node khác, và đổi IP mỗi lần như thế. Nhưng ứng dụng lại cần địa chỉ ổn định, load balancing, service discovery, và khả năng mở ra thế giới bên ngoài một cách có kiểm soát. Networking của Kubernetes tồn tại để lấp khoảng cách đó: cấp cho mỗi Pod một IP thật, cho phép mọi Pod nói chuyện với mọi Pod khác mà không cần NAT, rồi xếp thêm Service, DNS, IngressNetworkPolicy lên trên để cung cấp tên ổn định, phân phối traffic, truy cập từ ngoài, và phân vùng bảo mật.

Thiết kế cố ý đơn giản ở tầng nền và cắm-thay (pluggable) ở phía trên. Tầng nền là Kubernetes network model — một tập nhỏ các yêu cầu mà mọi cluster phải thỏa mãn. Tất cả phần còn lại (gói tin thực sự di chuyển ra sao, load balancer nào được cấp phát, DNS phân giải thế nào) do các thành phần pluggable đảm nhiệm: một CNI plugin cho Pod networking, kube-proxy (hoặc dataplane eBPF) cho Service routing, CoreDNS cho phân giải tên, và một Ingress/Gateway controller cho HTTP routing. Hiểu rõ tầng nào chịu trách nhiệm gì chính là chìa khóa để debug networking, bởi một lỗi “connection refused” có thể bắt nguồn từ bất kỳ tầng nào.

Một mô hình tư duy

Hãy hình dung networking của Kubernetes như bốn tầng tách rời xếp chồng trên mạng Pod phẳng. Tầng 0 — mạng Pod: mỗi Pod có một IP và tiếp cận trực tiếp mọi Pod khác (việc của CNI). Tầng 1 — Service: một virtual IP + tên DNS ổn định load-balance tới một tập Pod luôn thay đổi (việc của kube-proxy). Tầng 2 — DNS: biến tên như web.prod.svc.cluster.local thành Service IP (việc của CoreDNS). Tầng 3 — Ingress/Gateway: HTTP routing tầng L7 và TLS từ ngoài vào (việc của controller). Tầng 4 — NetworkPolicy: một firewall quyết định luồng nào thực sự được phép (lại là việc của CNI). Mỗi tầng phụ thuộc tầng dưới nhưng ngoài ra thì độc lập — và mỗi tầng có thể hỏng độc lập.

Kiến thức nền tảng

Kubernetes network model

Kubernetes không tự cài đặt networking; nó quy định một model và để các CNI plugin thỏa mãn. Mọi cluster hợp lệ đảm bảo:

  1. Mỗi Pod có một địa chỉ IP riêng, duy nhất trong toàn cluster. Các container của một Pod dùng chung IP (và network namespace) đó, gọi nhau qua localhost.
  2. Mọi Pod giao tiếp được với mọi Pod khác trên bất kỳ node nào mà không cần NAT. Mạng là phẳng — Pod IP route được trên toàn cluster.
  3. Các agent trên một node (kubelet, system daemon) tiếp cận được mọi Pod trên node đó.
  4. IP mà một Pod tự thấy chính là IP người khác dùng để tới nó. Không có source-NAT rewrite bên trong mạng Pod.

Model “IP-per-Pod, mạng phẳng, không NAT” này khiến networking Kubernetes cảm giác thống nhất. Nó loại bỏ trò port-mapping của Docker thuần (-p 8080:80) — trong cluster, một Pod chỉ cần lắng nghe trên port thật của nó và tiếp cận được ở IP thật. Đánh đổi là nó đòi hỏi hạ tầng routing thật, và đó chính xác là thứ CNI plugin cung cấp.

Bài toán mà Service giải quyết

Pod IP không ổn định: scale up, rollout phiên bản mới, hay mất một node, và tập IP đứng sau ứng dụng đổi hoàn toàn. Bạn không thể hardcode Pod IP. Một Service là lớp trừu tượng ổn định đặt trước một tập Pod động — một virtual IP cố định (ClusterIP) và tên DNS không bao giờ đổi suốt vòng đời Service, load-balance tới bất kỳ Pod nào hiện khớp label selector của nó. Service liên tục theo dõi backend qua EndpointSlices, nên khi Pod xuất hiện và biến mất, danh sách target của Service tự cập nhật.

Service đứng trước các Pod ephemeral
  1. Client
    Client Pod
  2. phân giải "web" → 10.96.0.42 (ClusterIP ổn định)
    Service
    Service "web"virtual IP, không bao giờ đổi
  3. kube-proxy load-balance qua các endpoint hiện tại — Pod là ephemeral, đổi liên tục
    Pods
    Pod 10.1.1.5
    Pod 10.1.2.8
    Pod 10.1.3.3

kube-proxy cài đặt Service như thế nào

ClusterIP là ảo — không tiến trình nào lắng nghe trên nó, và không máy nào sở hữu nó. kube-proxy, một agent kiểu DaemonSet trên mọi node, theo dõi Service và EndpointSlices rồi lập trình kernel của node sao cho gói tin nhắm tới Service IP được rewrite trong suốt (DNAT) sang một Pod IP thật. Có ba mode:

ModeCơ chếĐặc điểm
iptables (mặc định)Một chuỗi iptables rule mỗi Service; rule xác suất ngẫu nhiên chọn backendĐơn giản, phổ biến. Đánh giá rule O(n) theo số Service — hàng nghìn Service làm chậm cập nhật rule
IPVSKernel IP Virtual Server với load balancer bảng băm thậtScale tới hàng chục nghìn Service; tra cứu O(1); nhiều thuật toán LB (rr, lc, sh)
eBPF (Cilium, không kube-proxy)Chương trình gắn vào đường mạng làm DNAT ngay trong kernelHiệu năng cao nhất; thay thế hoàn toàn kube-proxy; hiệu quả theo từng request

Hệ quả quan trọng: load balancing của Service diễn ra trong kernel của node client, không phải tại một proxy trung tâm. Không có “hộp thắt cổ chai”; mỗi node độc lập forward tới một backend. Đây cũng là load balancing L4 mức connection (mỗi TCP connection), không phải L7 — các connection sống lâu (gRPC, HTTP/2) ghim vào một backend và có thể gây lệch tải, một cạm bẫy phổ biến được giải bằng client-side balancing hoặc service mesh.

EndpointSlices

Đứng sau mỗi Service theo selector, Kubernetes duy trì danh sách IP backend đã sẵn sàng. Ban đầu đây là một object Endpoints duy nhất mỗi Service; một Service 5000 Pod nghĩa là một object khổng lồ bị viết lại mỗi lần Pod thay đổi, làm quá tải etcd và mọi watcher. EndpointSlices (ổn định từ v1.21) chia danh sách đó thành các mảnh ~100 endpoint, nên một thay đổi Pod chỉ viết lại một slice nhỏ. Chúng còn mang dữ liệu phong phú hơn mỗi endpoint: topology hint (cho routing ưu tiên backend cùng zone), trạng thái ready, và thông tin theo từng port. EndpointSlices là nguồn sự thật mà kube-proxy thực sự đọc.

Khái niệm chính

Các loại Service

LoạiLàm gìTiếp cận từDùng khi
ClusterIP (mặc định)Virtual IP nội bộ ổn định + DNSChỉ trong clusterService-to-service nội bộ
NodePortMở một port tĩnh (30000–32767) trên mọi node, forward tới ServiceTừ ngoài, qua <IP-node-bất-kỳ>:<nodePort>Dev, hoặc đứng sau một LB ngoài
LoadBalancerCấp phát một cloud LB L4 trỏ tới Service (qua cloud-controller-manager)Từ ngoài, qua IP/hostname của LBMở ra ngoài ở production
ExternalNameÁnh xạ tên Service tới một tên DNS ngoài qua CNAME — không proxy, không ClusterIPTrong cluster (như một alias DNS)Đặt alias cho DB/API ngoài
Headless (clusterIP: None)Không có virtual IP; DNS trả về trực tiếp các Pod IPTrong clusterStatefulSet, client-side LB, địa chỉ theo từng Pod

NodePortLoadBalancer là các superset: một LoadBalancer Service cũng cấp phát một NodePort, và cloud LB forward tới NodePort đó trên các node. Vậy tầng lớp là LoadBalancer → NodePort → ClusterIP → Pods.

Headless Service và định danh Pod ổn định

Một Service thường giấu Pod của nó sau một virtual IP. Một Service headless (clusterIP: None) làm điều ngược lại: truy vấn DNS cho Service trả về trực tiếp A/AAAA record của tất cả Pod ready, và — khi kết hợp với StatefulSet — mỗi Pod còn có tên DNS ổn định riêng: pod-0.web.prod.svc.cluster.local, pod-1.web..., v.v. Điều này thiết yếu cho các hệ stateful (database, Kafka, etcd) nơi client phải nhắm tới replica cụ thể (ví dụ primary), không phải một replica ngẫu nhiên. Client tự load-balance hoặc peer-discovery từ các IP trả về.

DNS trong cluster (CoreDNS)

Mỗi cluster chạy một DNS server — CoreDNS (một server linh hoạt dựa trên plugin; mặc định hiện đại, thay cho kube-dns). Kubelet cấu hình /etc/resolv.conf của mỗi Pod trỏ tới ClusterIP của CoreDNS và tiêm các search domain, nên tên ngắn phân giải được. Sơ đồ đặt tên:

# Từ trong một Pod
nslookup web                       # tên ngắn, cùng namespace
nslookup web.prod.svc.cluster.local
dig +short db-0.db.prod.svc.cluster.local   # một replica StatefulSet cụ thể

DNS là xương sống của service discovery: ứng dụng kết nối tới tên, không phải IP, và Kubernetes giữ ánh xạ tên→IP luôn cập nhật. Khi “app không kết nối được database,” phân giải DNS bên trong Pod là một trong những thứ đầu tiên cần kiểm tra.

Ingress và Ingress controller

Một LoadBalancer Service cho bạn một external IP mỗi Service — tốn kém và chỉ L4. Ingress giải bài toán HTTP(S) routing L7: một điểm vào duy nhất route theo hostpath tới nhiều backend Service, terminate TLS, và gom việc mở ra ngoài sau một load balancer.

Điều then chốt: Ingress resource chỉ là cấu hình — bản thân nó không làm gì. Bạn phải chạy một Ingress controller (một Pod theo dõi các object Ingress và lập trình một proxy thật). Các controller phổ biến: ingress-nginx (dựa trên NGINX, được triển khai rộng nhất), Traefik, HAProxy, Contour (dựa trên Envoy), và các loại cloud-native (AWS Load Balancer Controller, GKE Ingress). Bạn chọn controller nào xử lý một Ingress qua ingressClassName.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod   # tự cấp phát TLS
spec:
  ingressClassName: nginx
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls          # cert lưu ở đây (bởi cert-manager)
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service: { name: api, port: { number: 80 } }
          - path: /
            pathType: Prefix
            backend:
              service: { name: web, port: { number: 80 } }

pathType quan trọng: Prefix khớp theo đoạn path (/api khớp /api/v1), Exact khớp toàn bộ path theo nghĩa đen, ImplementationSpecific giao cho controller quyết định. Hành vi nâng cao (rewrite, rate limit, auth, canary) được cấu hình qua annotation riêng của từng controller — chính là sự phân mảnh mà Gateway API được tạo ra để khắc phục.

Gateway API — kế thừa Ingress

Ingress chạm trần: nó chỉ thực sự mô hình hóa HTTP host/path routing, và mọi thứ vượt ra ngoài (TLS passthrough, khớp header, chia traffic, gRPC) rò rỉ vào các annotation không portable. Gateway API (GA từ v1.0, một dự án chính thức của Kubernetes SIG-Network) là bản thay thế hiện đại. Nó hướng vai trò (role-oriented) — tách trách nhiệm ra các resource thuộc sở hữu của các team khác nhau:

ResourceAi sở hữuMục đích
GatewayClassNhà cung cấp hạ tầngController/implementation (như StorageClass, nhưng cho gateway)
GatewayOperator cluster/platformMột listener thật: port, protocol, cấu hình TLS, route được phép
HTTPRoute (và TCPRoute, GRPCRoute, TLSRoute)Developer ứng dụngRule routing gắn vào một Gateway: match, filter, backend, weight

Sự tách bạch này cho phép team platform sở hữu Gateway (và TLS/bảo mật của nó) trong khi team ứng dụng quản lý route của mình mà không đụng tới hạ tầng dùng chung. Nó hỗ trợ chia/gán trọng số traffic, khớp header/method, và routing xuyên namespace một cách gốc — không còn “canh annotation”. Nó mở rộng theo protocol và là hướng đi của hệ sinh thái; các implementation mới (Envoy Gateway, Cilium, Istio, NGINX Gateway Fabric) đều nhắm tới nó. Hãy xem Ingress là ổn-định-nhưng-đóng-băng và Gateway API là con đường phía trước.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: shop }
spec:
  parentRefs: [{ name: prod-gateway }]
  hostnames: ["shop.example.com"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: /api } }]
      backendRefs: [{ name: api, port: 80 }]
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs:                      # canary: chia traffic 90/10
        - { name: web, port: 80, weight: 90 }
        - { name: web-next, port: 80, weight: 10 }

NetworkPolicy

Mặc định, mạng Kubernetes phẳng và mở hoàn toàn: bất kỳ Pod nào cũng kết nối được tới bất kỳ Pod nào khác, ở bất kỳ namespace nào. Đó là một rủi ro bảo mật — một frontend bị chiếm quyền có thể tiếp cận thẳng database của bạn. Một NetworkPolicy là một firewall theo namespace, chọn Pod bằng label và định nghĩa traffic ingress (vào) và egress (ra) được phép.

Các rule cộng dồn và logic rất quan trọng:

Một baseline phổ biến là policy default-deny-all cho mỗi namespace, rồi cho phép rõ ràng:

# 1. Default-deny mọi ingress VÀ egress trong namespace "prod"
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: prod }
spec:
  podSelector: {}                 # chọn mọi Pod trong namespace
  policyTypes: [Ingress, Egress]  # cả hai hướng giờ default-deny
---
# 2. Cho phép các Pod api chỉ nhận traffic từ Pod web, trên 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-allow-web, namespace: prod }
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: web } }
      ports:
        - { protocol: TCP, port: 8080 }
---
# 3. Cho phép mọi Pod phân giải DNS (egress tới CoreDNS) — gần như luôn cần
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: prod }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }

Cạm bẫy kinh điển: áp một policy default-deny-egress và đột nhiên DNS của mọi Pod hỏng, vì CoreDNS giờ bị chặn. Luôn đi kèm việc khóa egress với một allow DNS rõ ràng (rule 3). Với policy phong phú hơn (egress tới FQDN, rule L7, policy toàn cluster), các CNI như Cilium và Calico cung cấp CRD riêng (CiliumNetworkPolicy, Calico GlobalNetworkPolicy) vượt ngoài spec lõi.

Các CNI plugin

Container Network Interface (CNI) là chuẩn mà kubelet dùng để thiết lập Pod networking. Khi một Pod khởi động, kubelet gọi CNI plugin, plugin gán cho Pod một IP và nối network namespace của nó vào mạng cluster. CNI bạn chọn quyết định hiệu năng, kiểu encapsulation dùng, và liệu NetworkPolicy có được thực thi hay không.

CNIDataplaneNetworkPolicyGhi chú
CalicoL3 routing (BGP) hoặc overlay VXLAN/IP-in-IPCó (phong phú, CRD mở rộng)Trưởng thành, hiệu năng cao, policy engine mạnh; chạy được không cần overlay
CiliumeBPFCó (gồm L7/HTTP, DNS, FQDN)Hiện đại; thay được kube-proxy; observability sâu (Hubble); policy theo identity
FlannelOverlay VXLANKhông (cần Calico cho policy)Đơn giản nhất để chạy; tốt để học; không thực thi policy gốc
AWS VPC CNIENI VPC gốcQua một policy agentPod nhận IP VPC thật (tích hợp tuyệt, giới hạn mật độ ENI)

Overlay (VXLAN) đóng gói traffic Pod trong UDP để đi qua mạng nền bất kỳ với chút chi phí CPU/MTU; routing gốc (Calico BGP, AWS VPC CNI) tránh encapsulation cho overhead thấp hơn nhưng đòi mạng nền route được Pod IP. Với đa số cluster mới, lựa chọn là Cilium (eBPF, observability, thay kube-proxy) hoặc Calico (đã kiểm chứng, linh hoạt); Flannel vẫn phổ biến vì sự đơn giản khi bạn không cần NetworkPolicy.

Service discovery, từ đầu tới cuối

Ghép các tầng lại, đây là cách một request tìm tới đích:

  1. App kết nối tới một tên (http://api) — không bao giờ IP hardcode.
  2. CoreDNS phân giải apiapi.prod.svc.cluster.localClusterIP ổn định của Service.
  3. kube-proxy của node client (iptables/IPVS/eBPF) DNAT ClusterIP sang một Pod IP thật, chọn từ EndpointSlices hiện tại của Service.
  4. CNI route gói tin qua mạng Pod phẳng tới node/Pod đích.
  5. NetworkPolicy (do CNI thực thi) cho phép hoặc drop luồng.

Nếu bất kỳ bước nào sai cấu hình, kết nối sẽ hỏng, và mỗi bước có triệu chứng riêng — đó là lý do debug có phương pháp, từng tầng một, tốt hơn đoán mò.

Best Practices

  1. Kết nối tới tên DNS của Service, không bao giờ Pod IP. Pod IP là ephemeral; tên Service ổn định. Hardcode IP sẽ vỡ ngay lần reschedule đầu và phá bỏ toàn bộ lớp trừu tượng.
  2. Ưu tiên ClusterIP + Ingress/Gateway thay vì LoadBalancer cho từng Service. Một LoadBalancer mỗi Service tốn kém và chỉ L4; gom việc mở HTTP sau một Ingress/Gateway duy nhất với host/path routing và TLS dùng chung.
  3. Áp baseline NetworkPolicy default-deny, rồi cho phép rõ ràng. Mạng mặc định mở; khóa mỗi namespace về least-privilege và chỉ mở các luồng cần — điều này khoanh vùng lateral movement sau khi bị chiếm quyền.
  4. Luôn cho phép egress DNS khi khóa egress. Một policy default-deny-egress âm thầm phá phân giải CoreDNS; đi kèm nó với một allow UDP/TCP 53 tới kube-system, nếu không chẳng gì phân giải được.
  5. Xác minh CNI của bạn thực sự thực thi NetworkPolicy. Flannel thuần âm thầm bỏ qua policy — viết chúng cho cảm giác an toàn giả. Dùng Calico/Cilium (hoặc Flannel+Calico) nơi policy quan trọng.
  6. Đặt readiness probe để Service chỉ route tới Pod đã ready. Endpoint phản ánh trạng thái ready; một Pod không có readiness probe nhận traffic trước khi phục vụ được, gây lỗi trong lúc rollout và scale-up.
  7. Chọn đúng loại Service một cách có chủ đích. ClusterIP cho nội bộ, Ingress/Gateway cho HTTP ingress, LoadBalancer chỉ cho non-HTTP hoặc L4 thô, Headless cho StatefulSet/client-side LB. NodePort chủ yếu là primitive cho dev/đứng-sau-LB, không phải cửa ngõ production.
  8. Đưa routing ra ngoài mới sang Gateway API. Ingress ổn định nhưng đóng băng và lệ thuộc annotation; Gateway API cho routing portable, tách vai trò, có khả năng chia traffic. Bắt đầu các service mới ở đó.
  9. Dùng Headless Service cho workload stateful. Database và các hệ cluster cần định danh DNS ổn định theo từng Pod (pod-0.svc…), thứ mà ClusterIP thường giấu đi. Ghép StatefulSet với một headless Service.
  10. Tự động hóa TLS bằng cert-manager. Đừng quản cert thủ công trên Ingress/Gateway; cert-manager cấp phát và gia hạn (ví dụ Let’s Encrypt) tự động, xóa bỏ sự cố hết hạn cert.
  11. Ưu tiên IPVS hoặc eBPF cho cluster lớn. Rule Service của iptables scale O(n); ở hàng nghìn Service, chuyển kube-proxy sang IPVS hoặc dùng dataplane eBPF của Cilium để giữ lập trình rule nhanh.
  12. Cẩn thận load balancing L4 cho HTTP/2 và gRPC. kube-proxy balance theo từng connection; các connection multiplex sống lâu ghim vào một backend. Dùng client-side load balancing, headless Service, hoặc service mesh để trải traffic đó.
  13. Gán label cho Pod nhất quán và có ý nghĩa. Service, NetworkPolicy và EndpointSlices đều dựa vào label; một label thiếu hoặc sai âm thầm loại một Pod khỏi Service hoặc khỏi phạm vi một policy.
  14. Hạn chế dải nguồn của LoadBalancer/Ingress. Dùng loadBalancerSourceRanges hoặc annotation WAF/ingress để giới hạn ai tiếp cận được endpoint công khai; đừng phơi API admin hay nội bộ ra 0.0.0.0/0.
  15. Kiểm thử policy và routing trước khi rollout. Xác thực NetworkPolicy bằng test kết nối (Cilium connectivity test, kubectl exec curl) và rule Ingress ở staging; một selector sai có thể “hố đen” traffic hoặc để nó mở toang.
  16. Cấp tài nguyên và autoscaling cho CoreDNS. DNS nằm trên đường tới hạn của mọi request; CoreDNS thiếu tài nguyên gây độ trễ toàn cluster và lỗi phân giải chập chờn. Giám sát và scale nó theo kích thước cluster.

Tài liệu tham khảo

Part of the Kubernetes Roadmap knowledge base.

Overview

Networking is where Kubernetes stops being “Docker with a scheduler” and becomes a distributed system. Pods are ephemeral — they come and go, get rescheduled onto other nodes, and change IPs every time. Yet applications need stable addresses, load balancing, service discovery, and controlled exposure to the outside world. Kubernetes networking exists to bridge that gap: it gives every Pod a real IP, lets any Pod talk to any other Pod without NAT, and layers Services, DNS, Ingress, and NetworkPolicy on top to provide stable naming, traffic distribution, external access, and security segmentation.

The design is deliberately simple at the base and pluggable above it. The base is the Kubernetes network model — a small set of requirements every cluster must satisfy. Everything else (how packets actually move, which load-balancer gets provisioned, how DNS resolves) is implemented by pluggable components: a CNI plugin for Pod networking, kube-proxy (or an eBPF dataplane) for Service routing, CoreDNS for name resolution, and an Ingress/Gateway controller for HTTP routing. Understanding which layer owns which responsibility is the key to debugging networking, because a “connection refused” can originate in any one of them.

A mental model

Think of Kubernetes networking as four decoupled layers stacked on the flat Pod network. Layer 0 — Pod network: every Pod has an IP and can reach every other Pod directly (the CNI’s job). Layer 1 — Service: a stable virtual IP + DNS name that load-balances to a changing set of Pods (kube-proxy’s job). Layer 2 — DNS: turns names like web.prod.svc.cluster.local into Service IPs (CoreDNS’s job). Layer 3 — Ingress/Gateway: L7 HTTP routing and TLS from outside the cluster (the controller’s job). Layer 4 — NetworkPolicy: a firewall that decides which of those flows are actually allowed (the CNI’s job again). Each layer depends on the one below but is otherwise independent — and each can fail independently.

Fundamentals

The Kubernetes network model

Kubernetes does not implement networking itself; it mandates a model and lets CNI plugins satisfy it. Every conforming cluster guarantees:

  1. Every Pod gets its own unique, cluster-wide IP address. A Pod’s containers share that IP (and a network namespace), reaching each other over localhost.
  2. Every Pod can communicate with every other Pod on any node without NAT. The network is flat — Pod IPs are routable across the whole cluster.
  3. Agents on a node (kubelet, system daemons) can reach all Pods on that node.
  4. The IP a Pod sees itself as is the same IP others use to reach it. No source-NAT rewriting inside the Pod network.

This “IP-per-Pod, flat network, no NAT” model is what makes Kubernetes networking feel uniform. It removes the port-mapping gymnastics of plain Docker (-p 8080:80) — inside the cluster, a Pod just listens on its real port and is reachable at its real IP. The trade-off is that it demands real routing infrastructure, which is precisely what the CNI plugin provides.

The problem Services solve

Pod IPs are unstable: scale up, roll out a new version, or lose a node, and the set of IPs backing your app changes completely. You cannot hardcode Pod IPs. A Service is a stable abstraction in front of a dynamic set of Pods — a fixed virtual IP (the ClusterIP) and DNS name that never changes for the life of the Service, load-balancing to whichever Pods currently match its label selector. The Service continuously tracks its backends through EndpointSlices, so as Pods appear and disappear the Service’s target list updates automatically.

A Service in front of ephemeral Pods
  1. Client
    Client Pod
  2. resolves "web" → 10.96.0.42 (stable ClusterIP)
    Service
    Service "web"virtual IP, never changes
  3. kube-proxy load-balances across current endpoints — Pods are ephemeral, change constantly
    Pods
    Pod 10.1.1.5
    Pod 10.1.2.8
    Pod 10.1.3.3

How kube-proxy implements Services

A ClusterIP is virtual — no process listens on it, and no single machine owns it. kube-proxy, a DaemonSet-like agent on every node, watches Services and EndpointSlices and programs the node’s kernel so that packets destined for a Service IP are transparently rewritten (DNAT) to a real Pod IP. There are three modes:

ModeMechanismCharacteristics
iptables (default)A chain of iptables rules per Service; a random-probability rule picks a backendSimple, ubiquitous. Rule evaluation is O(n) with Service count — thousands of Services slow rule updates
IPVSKernel IP Virtual Server with a real hash-table load balancerScales to tens of thousands of Services; O(1) lookups; more LB algorithms (rr, lc, sh)
eBPF (Cilium, no kube-proxy)Programs attached to the network path do the DNAT in the kernelHighest performance; can replace kube-proxy entirely; per-request efficiency

The important consequence: Service load balancing happens in the kernel of the client’s node, not at a central proxy. There is no bottleneck box; each node independently forwards to a backend. It is also connection-level L4 load balancing (per TCP connection), not L7 — long-lived connections (gRPC, HTTP/2) pin to one backend and can unbalance, which is a common gotcha solved by client-side balancing or a service mesh.

EndpointSlices

Behind every selector-based Service, Kubernetes maintains the list of ready backend IPs. Originally this was a single Endpoints object per Service; a 5000-Pod Service meant one enormous object rewritten on every Pod change, overwhelming etcd and every watcher. EndpointSlices (stable since v1.21) shard that list into chunks of ~100 endpoints, so a single Pod change rewrites only one small slice. They also carry richer per-endpoint data: topology hints (for topology-aware routing that prefers same-zone backends), readiness, and per-port info. EndpointSlices are the source of truth kube-proxy actually reads.

Key Concepts

Service types

TypeWhat it doesReachable fromTypical use
ClusterIP (default)Stable internal virtual IP + DNSInside cluster onlyInternal service-to-service
NodePortOpens a static port (30000–32767) on every node, forwarding to the ServiceOutside, via <any-node-IP>:<nodePort>Dev, or behind an external LB
LoadBalancerProvisions a cloud L4 load balancer pointing at the Service (via cloud-controller-manager)Outside, via the LB’s IP/hostnameProduction external exposure
ExternalNameMaps the Service name to an external DNS name via a CNAME — no proxying, no ClusterIPInside cluster (as a DNS alias)Aliasing an external DB/API
Headless (clusterIP: None)No virtual IP; DNS returns the Pod IPs directlyInside clusterStatefulSets, client-side LB, per-Pod addressing

NodePort and LoadBalancer are supersets: a LoadBalancer Service also allocates a NodePort, and the cloud LB forwards to that NodePort on the nodes. So the layering is LoadBalancer → NodePort → ClusterIP → Pods.

Headless Services and stable Pod identity

A normal Service hides its Pods behind one virtual IP. A headless Service (clusterIP: None) does the opposite: a DNS query for the Service returns the A/AAAA records of all ready Pods directly, and — when combined with a StatefulSet — each Pod also gets its own stable DNS name: pod-0.web.prod.svc.cluster.local, pod-1.web..., etc. This is essential for stateful systems (databases, Kafka, etcd) where clients must address specific replicas (e.g., the primary), not a random one. The client does its own load balancing or peer discovery from the returned IPs.

DNS in the cluster (CoreDNS)

Every cluster runs a DNS server — CoreDNS (a flexible, plugin-based server; the modern default, replacing kube-dns). The kubelet configures every Pod’s /etc/resolv.conf to point at CoreDNS’s ClusterIP and injects search domains, so short names resolve. The naming scheme:

# From inside a Pod
nslookup web                       # short name, same namespace
nslookup web.prod.svc.cluster.local
dig +short db-0.db.prod.svc.cluster.local   # a specific StatefulSet replica

DNS is the backbone of service discovery: applications connect to names, not IPs, and Kubernetes keeps the name→IP mapping current. When “the app can’t reach the database,” DNS resolution inside the Pod is one of the first things to check.

Ingress and Ingress controllers

A LoadBalancer Service gives you one external IP per Service — expensive and L4-only. Ingress solves L7 HTTP(S) routing: a single entry point that routes by host and path to many backend Services, terminates TLS, and consolidates external exposure behind one load balancer.

Critically, the Ingress resource is just configuration — it does nothing on its own. You must run an Ingress controller (a Pod that watches Ingress objects and programs an actual proxy). Common controllers: ingress-nginx (NGINX-based, the most widely deployed), Traefik, HAProxy, Contour (Envoy-based), and cloud-native ones (AWS Load Balancer Controller, GKE Ingress). You select which controller handles an Ingress via ingressClassName.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod   # auto-provision TLS
spec:
  ingressClassName: nginx
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls          # cert stored here (by cert-manager)
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service: { name: api, port: { number: 80 } }
          - path: /
            pathType: Prefix
            backend:
              service: { name: web, port: { number: 80 } }

pathType matters: Prefix matches path segments (/api matches /api/v1), Exact matches the whole path literally, ImplementationSpecific defers to the controller. Advanced behavior (rewrites, rate limits, auth, canary) is configured through controller-specific annotations — which is exactly the fragmentation the Gateway API was created to fix.

Gateway API — the successor to Ingress

Ingress hit a ceiling: it only really models HTTP host/path routing, and everything beyond that (TLS passthrough, header matching, traffic splitting, gRPC) leaked into non-portable annotations. The Gateway API (GA since v1.0, an official Kubernetes SIG-Network project) is the modern replacement. It is role-oriented — it splits responsibilities across resources owned by different teams:

ResourceOwned byPurpose
GatewayClassInfrastructure providerThe controller/implementation (like a StorageClass, but for gateways)
GatewayCluster/platform operatorAn actual listener: ports, protocols, TLS config, allowed routes
HTTPRoute (also TCPRoute, GRPCRoute, TLSRoute)App developerRouting rules attached to a Gateway: matches, filters, backends, weights

This separation lets platform teams own the Gateway (and its TLS/security) while app teams manage their own routes without touching shared infrastructure. It supports traffic splitting/weighting, header/method matching, and cross-namespace routing natively — no annotation soup. It is protocol-extensible and is where the ecosystem is heading; new implementations (Envoy Gateway, Cilium, Istio, NGINX Gateway Fabric) target it. Treat Ingress as stable-but-frozen and Gateway API as the forward path.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: shop }
spec:
  parentRefs: [{ name: prod-gateway }]
  hostnames: ["shop.example.com"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: /api } }]
      backendRefs: [{ name: api, port: 80 }]
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs:                      # canary: 90/10 traffic split
        - { name: web, port: 80, weight: 90 }
        - { name: web-next, port: 80, weight: 10 }

NetworkPolicy

By default, the Kubernetes network is flat and fully open: any Pod can connect to any other Pod, in any namespace. That is a security liability — a compromised frontend can reach your database directly. A NetworkPolicy is a namespaced firewall that selects Pods by label and defines allowed ingress (inbound) and egress (outbound) traffic.

The rules are additive and the logic is important:

A common baseline is a default-deny-all policy per namespace, then explicit allows:

# 1. Default-deny all ingress AND egress in namespace "prod"
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: prod }
spec:
  podSelector: {}                 # selects every Pod in the namespace
  policyTypes: [Ingress, Egress]  # both directions now default-deny
---
# 2. Allow the api Pods to receive traffic only from web Pods, on 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-allow-web, namespace: prod }
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: web } }
      ports:
        - { protocol: TCP, port: 8080 }
---
# 3. Allow all Pods to resolve DNS (egress to CoreDNS) — almost always needed
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: prod }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }

The classic pitfall: apply a default-deny-egress policy and suddenly every Pod’s DNS breaks, because CoreDNS is now blocked. Always pair egress lockdown with an explicit DNS allow (rule 3). For richer policies (egress to FQDNs, L7 rules, cluster-wide policy), CNIs like Cilium and Calico offer their own CRDs (CiliumNetworkPolicy, Calico GlobalNetworkPolicy) beyond the core spec.

CNI plugins

The Container Network Interface (CNI) is the standard the kubelet uses to set up Pod networking. When a Pod starts, the kubelet calls the CNI plugin, which assigns the Pod an IP and wires its network namespace into the cluster network. The CNI you choose determines performance, the encapsulation used, and whether NetworkPolicy is enforced.

CNIDataplaneNetworkPolicyNotes
CalicoL3 routing (BGP) or VXLAN/IP-in-IP overlayYes (rich, extended CRDs)Mature, high-performance, strong policy engine; can run without an overlay
CiliumeBPFYes (incl. L7/HTTP, DNS, FQDN)Modern; can replace kube-proxy; deep observability (Hubble); identity-based policy
FlannelVXLAN overlayNo (needs Calico for policy)Simplest to run; good for learning; no native policy enforcement
AWS VPC CNINative VPC ENIsVia a policy agentPods get real VPC IPs (great integration, ENI density limits)

Overlays (VXLAN) encapsulate Pod traffic in UDP so it can traverse any underlying network at some CPU/MTU cost; native routing (Calico BGP, AWS VPC CNI) avoids encapsulation for lower overhead but requires the underlying network to route Pod IPs. For most new clusters the choice is Cilium (eBPF, observability, replaces kube-proxy) or Calico (proven, flexible); Flannel remains popular for its simplicity when you don’t need NetworkPolicy.

Service discovery, end to end

Putting the layers together, here is how a request finds its target:

  1. App connects to a name (http://api) — never a hardcoded IP.
  2. CoreDNS resolves apiapi.prod.svc.cluster.local → the Service’s stable ClusterIP.
  3. The client node’s kube-proxy (iptables/IPVS/eBPF) DNATs the ClusterIP to a real Pod IP, chosen from the Service’s current EndpointSlices.
  4. The CNI routes the packet across the flat Pod network to the target node/Pod.
  5. NetworkPolicy (enforced by the CNI) allows or drops the flow.

If any step is misconfigured the connection fails, and each step has a distinct symptom — which is why methodical, layer-by-layer debugging beats guessing.

Best Practices

  1. Connect to Service DNS names, never Pod IPs. Pod IPs are ephemeral; Service names are stable. Hardcoding IPs breaks on the first reschedule and defeats the entire abstraction.
  2. Prefer ClusterIP + Ingress/Gateway over per-Service LoadBalancers. One LoadBalancer per Service is costly and L4-only; consolidate HTTP exposure behind a single Ingress/Gateway with host/path routing and shared TLS.
  3. Adopt a default-deny NetworkPolicy baseline, then allow explicitly. The network is open by default; lock each namespace down to least-privilege and open only required flows — this contains lateral movement after a compromise.
  4. Always allow DNS egress when locking down egress. A default-deny-egress policy silently breaks CoreDNS resolution; pair it with an explicit UDP/TCP 53 allow to kube-system, or nothing resolves.
  5. Verify your CNI actually enforces NetworkPolicy. Plain Flannel ignores policies silently — writing them gives false security. Use Calico/Cilium (or Flannel+Calico) where policy matters.
  6. Set readiness probes so Services only route to ready Pods. Endpoints reflect readiness; a Pod without a readiness probe receives traffic before it can serve it, causing errors during rollouts and scale-ups.
  7. Choose the right Service type deliberately. ClusterIP for internal, Ingress/Gateway for HTTP ingress, LoadBalancer only for non-HTTP or raw L4, Headless for StatefulSets/client-side LB. NodePort is mostly a dev/behind-LB primitive, not a production front door.
  8. Migrate new external routing to the Gateway API. Ingress is stable but frozen and annotation-bound; Gateway API gives portable, role-separated, traffic-splitting-capable routing. Start greenfield services there.
  9. Use Headless Services for stateful workloads. Databases and clustered systems need per-Pod stable DNS identity (pod-0.svc…), which a normal ClusterIP hides. Pair StatefulSets with a headless Service.
  10. Automate TLS with cert-manager. Don’t hand-manage certificates on Ingress/Gateway; cert-manager provisions and renews (e.g., Let’s Encrypt) automatically, eliminating expiry outages.
  11. Prefer IPVS or eBPF for large clusters. iptables Service rules scale O(n); at thousands of Services, switch kube-proxy to IPVS or adopt Cilium’s eBPF dataplane to keep rule programming fast.
  12. Beware L4 load balancing for HTTP/2 and gRPC. kube-proxy balances per-connection; long-lived multiplexed connections pin to one backend. Use client-side load balancing, headless Services, or a service mesh to spread that traffic.
  13. Label Pods consistently and meaningfully. Services, NetworkPolicies, and EndpointSlices all key off labels; an inconsistent or missing label silently removes a Pod from a Service or a policy’s scope.
  14. Restrict LoadBalancer/Ingress source ranges. Use loadBalancerSourceRanges or WAF/ingress annotations to limit who can reach public endpoints; don’t expose admin or internal APIs to 0.0.0.0/0.
  15. Test policies and routing before rollout. Validate NetworkPolicies with connectivity tests (Cilium connectivity test, kubectl exec curl) and Ingress rules in staging; a wrong selector can either black-hole traffic or leave it wide open.
  16. Give CoreDNS resources and autoscaling. DNS is on the critical path of every request; under-provisioned CoreDNS causes cluster-wide latency and intermittent resolution failures. Monitor it and scale it with cluster size.

References