K8s 6 — Service, Ingress & mạng
Vấn đề: Pod đến rồi đi, IP thì đổi liên tục
Ở bài Workloads, chúng ta đã biết Deployment tạo ra một tập Pod và tự thay thế chúng khi cần: Pod crash thì mọc lại, rolling update thì Pod cũ bị xoá và Pod mới ra đời, scale thì thêm bớt bản sao. Mỗi lần như vậy, Pod mới có một IP mới.
Điều này tạo ra một vấn đề nền tảng: nếu service web muốn gọi service api, nó không thể ghi cứng IP của một Pod api nào đó — vì cái IP ấy có thể biến mất bất cứ lúc nào. IP của Pod là ephemeral (nhất thời), không phải là một địa chỉ bạn có thể tin cậy để trỏ tới lâu dài.
Cần một lớp trừu tượng cung cấp một địa chỉ ổn định đứng trước tập Pod hay thay đổi đó, tự động biết Pod nào đang sống và rải tải cho chúng. Đó chính là Service. Và khi cần cho người dùng bên ngoài truy cập qua HTTP/HTTPS, ta có thêm Ingress. Bài này đi từ tầng thấp (mô hình mạng, Pod IP) lên tầng cao (Ingress, Network Policy).
Nếu bạn chưa nắm control plane và node, nên xem trước K8s kiến trúc — đặc biệt là kube-proxy, thành phần đứng sau phần lớn cơ chế Service.
Mô hình mạng Kubernetes: một mạng phẳng
Kubernetes áp đặt một mô hình mạng đơn giản đến mức gần như là một lời hứa với ứng dụng:
- Mỗi Pod có một địa chỉ IP riêng, duy nhất trong cluster.
- Mọi Pod đều nói chuyện được trực tiếp với mọi Pod khác, kể cả khi chúng nằm trên các node vật lý khác nhau — không cần NAT (không phải dịch địa chỉ).
- Container bên trong cùng một Pod chia sẻ chung network namespace: chúng thấy nhau qua
localhostvà cùng dùng một IP.
Đây gọi là flat network (mạng phẳng): từ góc nhìn của ứng dụng, cả cluster giống như một mạng LAN lớn, mọi Pod là một "máy" có IP riêng và ping thẳng tới nhau được. Ứng dụng không cần biết Pod đích nằm trên node nào.
Điểm mấu chốt: Kubernetes bản thân nó không cài đặt mạng này — nó chỉ đặt ra các yêu cầu mà mạng phải thoả mãn. Việc hiện thực hoá được giao cho một CNI plugin (Container Network Interface), ví dụ Calico, Cilium, Flannel. CNI plugin lo việc: cấp IP cho Pod, dựng route để gói tin đi được giữa các node, và (với một số plugin) thực thi Network Policy. Khi bạn dựng cluster, chọn CNI là một trong các quyết định đầu tiên; không có CNI thì Pod ở trạng thái Pending/NotReady vì chưa có mạng.
Vì Pod IP là ephemeral như đã nói, mọi giao tiếp ổn định trong cluster đều nên đi qua Service chứ không trỏ thẳng Pod IP.
Service: địa chỉ ổn định trước một tập Pod
Service là một đối tượng cung cấp:
- Một địa chỉ ổn định (một IP ảo và một tên DNS) không đổi trong suốt vòng đời của Service, bất kể Pod phía sau sinh/tử thế nào.
- Cân bằng tải (load balancing): lưu lượng gửi tới Service được rải cho các Pod thành viên.
Service tìm các Pod thành viên bằng selector — khớp với label trên Pod (nhắc lại từ Workloads: label/selector là "keo dán" của Kubernetes). Ví dụ Service có selector: app=web sẽ tự động gom mọi Pod mang label app=web. Khi Pod mới ra đời với label đó, nó tự vào tập; khi Pod chết, nó tự rời ra. Cơ chế đằng sau là đối tượng Endpoints (hay EndpointSlice trong bản mới), một danh sách IP:port của các Pod đang thực sự sẵn sàng — được cập nhật liên tục bởi control plane.
Một điểm tinh tế đáng nhớ: chỉ Pod Ready (qua readiness probe, xem Workloads) mới được đưa vào tập nhận lưu lượng. Đây là lý do readiness probe quan trọng: Pod chưa sẵn sàng sẽ không nhận request.
Service có nhiều loại (type), phân biệt bởi phạm vi phơi bày.
ClusterIP — nội bộ, mặc định
ClusterIP là loại mặc định. Service được cấp một IP ảo chỉ truy cập được từ bên trong cluster. Đây là loại phổ biến nhất: dùng để các service nói chuyện với nhau (web gọi api, api gọi database). Người dùng bên ngoài không chạm tới được ClusterIP.
Đây là nền tảng của service discovery: bên trong cluster, bạn gọi http://api:8080 (qua DNS, mục sau) mà không cần biết có bao nhiêu Pod api hay IP của chúng là gì.
NodePort — mở một port trên mọi node
NodePort mở một port cố định (mặc định trong khoảng 30000–32767) trên mọi node của cluster. Bất kỳ ai gõ IP-của-node:nodePort đều được chuyển vào Service (và từ đó tới các Pod). NodePort bao hàm luôn ClusterIP bên trong.
NodePort hữu ích cho môi trường on-prem hoặc để test nhanh, nhưng ít khi dùng trực tiếp cho production public: port lạ, phải tự lo IP node và tính sẵn sàng của node.
LoadBalancer — bộ cân bằng tải bên ngoài của cloud
LoadBalancer yêu cầu hạ tầng cloud cấp một bộ cân bằng tải bên ngoài (external load balancer) với một IP công khai, trỏ vào Service. Trên EKS/GKE/AKS, tạo Service loại này sẽ khiến cloud provider dựng một LB thật (ví dụ AWS NLB/ELB). LoadBalancer bao hàm cả NodePort và ClusterIP.
Đây là cách "chính thống" để phơi một service ra Internet ở cấp L4 (TCP/UDP). Nhược điểm: mỗi Service loại này thường tương ứng một LB của cloud, mà LB thì tốn tiền — phơi 20 service qua 20 LoadBalancer là lãng phí. Với lưu lượng HTTP, ta có giải pháp gọn hơn: Ingress (mục sau).
Headless Service — cho StatefulSet
Đôi khi bạn không muốn một IP ảo và load balancing, mà muốn địa chỉ của từng Pod cụ thể. Đặt clusterIP: None tạo ra một headless Service: DNS sẽ trả về danh sách IP của các Pod thay vì một IP ảo duy nhất.
Điều này quan trọng cho StatefulSet (xem Workloads): mỗi Pod stateful có danh tính ổn định và địa chỉ DNS riêng, ví dụ db-0.db.default.svc.cluster.local, db-1.db.... Các hệ như cơ sở dữ liệu phân tán cần biết từng thành viên cụ thể (để bầu leader, replicate) chứ không thể chấp nhận một IP ảo rải tải ngẫu nhiên.
kube-proxy: iptables vs IPVS
Cái "IP ảo" của ClusterIP không ứng với một tiến trình proxy thật đang lắng nghe. Thay vào đó, kube-proxy chạy trên mỗi node lập trình các quy tắc chuyển tiếp gói ở tầng kernel để khi có gói tin gửi tới ClusterIP, kernel DNAT (viết lại địa chỉ đích) sang IP của một Pod thành viên và chọn ngẫu nhiên trong số các Pod Ready.
kube-proxy có vài chế độ:
- iptables (mặc định phổ biến): dùng chuỗi quy tắc iptables. Đơn giản, ổn định, nhưng khi số Service/Endpoint rất lớn thì số quy tắc phình to, ảnh hưởng hiệu năng cập nhật.
- IPVS: dùng module cân bằng tải trong kernel Linux, xử lý số lượng lớn service/endpoint hiệu quả hơn và có thêm thuật toán rải tải (round-robin, least-conn...).
Điểm cần nhớ: load balancing của Service là ở tầng kết nối (L4) và do kernel làm, không có một tiến trình proxy tập trung là điểm nghẽn.
DNS nội bộ: gọi nhau bằng tên
Cluster có một dịch vụ DNS nội bộ, ngày nay là CoreDNS (chạy như Pod trong namespace kube-system). Mỗi Service tự động được cấp một bản ghi DNS. Nhờ đó, thay vì nhớ IP ảo, bạn gọi service bằng tên.
Tên DNS đầy đủ (FQDN) của một Service có dạng:
<service>.<namespace>.svc.cluster.local
Ví dụ Service api trong namespace default có tên đầy đủ api.default.svc.cluster.local. Nhưng bạn hiếm khi gõ hết:
- Trong cùng namespace, chỉ cần
api. - Khác namespace, dùng
api.<namespace>(ví dụapi.payments).
Đây chính là cơ chế service discovery hoàn chỉnh: code trong Pod web chỉ cần biết hostname api và cổng, phần còn lại (phân giải tên → IP ảo → DNAT sang Pod sống) là việc của cluster. Không cần service registry riêng, không cần config IP.
Ingress: một cửa ngõ HTTP cho nhiều Service
Nếu mỗi service public đều dùng LoadBalancer thì tốn kém và không định tuyến được theo host/path. Ingress giải bài toán này ở tầng HTTP/HTTPS (L7): một cửa ngõ duy nhất, định tuyến theo host (tên miền) và path (đường dẫn) vào nhiều Service ClusterIP khác nhau, và xử lý TLS (HTTPS) tập trung.
Ví dụ định tuyến bằng Ingress:
shop.example.com/→ Servicewebshop.example.com/api→ Serviceapiadmin.example.com/→ Serviceadmin
Tất cả qua một địa chỉ IP công khai, một chứng chỉ TLS quản lý ở một chỗ.
Quan trọng: đối tượng Ingress chỉ là luật định tuyến — nó không tự làm gì cả. Phải có một Ingress Controller thực sự thực thi các luật đó. Ingress Controller là một reverse proxy chạy trong cluster (thường được phơi ra ngoài qua một Service LoadBalancer duy nhất) đọc các đối tượng Ingress và cấu hình chính nó. Phổ biến nhất là ingress-nginx; ngoài ra có Traefik, HAProxy, và các controller của cloud. Không cài controller thì tạo Ingress vô tác dụng.
Gateway API — thế hệ mới
Ingress đã tồn tại lâu và có giới hạn (cấu hình nâng cao thường phải nhét vào annotation riêng của từng controller, kém chuẩn hoá). Kubernetes đang đẩy mạnh Gateway API — một bộ API mới, biểu đạt hơn, tách vai trò rõ ràng (người quản trị hạ tầng định nghĩa Gateway, đội ứng dụng định nghĩa Route). Với dự án mới, đây là hướng đáng cân nhắc, dù Ingress vẫn còn được dùng rộng rãi.
Sơ đồ: lưu lượng đi từ client tới Pod
Chú ý hai luồng: luồng dữ liệu (client → LB → Ingress Controller → Service → Pod) và luồng phân giải tên (Ingress Controller hỏi CoreDNS để biết ClusterIP của Service). Bên trong cluster, một Pod khác muốn gọi web cũng đi qua đúng Service ClusterIP + DNS này — không cần đụng tới Ingress.
ConfigMap & Secret: cấu hình đi kèm (điểm qua)
Service/Ingress lo kết nối, nhưng ứng dụng còn cần cấu hình và bí mật (chuỗi kết nối DB, khoá API, cờ tính năng). Kubernetes tách chúng khỏi image container qua:
- ConfigMap: dữ liệu cấu hình dạng key-value không nhạy cảm, tiêm vào Pod dưới dạng biến môi trường hoặc file.
- Secret: tương tự nhưng cho dữ liệu nhạy cảm (mật khẩu, token, chứng chỉ TLS). Lưu ý: Secret mặc định chỉ được mã hoá base64 (không phải mã hoá thật) — cần bật encryption at rest và RBAC để thực sự an toàn.
Chẳng hạn chứng chỉ TLS mà Ingress dùng thường được lưu trong một Secret. Chi tiết về ConfigMap/Secret, cách mount và cập nhật, để dành cho bài Storage & cấu hình.
Network Policy: mặc định là mở, hãy siết lại
Đây là điểm về bảo mật mà nhiều người bỏ sót. Theo mặc định trong Kubernetes, mọi Pod nói chuyện được với mọi Pod (đúng như mô hình mạng phẳng). Nghĩa là nếu một Pod frontend bị xâm nhập, kẻ tấn công có thể thử kết nối thẳng tới Pod database, tới mọi service nội bộ — không có tường ngăn.
NetworkPolicy là một firewall ở cấp Pod: bạn khai báo ai được gọi ai, theo label và namespace, cho cả lưu lượng vào (ingress) lẫn ra (egress). Ví dụ điển hình: "chỉ Pod có label app=api mới được kết nối tới Pod database ở cổng 5432; mọi nguồn khác bị chặn".
Hai điều phải nhớ:
- Network Policy cần CNI plugin hỗ trợ (Calico, Cilium... có; một số plugin tối giản thì không). Nếu CNI không thực thi policy, bạn khai báo NetworkPolicy vẫn không có tác dụng — một cái bẫy nguy hiểm.
- NetworkPolicy có tính cộng dồn (additive) và một khi có policy chọn tới một Pod, mọi thứ không được cho phép rõ ràng sẽ bị chặn cho chiều đó. Mẫu thực hành tốt là bắt đầu bằng một policy default-deny rồi mở dần các luồng cần thiết.
Đây là yêu cầu gần như bắt buộc trong môi trường đa tenant (nhiều đội/khách hàng chung một cluster) hoặc bất kỳ hệ thống nào coi trọng "giả định breach" (least privilege ở tầng mạng).
Ví dụ YAML (minh hoạ)
Lưu ý môi trường: sandbox của loạt bài này chỉ có PostgreSQL, không có cluster Kubernetes, nên các YAML/lệnh dưới đây là minh hoạ — cần cluster thật (minikube, kind, hay managed) để chạy.
Một Service ClusterIP cho web, và Ingress phơi nó ra ngoài (minh hoạ):
# (minh hoạ) — Service ClusterIP: địa chỉ ổn định cho tập Pod app=web
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # mặc định; nội bộ cluster
selector:
app: web # gom mọi Pod có label này
ports:
- port: 80 # cổng của Service
targetPort: 8080 # cổng container trong Pod
---
# (minh hoạ) — Ingress: định tuyến HTTP theo host/path, kết thúc TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
kubernetes.io/ingress.class: nginx
spec:
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls # chứng chỉ TLS nằm trong Secret này
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web # trỏ vào Service ClusterIP ở trên
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
Một NetworkPolicy siết chặt truy cập tới database (minh hoạ):
# (minh hoạ) — chỉ Pod app=api mới được kết nối tới DB, cổng 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api-only
spec:
podSelector:
matchLabels:
app: db
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
Vài lệnh khảo sát mạng thường dùng (minh hoạ):
# (minh hoạ)
kubectl get svc # liệt kê Service, xem CLUSTER-IP, PORT
kubectl get endpoints web # xem IP:port các Pod đứng sau Service
kubectl get ingress # xem host/path và địa chỉ Ingress
kubectl describe ingress shop # xem chi tiết luật định tuyến
# Thử phân giải DNS và gọi Service từ bên trong cluster
kubectl run tmp --rm -it --image=busybox -- sh
# nslookup web.default.svc.cluster.local
# wget -qO- http://web/
Use case thực tế
Phơi một web app ra ngoài, gọi backend nội bộ. Hình dung một shop có 3 tầng: web (giao diện), api (nghiệp vụ), db (PostgreSQL).
dblà StatefulSet đứng sau một headless Service — chỉ cần dùng nội bộ, có địa chỉ Pod ổn định đểapikết nối tin cậy.apilà Deployment đứng sau một Service ClusterIP tênapi.webgọi backend chỉ bằnghttp://api/..., không quan tâm có bao nhiêu Podapihay IP của chúng — DNS + ClusterIP lo hết.weblà Deployment đứng sau Service ClusterIPweb. Người dùng Internet đến qua một Ingress:shop.example.com/→web,shop.example.com/api→api, TLS kết thúc tại Ingress Controller. Toàn bộ đi qua một LoadBalancer của cloud (đứng trước Ingress Controller), tiết kiệm so với mở LoadBalancer cho từng service.- Về bảo mật: một NetworkPolicy chỉ cho phép
apikết nốidb(cổng 5432), và chỉ cho Ingress Controller gọiweb/api. Nếuwebbị chiếm quyền, kẻ tấn công vẫn không kết nối thẳng được tớidb.
Kết quả: bên ngoài thấy một tên miền HTTPS gọn gàng; bên trong là các Service ClusterIP nói chuyện qua DNS; và các luồng không mong muốn bị NetworkPolicy chặn.
Ghi nhớ
- Mạng K8s là phẳng: mỗi Pod một IP, mọi Pod nói chuyện trực tiếp không NAT. Kubernetes chỉ ra yêu cầu; CNI plugin (Calico/Cilium/Flannel) mới cài đặt mạng. Pod IP ephemeral → đừng trỏ thẳng, hãy qua Service.
- Service = địa chỉ ổn định + load balancing cho tập Pod chọn theo selector/label; chỉ Pod Ready mới nhận lưu lượng (nhờ Endpoints/EndpointSlice).
- Bốn dạng dùng nhiều: ClusterIP (nội bộ, mặc định) · NodePort (mở port trên mọi node) · LoadBalancer (LB ngoài của cloud, L4) · headless (
clusterIP: None, trả IP từng Pod — cho StatefulSet). - kube-proxy lập trình iptables/IPVS trong kernel để DNAT về Pod; không có proxy tập trung làm điểm nghẽn.
- DNS nội bộ (CoreDNS): gọi service bằng tên —
service(cùng namespace) hoặcservice.namespace.svc.cluster.local. Đây là service discovery đầy đủ. - Ingress định tuyến HTTP/HTTPS theo host/path vào nhiều Service ClusterIP, xử lý TLS — nhưng chỉ là luật, cần Ingress Controller (ingress-nginx...) mới có tác dụng. Gateway API là hướng mới.
- ConfigMap/Secret tách cấu hình khỏi image (Secret chỉ base64, không phải mã hoá thật) — chi tiết ở bài sau.
- Network Policy: mặc định mọi thứ mở → dùng policy (cần CNI hỗ trợ) để giới hạn ai gọi ai; nên bắt đầu bằng default-deny. Quan trọng cho bảo mật và đa tenant.
Tiếp theo: Storage & cấu hình (Volume, PV/PVC, ConfigMap/Secret chi tiết), rồi Vận hành production.
Nguồn tham khảo: tài liệu chính thức kubernetes.io — Service, Ingress, DNS for Services và Network Policies.
Bài viết liên quan
Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.