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, Ingress và NetworkPolicy 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:
- 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. - 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.
- Các agent trên một node (kubelet, system daemon) tiếp cận được mọi Pod trên node đó.
- 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.
- ClientClient Pod
- phân giải "web" → 10.96.0.42 (ClusterIP ổn định)ServiceService "web"virtual IP, không bao giờ đổi
- kube-proxy load-balance qua các endpoint hiện tại — Pod là ephemeral, đổi liên tụcPodsPod 10.1.1.5Pod 10.1.2.8Pod 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:
| Mode | Cơ 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 |
| IPVS | Kernel IP Virtual Server với load balancer bảng băm thật | Scale 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 kernel | Hiệ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ại | Làm gì | Tiếp cận từ | Dùng khi |
|---|---|---|---|
| ClusterIP (mặc định) | Virtual IP nội bộ ổn định + DNS | Chỉ trong cluster | Service-to-service nội bộ |
| NodePort | Mở một port tĩnh (30000–32767) trên mọi node, forward tới Service | Từ ngoài, qua <IP-node-bất-kỳ>:<nodePort> | Dev, hoặc đứng sau một LB ngoài |
| LoadBalancer | Cấp phát một cloud LB L4 trỏ tới Service (qua cloud-controller-manager) | Từ ngoài, qua IP/hostname của LB | Mở 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 ClusterIP | Trong 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 IP | Trong cluster | StatefulSet, client-side LB, địa chỉ theo từng Pod |
NodePort và LoadBalancer 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:
- A record của Service:
<service>.<namespace>.svc.<cluster-domain>→ ClusterIP của Service. Cluster domain mặc định làcluster.local. - Tên ngắn: từ một Pod trong namespace
prod,webphân giải thànhweb.prod.svc.cluster.local(cùng namespace), vàweb.stagingthànhweb.staging.svc.cluster.local, nhờ cácsearchdomain trongresolv.conf. - Headless Service: trả về nhiều A record (một cho mỗi Pod ready) thay vì một ClusterIP duy nhất.
- Pod DNS (StatefulSet):
<pod-name>.<service>.<namespace>.svc.cluster.localcho mỗi replica một tên ổn định. - SRV record:
_<port-name>._<proto>.<service>.<namespace>.svc.cluster.localcho các named port.
# 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 host và path 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:
| Resource | Ai sở hữu | Mục đích |
|---|---|---|
| GatewayClass | Nhà cung cấp hạ tầng | Controller/implementation (như StorageClass, nhưng cho gateway) |
| Gateway | Operator cluster/platform | Một listener thật: port, protocol, cấu hình TLS, route được phép |
| HTTPRoute (và TCPRoute, GRPCRoute, TLSRoute) | Developer ứng dụng | Rule 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 Pod “chưa được chọn” (mở hoàn toàn) cho tới khi một NetworkPolicy chọn nó. Khi bất kỳ policy nào chọn một Pod cho một hướng (ingress/egress), hướng đó lật sang default-deny, và chỉ traffic được cho phép rõ ràng mới đi qua.
- Policy chỉ cho-phép (allow-only) — không có rule “deny”; bạn từ chối bằng cách không cho phép.
- NetworkPolicy được thực thi bởi CNI plugin. Một cluster mà CNI không hỗ trợ (ví dụ Flannel thuần) sẽ âm thầm bỏ qua policy — một cảm giác an toàn giả nguy hiểm.
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.
| CNI | Dataplane | NetworkPolicy | Ghi chú |
|---|---|---|---|
| Calico | L3 routing (BGP) hoặc overlay VXLAN/IP-in-IP | Có (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 |
| Cilium | eBPF | Có (gồm L7/HTTP, DNS, FQDN) | Hiện đại; thay được kube-proxy; observability sâu (Hubble); policy theo identity |
| Flannel | Overlay VXLAN | Khô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 CNI | ENI VPC gốc | Qua một policy agent | Pod 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:
- App kết nối tới một tên (
http://api) — không bao giờ IP hardcode. - CoreDNS phân giải
api→api.prod.svc.cluster.local→ ClusterIP ổn định của Service. - 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.
- CNI route gói tin qua mạng Pod phẳng tới node/Pod đích.
- 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
- 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.
- Ư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.
- Á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.
- 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.
- 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.
- Đặ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.
- 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.
- Đư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 ở đó.
- 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. - 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.
- Ư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.
- 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 đó.
- 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.
- Hạn chế dải nguồn của LoadBalancer/Ingress. Dùng
loadBalancerSourceRangeshoặ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ộ ra0.0.0.0/0. - 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 execcurl) và rule Ingress ở staging; một selector sai có thể “hố đen” traffic hoặc để nó mở toang. - 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
- Kubernetes — Cluster Networking (network model)
- Kubernetes — Service
- Kubernetes — EndpointSlices
- Kubernetes — DNS for Services and Pods
- Kubernetes — Ingress · Ingress Controllers
- Kubernetes — Network Policies
- Kubernetes — Virtual IPs and Service Proxies (các mode kube-proxy)
- Gateway API
- ingress-nginx · Traefik · Contour
- Calico · Cilium · Flannel
- CoreDNS · cert-manager
- CNCF landscape — networking
- roadmap.sh — Kubernetes
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:
- 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. - 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.
- Agents on a node (kubelet, system daemons) can reach all Pods on that node.
- 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.
- ClientClient Pod
- resolves "web" → 10.96.0.42 (stable ClusterIP)ServiceService "web"virtual IP, never changes
- kube-proxy load-balances across current endpoints — Pods are ephemeral, change constantlyPodsPod 10.1.1.5Pod 10.1.2.8Pod 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:
| Mode | Mechanism | Characteristics |
|---|---|---|
| iptables (default) | A chain of iptables rules per Service; a random-probability rule picks a backend | Simple, ubiquitous. Rule evaluation is O(n) with Service count — thousands of Services slow rule updates |
| IPVS | Kernel IP Virtual Server with a real hash-table load balancer | Scales 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 kernel | Highest 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
| Type | What it does | Reachable from | Typical use |
|---|---|---|---|
| ClusterIP (default) | Stable internal virtual IP + DNS | Inside cluster only | Internal service-to-service |
| NodePort | Opens a static port (30000–32767) on every node, forwarding to the Service | Outside, via <any-node-IP>:<nodePort> | Dev, or behind an external LB |
| LoadBalancer | Provisions a cloud L4 load balancer pointing at the Service (via cloud-controller-manager) | Outside, via the LB’s IP/hostname | Production external exposure |
| ExternalName | Maps the Service name to an external DNS name via a CNAME — no proxying, no ClusterIP | Inside cluster (as a DNS alias) | Aliasing an external DB/API |
Headless (clusterIP: None) | No virtual IP; DNS returns the Pod IPs directly | Inside cluster | StatefulSets, 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:
- Service A record:
<service>.<namespace>.svc.<cluster-domain>→ the Service’s ClusterIP. Default cluster domain iscluster.local. - Short names: from a Pod in namespace
prod,webresolves toweb.prod.svc.cluster.local(same namespace), andweb.stagingtoweb.staging.svc.cluster.local, thanks to thesearchdomains inresolv.conf. - Headless Service: returns multiple A records (one per ready Pod) instead of a single ClusterIP.
- Pod DNS (StatefulSet):
<pod-name>.<service>.<namespace>.svc.cluster.localgives each replica a stable name. - SRV records:
_<port-name>._<proto>.<service>.<namespace>.svc.cluster.localfor named ports.
# 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:
| Resource | Owned by | Purpose |
|---|---|---|
| GatewayClass | Infrastructure provider | The controller/implementation (like a StorageClass, but for gateways) |
| Gateway | Cluster/platform operator | An actual listener: ports, protocols, TLS config, allowed routes |
| HTTPRoute (also TCPRoute, GRPCRoute, TLSRoute) | App developer | Routing 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 Pod is “unselected” (fully open) until some NetworkPolicy selects it. Once any policy selects a Pod for a direction (ingress/egress), that direction flips to default-deny, and only explicitly allowed traffic passes.
- Policies are allow-only — there is no “deny” rule; you deny by not allowing.
- NetworkPolicy is enforced by the CNI plugin. A cluster whose CNI doesn’t support it (e.g., plain Flannel) silently ignores the policies — a dangerous false sense of security.
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.
| CNI | Dataplane | NetworkPolicy | Notes |
|---|---|---|---|
| Calico | L3 routing (BGP) or VXLAN/IP-in-IP overlay | Yes (rich, extended CRDs) | Mature, high-performance, strong policy engine; can run without an overlay |
| Cilium | eBPF | Yes (incl. L7/HTTP, DNS, FQDN) | Modern; can replace kube-proxy; deep observability (Hubble); identity-based policy |
| Flannel | VXLAN overlay | No (needs Calico for policy) | Simplest to run; good for learning; no native policy enforcement |
| AWS VPC CNI | Native VPC ENIs | Via a policy agent | Pods 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:
- App connects to a name (
http://api) — never a hardcoded IP. - CoreDNS resolves
api→api.prod.svc.cluster.local→ the Service’s stable ClusterIP. - The client node’s kube-proxy (iptables/IPVS/eBPF) DNATs the ClusterIP to a real Pod IP, chosen from the Service’s current EndpointSlices.
- The CNI routes the packet across the flat Pod network to the target node/Pod.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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.
- Restrict LoadBalancer/Ingress source ranges. Use
loadBalancerSourceRangesor WAF/ingress annotations to limit who can reach public endpoints; don’t expose admin or internal APIs to0.0.0.0/0. - Test policies and routing before rollout. Validate NetworkPolicies with connectivity tests (Cilium
connectivity test,kubectl execcurl) and Ingress rules in staging; a wrong selector can either black-hole traffic or leave it wide open. - 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
- Kubernetes — Cluster Networking (the network model)
- Kubernetes — Service
- Kubernetes — EndpointSlices
- Kubernetes — DNS for Services and Pods
- Kubernetes — Ingress · Ingress Controllers
- Kubernetes — Network Policies
- Kubernetes — Virtual IPs and Service Proxies (kube-proxy modes)
- Gateway API
- ingress-nginx · Traefik · Contour
- Calico · Cilium · Flannel
- CoreDNS · cert-manager
- CNCF landscape — networking
- roadmap.sh — Kubernetes