Mở đầu — vì sao bài này quan trọng
Nếu bạn từng deploy một ứng dụng lên Kubernetes rồi tự hỏi: "Pod đã chạy rồi, mà sao gọi từ ngoài vào không được?", "Tại sao service này gọi được service kia mà service kia lại gọi ngược không nổi?", hay "Làm sao chặn không cho service thanh toán bị các service linh tinh khác chọc vào?" — thì bài này chính là chỗ để bạn gỡ rối.
Networking là một trong những phần khiến người mới học Kubernetes bối rối nhất, vì nó có nhiều lớp trừu tượng chồng lên nhau: Pod có IP riêng, rồi lại có Service với IP ảo, rồi Ingress đứng ở tầng HTTP, rồi NetworkPolicy để kiểm soát ai được nói chuyện với ai. Mỗi lớp giải quyết một bài toán khác nhau, và nếu bạn không phân biệt được ranh giới giữa chúng, bạn sẽ debug mù mờ, thử sai lung tung, và mất cả buổi chiều cho một lỗi lẽ ra chỉ mất 5 phút.
Với vai trò DevOps Engineer, mạng trong Kubernetes là thứ bạn sẽ đụng đến mỗi ngày: expose một API ra internet, kết nối microservices nội bộ, phân vùng bảo mật (zero-trust), và xử lý sự cố khi traffic không đi đúng đường. Bài này tập trung vào ba trụ cột: Service (kết nối và load balancing bên trong cluster), Ingress (đưa traffic HTTP/HTTPS từ ngoài vào), và NetworkPolicy (firewall ở tầng Pod). Nắm chắc ba thứ này, bạn đã cầm trong tay 90% những gì cần để vận hành mạng của một cluster production.
Khái niệm cốt lõi
Mô hình mạng của Kubernetes
Trước khi nói về Service hay Ingress, phải hiểu nền tảng: mô hình mạng phẳng (flat network model) của Kubernetes. Quy tắc cốt lõi rất đơn giản:
- Mỗi Pod có một địa chỉ IP riêng, duy nhất trong toàn cluster. Không phải IP của node, mà là IP riêng của Pod (ví dụ
10.244.3.17). - Mọi Pod có thể gọi trực tiếp đến IP của mọi Pod khác mà không cần NAT, bất kể chúng nằm trên node nào. Đây gọi là "pod-to-pod routable".
- Các container trong cùng một Pod chia sẻ chung network namespace, nên gọi nhau qua
localhost.
Điểm mấu chốt cần nhớ: IP của Pod là phù du (ephemeral). Pod chết đi, được scheduler tạo lại, IP mới hoàn toàn khác. Vì vậy bạn KHÔNG BAO GIỜ được hardcode IP của Pod trong cấu hình. Đây chính là lý do Service ra đời.
Service — địa chỉ ổn định cho một nhóm Pod
Service là một lớp trừu tượng cung cấp một IP ảo (ClusterIP) và tên DNS ổn định đứng trước một nhóm Pod. Dù các Pod phía sau có chết đi sống lại, IP và tên của Service không đổi. Service dùng label selector để biết những Pod nào thuộc về nó.
Cơ chế bên dưới: mỗi node chạy kube-proxy, thành phần này lập trình các quy tắc (iptables hoặc IPVS) để khi có traffic gửi đến ClusterIP, nó được load balance đến một trong các Pod đang khỏe mạnh (được liệt kê trong Endpoints/EndpointSlice). Bạn gọi Service, kube-proxy lo phần chia tải.
Có bốn loại Service chính, hãy phân biệt rõ:
- ClusterIP (mặc định): chỉ truy cập được từ bên trong cluster. Dùng cho giao tiếp nội bộ giữa các microservices. Ví dụ frontend gọi
http://api-service:8080. - NodePort: mở một cổng (thường trong dải 30000–32767) trên MỌI node, cho phép truy cập từ ngoài qua
IP-của-node:NodePort. Đơn giản nhưng thô, ít dùng cho production trực tiếp. - LoadBalancer: trên cloud (AWS, GCP, Azure), tự động tạo một load balancer thật của nhà cung cấp (ELB/NLB, GCP LB...) trỏ vào Service. Đây là cách phổ biến để expose một Service ra internet.
- ExternalName: ánh xạ Service tới một tên DNS bên ngoài (CNAME), dùng để trỏ tới dịch vụ ngoài cluster.
<service-name>.<namespace>.svc.cluster.local. Trong cùng namespace, bạn chỉ cần gọi ngắn gọn service-name. Khác namespace thì gọi service-name.namespace. Đây là cách microservices tìm nhau — dựa vào tên chứ không phải IP.Ingress — cửa ngõ HTTP/HTTPS thông minh
Nếu mỗi API bạn muốn expose ra ngoài đều tạo một Service LoadBalancer riêng, bạn sẽ có hàng chục load balancer của cloud, mỗi cái tốn tiền và một IP public riêng. Rất lãng phí. Ingress giải quyết chuyện này: nó là một tài nguyên cho phép bạn định tuyến traffic HTTP/HTTPS dựa trên hostname và đường dẫn (path), chỉ qua MỘT điểm vào duy nhất.
Cần phân biệt hai khái niệm dễ lẫn:
- Ingress resource: là file YAML mô tả quy tắc định tuyến ("host
api.abc.vnpath/ordersthì gửi tới Serviceorder-service"). - Ingress Controller: là thành phần thực thi thật sự đọc các quy tắc đó và làm việc chuyển tiếp traffic. Phổ biến nhất là ingress-nginx, ngoài ra có Traefik, HAProxy, hoặc các controller của cloud. Bản thân Kubernetes KHÔNG có sẵn Ingress Controller — bạn phải cài. Đây là lỗi kinh điển: viết Ingress resource mà quên cài controller, rồi ngồi thắc mắc sao không chạy.
NetworkPolicy — firewall ở tầng Pod
Mặc định, mô hình mạng phẳng của Kubernetes cho phép mọi Pod nói chuyện với mọi Pod — cực kỳ thoải mái nhưng cũng cực kỳ nguy hiểm về bảo mật. Nếu một Pod bị hack, kẻ tấn công có thể lateral movement (di chuyển ngang) tới mọi service khác, kể cả database.
NetworkPolicy là "firewall" ở tầng Pod, cho phép bạn khai báo Pod nào được nhận traffic từ đâu (ingress rules) và được gửi traffic đến đâu (egress rules), dựa trên label selector, namespace, hoặc dải IP. Hai điều PHẢI nhớ:
- NetworkPolicy chỉ có tác dụng nếu CNI plugin hỗ trợ nó. Calico và Cilium hỗ trợ tốt; Flannel thuần thì KHÔNG. Bạn apply policy trên Flannel, nó im lặng không báo lỗi nhưng cũng chẳng chặn gì — một cái bẫy nguy hiểm.
- Cơ chế mặc định là "cho phép hết", nhưng khi một Pod bị khớp bởi ít nhất một policy, nó chuyển sang "default deny" cho hướng đó. Nghĩa là policy hoạt động theo whitelist: khi đã có policy chọn Pod, chỉ traffic được liệt kê rõ ràng mới được đi, phần còn lại bị chặn.
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT "ChợViệt" và bài toán một domain nhiều service
Một startup thương mại điện tử tại TP.HCM, gọi là ChợViệt, chạy khoảng 12 microservices trên EKS (Kubernetes trên AWS). Ban đầu team dùng Service LoadBalancer cho mỗi service cần public: web, API sản phẩm, API đơn hàng, API thanh toán. Kết quả: 4 con AWS Network Load Balancer, mỗi con tốn khoảng 18-20 USD/tháng cộng phí xử lý dữ liệu, và 4 IP public khác nhau khiến việc cấu hình DNS lẫn SSL trở nên rối rắm.
Team chuyển sang mô hình một ingress-nginx duy nhất. Chỉ còn MỘT LoadBalancer đứng trước ingress-nginx, và một Ingress resource định tuyến: chovietapp.vn/ tới web-service, chovietapp.vn/api/products tới product-service, chovietapp.vn/api/orders tới order-service. Chứng chỉ HTTPS được cert-manager cấp tự động từ Let's Encrypt, gắn ngay tại Ingress.
Bài học rút ra: khi bạn có nhiều dịch vụ HTTP cần expose, đừng nhân bản LoadBalancer. Một Ingress Controller gom tất cả về một điểm vào, cắt giảm chi phí (từ ~80 USD xuống ~20 USD/tháng cho phần LB), đơn giản hóa quản lý DNS và SSL. LoadBalancer Service phù hợp khi bạn cần expose giao thức không phải HTTP (ví dụ TCP thuần cho một game server hay gRPC đặc thù).
Ví dụ 2 — Ví điện tử và yêu cầu cô lập service thanh toán
Một công ty fintech ở Hà Nội vận hành ví điện tử, cluster có namespace payment chứa service xử lý giao dịch và một database nội bộ. Trong đợt audit bảo mật để chuẩn bị cho PCI DSS, kiểm toán viên phát hiện: bất kỳ Pod nào trong cluster — kể cả một service marketing tầm thường — đều có thể mở kết nối trực tiếp đến database thanh toán. Về mặt kỹ thuật thì "chạy được", nhưng về bảo mật đây là lỗ hổng nghiêm trọng.
Team dùng CNI là Calico (may mắn là nó hỗ trợ NetworkPolicy). Họ áp một chính sách default-deny cho toàn namespace payment, sau đó chỉ mở đúng luồng: cho phép payment-api nhận traffic từ Pod có label role: api-gateway, và cho phép payment-api gửi traffic đến payment-db trên đúng cổng 5432. Mọi luồng khác — bao gồm việc service marketing chọc vào — bị chặn ở tầng mạng.
Bài học rút ra: trong hệ thống có dữ liệu nhạy cảm (tài chính, y tế, thông tin cá nhân), "mạng phẳng cho phép hết" là không chấp nhận được. NetworkPolicy với chiến lược default-deny là công cụ bắt buộc để đạt các chuẩn tuân thủ như PCI DSS. Và bài học phụ nhưng quan trọng: hãy kiểm tra CNI của bạn có hỗ trợ NetworkPolicy không TRƯỚC khi tin rằng mình đã bảo mật — nếu họ đang dùng Flannel, mọi policy sẽ vô hiệu mà không báo lỗi.
Ví dụ 3 — Lỗi "gọi service không được" ở một team logistics
Một team làm phần mềm logistics tại Đà Nẵng gặp sự cố: service tracking gọi http://inventory-service liên tục bị timeout, dù cả hai Pod đều đang Running. Họ nghi ngờ ứng dụng, đọc log mãi không ra. Cuối cùng một bạn DevOps kiểm tra bằng kubectl get endpoints inventory-service và phát hiện danh sách Endpoints rỗng — nghĩa là Service không match được Pod nào.
Nguyên nhân: label selector trong Service ghi app: inventory nhưng Deployment lại gắn label app: inventory-svc. Sai một ký tự, Service trỏ vào hư không. IP ảo vẫn tồn tại, DNS vẫn resolve, nhưng không có Pod nào phía sau nên mọi request rơi vào timeout.
Bài học rút ra: khi Service "không hoạt động", việc đầu tiên cần làm là kubectl get endpoints <service>. Nếu Endpoints rỗng, vấn đề gần như chắc chắn nằm ở label selector không khớp hoặc Pod chưa pass readiness probe (Pod chưa "ready" thì không được đưa vào Endpoints). Đây là quy trình chẩn đoán tiết kiệm hàng giờ so với việc đoán mò trong code ứng dụng.
Hướng dẫn từng bước
Ta sẽ đi từ trong ra ngoài: tạo Deployment, phủ Service lên, expose qua Ingress, rồi khóa lại bằng NetworkPolicy.
Bước 1 — Tạo một Deployment để có Pod:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: nginx:1.27
ports: [{ containerPort: 80 }]
Bước 2 — Tạo ClusterIP Service phủ lên các Pod:
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector: { app: web } # PHẢI khớp label của Pod ở Bước 1
ports:
- port: 80 # cổng của Service
targetPort: 80 # cổng của container
Kiểm tra ngay: kubectl get endpoints web-service — phải thấy 3 IP Pod. Nếu rỗng, quay lại soi selector.
Bước 3 — Cài Ingress Controller (nếu chưa có):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
Bước 4 — Tạo Ingress resource định tuyến theo host và path:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.vn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port: { number: 80 }
Bước 5 — Khóa lại bằng NetworkPolicy (default-deny ingress cho namespace):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {} # áp cho MỌI Pod trong namespace
policyTypes: [Ingress]
Sau đó mở đúng luồng cần thiết:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-nginx
spec:
podSelector: { matchLabels: { app: web } }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
Bước 6 — Kiểm chứng: dùng một Pod tạm để test kết nối nội bộ:
kubectl run test --rm -it --image=busybox --restart=Never -- \
wget -qO- http://web-service
Lỗi thường gặp & mẹo
- Quên cài Ingress Controller. Viết Ingress resource nhưng cluster không có controller nào chạy. Ingress hiện lên trong
kubectl get ingressnhưng cột ADDRESS trống. Mẹo: luôn kiểm trakubectl get pods -n ingress-nginx.
- Label selector của Service không khớp Pod. Endpoints rỗng, request timeout. Luôn
kubectl get endpoints <svc>trước tiên khi debug.
- Nhầm
portvàtargetPort.portlà cổng Service lắng nghe,targetPortlà cổng container thật sự. Đảo hai giá trị là request rơi vào hư không.
- Áp NetworkPolicy trên CNI không hỗ trợ (Flannel thuần). Policy được lưu nhưng không có hiệu lực, tạo cảm giác an toàn giả. Kiểm tra CNI trước khi tin vào policy.
- Quên egress khi làm default-deny cả hai chiều. Nếu bạn deny cả egress, Pod sẽ không gọi được cả CoreDNS (cổng 53), dẫn đến DNS resolve fail và mọi thứ vỡ. Nhớ mở egress tới
kube-systemcho DNS.
- Ingress path-based routing mà app không hiểu prefix. Khi route
/apitới một service, backend nhận nguyên/api/.... Dùng annotationrewrite-targetđể cắt prefix nếu cần.
- Mẹo debug DNS: dùng
kubectl run -it --rm debug --image=nicolaka/netshoot— image này có sẵndig,curl,nslookup,tcpdump, cực tiện để soi mạng bên trong cluster.
Bài tập thực hành
- Cơ bản: Tạo một Deployment
hello(3 replicas, imagenginxdemos/hello) và một ClusterIP Service phủ lên nó. Dùng một Pod busybox đểwgetvào Service qua tên DNS. Cố tình sửa sai một ký tự trong selector của Service, quan sátkubectl get endpointschuyển sang rỗng và request timeout — rồi sửa lại.
- Trung bình: Cài ingress-nginx, tạo hai Service (
app-a,app-b) và một Ingress định tuyến/atớiapp-a,/btớiapp-btrên cùng một host. Test bằngcurl -H "Host: myapp.local" http://<ingress-ip>/a. Thêm annotationrewrite-targetvà quan sát khác biệt.
- Nâng cao: Trong một namespace riêng, áp policy
default-deny-ingress, xác nhận rằng một Pod bên ngoài không còn gọi được vào. Sau đó viết policy mở đúng một luồng: chỉ Pod có labelrole: frontendmới gọi được Podrole: backendtrên cổng 8080. Dùng hai Pod test (một có label đúng, một không) để chứng minh policy hoạt động chính xác. (Lưu ý: cần cluster dùng Calico hoặc Cilium.)
Tóm tắt
- Mô hình mạng phẳng: mỗi Pod có IP riêng, mọi Pod routable với nhau, do CNI plugin (Calico, Cilium, Flannel) cấp. IP Pod là phù du — không bao giờ hardcode.
- Service cung cấp IP ảo và tên DNS ổn định trước một nhóm Pod qua label selector, kèm load balancing bởi kube-proxy. Bốn loại: ClusterIP (nội bộ), NodePort, LoadBalancer (ra internet), ExternalName. DNS dạng
service.namespace.svc.cluster.local. - Ingress định tuyến HTTP/HTTPS theo host và path qua một điểm vào duy nhất, xử lý TLS termination, tiết kiệm chi phí so với nhiều LoadBalancer. Cần phân biệt Ingress resource (quy tắc) và Ingress Controller (thực thi — phải tự cài).
- NetworkPolicy là firewall tầng Pod, kiểm soát ingress/egress theo label/namespace/IP, nền tảng của zero-trust. Chỉ hiệu lực khi CNI hỗ trợ; chiến lược production là default-deny rồi mở từng luồng.
- Khi debug: bắt đầu từ
kubectl get endpoints, kiểm tra selector, kiểm tra controller đã cài chưa, và đừng quên mở egress DNS khi làm default-deny.