Service types, ClusterIP, NodePort và endpoints
Mục tiêu bài học
Sau bài này, bạn phải làm được ba việc sau trong khoảng 2 đến 3 phút mỗi việc:
- Chọn đúng
typecho Service khi đề bài yêu cầu (ClusterIP, NodePort, LoadBalancer, ExternalName). - Tạo Service bằng lệnh imperative nhanh, rồi sửa YAML khi cần.
- Nhìn ra ngay vì sao
kubectl get endpointstrả về<none>và sửa được trong 1 phút.
[!NOTE] Đây là bài mở đầu của module Networking. Mình sẽ không nói về DNS và CoreDNS ở đây (bài sau), chỉ tập trung vào Service object và Endpoints object.
Ôn lại nhanh
Ở module Workloads bạn đã tạo Deployment, và Pod do Deployment sinh ra luôn có label do spec.selector.matchLabels quy định. Service không tìm Pod theo tên, nó tìm theo label selector. Đây là gốc rễ của 90% lỗi trong bài này, nên nhớ kỹ: Service là một "bộ lọc label" cộng với một "địa chỉ ảo".
Bốn loại Service, nhìn một bảng là đủ
| Type | Có ClusterIP? | Truy cập từ đâu? | Dùng khi nào |
|---|---|---|---|
ClusterIP (mặc định) |
Có | Chỉ trong cluster | Giao tiếp nội bộ giữa các service |
NodePort |
Có | Từ ngoài qua nodeIP:nodePort |
Cần expose ra ngoài không có cloud LB |
LoadBalancer |
Có | Qua IP của LB do cloud cấp | Cluster trên cloud (GKE, EKS, AKS) |
ExternalName |
Không | Qua CNAME DNS | Trỏ Service tới một DNS name bên ngoài |
Điểm quan trọng: NodePort và LoadBalancer đều là ClusterIP ở bên dưới. Chúng chỉ thêm một lớp phía trước. Nên nếu bạn hiểu ClusterIP thì hiểu luôn hai loại kia.
flowchart LR
Client["👤 Client ngoài cluster"] -->|"nodeIP:30080"| NP["🚪 NodePort<br/>mọi node"]
Client2["📦 Pod trong cluster"] -->|"web-svc:8080"| SVC
NP --> SVC["🎯 Service ClusterIP<br/>10.96.0.10"]
SVC --> EP["📋 Endpoints<br/>danh sách IP:port"]
EP --> P1["📦 Pod 1"]
EP --> P2["📦 Pod 2"]
EP --> P3["📦 Pod 3"]
Ba khái niệm port phải phân biệt được, đây là chỗ đề thi hay gài:
port: port của Service, client gọi vào đây.targetPort: port trong container của Pod, traffic được đẩy tới đây.nodePort: port mở trên mỗi node, mặc định thuộc dải {{c1::30000-32767}}.
Ví dụ mẫu từng bước
Đề bài kiểu này rất hay gặp: "Tạo Deployment tên web dùng image nginx với 3 replicas. Expose nó bằng một Service tên web-svc lắng nghe port 8080 và chuyển tiếp vào port 80 của container."
Bước 1: Tạo workload.
kubectl create deployment web --image=nginx --replicas=3
kubectl get pods -l app=web
Bước 2: Sinh Service bằng kubectl expose. Lệnh này tự lấy selector từ Deployment nên ít lỗi hơn viết YAML tay.
kubectl expose deployment web \
--name=web-svc \
--port=8080 \
--target-port=80
Bước 3: Kiểm tra Service và Endpoints. Kiểm tra Endpoints là bước bắt buộc, đừng bỏ qua.
kubectl get svc web-svc -o wide
kubectl get endpoints web-svc
Kết quả mong đợi:
NAME TYPE CLUSTER-IP PORT(S) AGE
web-svc ClusterIP 10.96.142.31 8080/TCP 15s
NAME ENDPOINTS AGE
web-svc 10.244.1.5:80,10.244.2.4:80,10.244.3.6:80 15s
Đọc kết quả: có 3 địa chỉ IP, đúng bằng 3 replicas, và mỗi địa chỉ đi kèm port 80 (là targetPort, không phải port 8080).
Bước 4: Test từ bên trong cluster. ClusterIP không truy cập được từ máy host của bạn, phải test từ một Pod.
kubectl run tmp --rm -it --image=busybox:1.28 --restart=Never -- \
wget -qO- http://web-svc:8080
Nếu thấy HTML của nginx là xong. Nếu treo, thoát bằng Ctrl+C rồi quay lại mục troubleshoot.
Bước 5: Đổi sang NodePort và test từ ngoài. Không cần xóa Service cũ, chỉ cần sửa type.
kubectl patch svc web-svc -p '{"spec":{"type":"NodePort"}}'
kubectl get svc web-svc
NAME TYPE CLUSTER-IP PORT(S) AGE
web-svc NodePort 10.96.142.31 8080:31234/TCP 3m
Giờ curl <IP-node>:31234 từ máy bạn sẽ ra nginx. Nếu đề yêu cầu nodePort cụ thể, phải khai báo tường minh:
kubectl patch svc web-svc -p '{"spec":{"ports":[{"port":8080,"targetPort":80,"nodePort":30080}]}}'
[!TIP] Luôn dùng
--dry-run=client -o yamlđể sinh YAML rồi sửa, nhanh hơn viết từ đầu và ít sai indent.kubectl expose deployment web --name=web-svc --port=8080 --target-port=80 \ --type=NodePort --dry-run=client -o yaml > svc.yaml
Luyện tập có hướng dẫn
Làm đúng thứ tự, mỗi bài tự bấm giờ 2 đến 3 phút. Đáp án nằm trong phần gập lại, chỉ mở sau khi đã thử.
Bài 1: ClusterIP và đọc endpoints
Tạo Deployment api dùng image nginx:1.25 với 2 replicas. Expose thành Service api-svc port 80, targetPort 80. Sau đó in ra danh sách endpoints.
kubectl create deployment api --image=nginx:1.25 --replicas=2
# viết tiếp ở đây
Xem đáp án
kubectl expose deployment api --name=api-svc --port=80 --target-port=80
kubectl get endpoints api-svc
Phải thấy đúng 2 IP kèm :80. Nếu thấy <none>, chuyển sang bài 3.
Bài 2: NodePort với port chỉ định
Sửa api-svc thành NodePort, giữ nguyên port 80 và ép nodePort là 30081.
Xem đáp án
kubectl patch svc api-svc -p \
'{"spec":{"type":"NodePort","ports":[{"port":80,"targetPort":80,"nodePort":30081}]}}'
kubectl get svc api-svc
Kiểm tra cột PORT(S) phải hiện 80:30081/TCP. Nhớ: dải hợp lệ là 30000 đến 32767, đặt 8081 sẽ bị API server từ chối.
Bài 3: Cố tình làm hỏng selector
Đây là bài quan trọng nhất. Tạo một Service trỏ sai label rồi tự sửa.
kubectl create service clusterip broken-svc --tcp=80:80
Lệnh này tạo Service với selector app=broken-svc, trong khi Pod của bạn có label app=api.
kubectl get endpoints broken-svc
kubectl get pods --show-labels
Xem đáp án
kubectl get endpoints broken-svc trả về <none>. Sửa selector:
kubectl patch svc broken-svc -p '{"spec":{"selector":{"app":"api"}}}'
kubectl get endpoints broken-svc
Endpoints xuất hiện ngay, không cần restart Pod hay Service. Đây là câu thần chú: Service selector sửa được bất cứ lúc nào, không immutable.
Bài 4: Pod Running nhưng endpoints rỗng
Thêm một readinessProbe trỏ vào đường dẫn không tồn tại, quan sát endpoints biến mất, rồi sửa lại.
kubectl patch deployment api -p \
'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","readinessProbe":{"httpGet":{"path":"/healthz","port":80}}}]}}}}'
kubectl get pods -w
Xem đáp án
nginx trả 404 cho /healthz nên probe fail, Pod chuyển sang READY 0/1 nhưng vẫn Running. Endpoints của api-svc sẽ rỗng dần.
kubectl get endpoints api-svc # <none>
kubectl get pods # READY 0/1
Sửa bằng cách đổi path về /:
kubectl patch deployment api -p \
'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","readinessProbe":{"httpGet":{"path":"/","port":80}}}]}}}}'
Bài học: Pod chỉ vào endpoints khi ở trạng thái Ready. Running chưa đủ.
Bài 5: Service không selector, endpoints thủ công
Đề bài hay cho tình huống: "Trỏ một Service tới database nằm ngoài cluster có IP 10.0.0.50 port 3306". Service không có selector, bạn tự tạo Endpoints.
Xem đáp án
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
ports:
- port: 3306
targetPort: 3306
---
apiVersion: v1
kind: Endpoints
metadata:
name: external-db
subsets:
- addresses:
- ip: 10.0.0.50
ports:
- port: 3306
kubectl apply -f external-db.yaml
kubectl get endpoints external-db
Quy tắc: Endpoints phải cùng tên, cùng namespace với Service. Không có selector thì Kubernetes không ghi đè Endpoints của bạn.
Bài 6: Nhận diện type qua YAML
Cho nhanh, đọc ba đoạn YAML sau và gọi tên type, không cần apply.
# A
spec:
type: ExternalName
externalName: db.prod.example.com
# B
spec:
clusterIP: None
selector:
app: api
# C
spec:
type: NodePort
ports:
- port: 80
nodePort: 30080
Xem đáp án
- A:
ExternalName, không có ClusterIP, không có endpoints, chỉ trả về CNAME. - B: Headless Service (
clusterIP: None), DNS trả về IP của từng Pod thay vì một VIP. - C: NodePort, mở port 30080 trên mọi node.
Lỗi thường gặp và cách xử lý
Endpoints rỗng (<none>). Kiểm tra theo đúng thứ tự này, đừng nhảy bước:
kubectl get svc <name> -o yaml | grep -A3 selector # 1. selector là gì
kubectl get pods --show-labels # 2. label Pod có khớp không
kubectl get pods # 3. Pod có READY 1/1 không
kubectl describe svc <name> # 4. xem Events và Endpoints
Ba nguyên nhân theo tần suất: selector sai hoặc sai namespace, Pod chưa Ready (readiness probe fail), và Pod chưa chạy (ImagePullBackOff, Pending, CrashLoopBackOff).
Nhầm port với targetPort. App trong container nghe 8080 nhưng bạn để targetPort: 80 thì kết nối sẽ bị từ chối. Mẹo: kubectl exec vào Pod chạy ss -tlnp hoặc xem containerPort trong Deployment để biết app nghe port nào.
Quên namespace. kubectl expose và kubectl get endpoints mặc định dùng namespace hiện tại. Trong đề thi hầu như luôn có namespace riêng, nên thêm -n <ns> ngay từ lệnh đầu tiên.
Đặt nodePort ngoài dải. Giá trị phải nằm trong {{c2::30000-32767}}. Nếu đề cho 3xxxx cụ thể thì bắt buộc khai báo, còn không thì để Kubernetes tự cấp và đọc lại bằng kubectl get svc.
Cố ping ClusterIP. ClusterIP là địa chỉ ảo do kube-proxy lập trình bằng iptables/IPVS, nó không trả lời ICMP. ping 10.96.x.x fail là bình thường, hãy test bằng TCP: wget, curl, hoặc nc -zv.
Sửa Service rồi tưởng phải restart Pod. Không cần. Ngược lại, nếu sửa label của Pod thì endpoints tự cập nhật trong vài giây.
Nhầm Endpoints với EndpointSlice. Cluster mới dùng EndpointSlice làm nguồn chính, Endpoints chỉ là view tương thích. Khi debug, kubectl get endpoints vẫn đủ dùng, nhưng nếu cần chi tiết thì:
kubectl get endpointslices -l kubernetes.io/service-name=<ten-svc>
[!WARNING] Đừng đặt tên Service bắt đầu bằng số hoặc chứa dấu chấm. Tên Service phải là DNS-1035 label: chữ thường, số, dấu gạch ngang, bắt đầu bằng chữ cái.
Góc chuyên gia
Câu thần chú khi thi: "Service không có endpoints thì 90% là selector hoặc readiness". Hai lệnh này giải quyết gần như mọi ca:
kubectl get svc,endpoints -o wide
kubectl get pods --show-labels
Ưu tiên imperative, sửa bằng patch. Với Service, kubectl expose cộng kubectl patch nhanh hơn hẳn việc gõ YAML từ đầu. Chỉ viết YAML khi cần Service không selector hoặc cần cấu hình đặc biệt.
Đọc đề thật kỹ phần port. Đề CKA rất hay viết: "expose the deployment on port 8080, the container listens on 80, use nodePort 30080". Ba con số đó map chính xác vào port, targetPort, nodePort. Đọc sai một số là mất điểm cả câu.
Test đúng chỗ. ClusterIP test từ trong Pod, NodePort test từ node. Không phân biệt được hai môi trường này là nguyên nhân phổ biến khiến bạn tưởng mình làm sai.
Giới hạn thời gian. Một câu Service không nên quá 3 phút. Nếu quá, ghi chú lại, đánh dấu cờ trong giao diện thi rồi làm tiếp. Quay lại sau.
Đừng quên kubectl get svc sau cùng. Rất nhiều bạn tạo xong, test xong, nhưng không kiểm tra lại PORT(S), nên không phát hiện nodePort bị auto-assign khác yêu cầu.
Tóm tắt và bước tiếp theo
Chốt lại bốn dòng:
ClusterIPlà mặc định, chỉ dùng trong cluster;NodePortvàLoadBalancerđều dựa trên nó.- Ba port phải phân biệt:
portcủa Service,targetPortcủa container,nodePorttrên node. - Endpoints được sinh tự động từ selector khớp label và Pod đang Ready.
- Endpoints rỗng thì đi theo thứ tự: selector, label, trạng thái Pod, rồi mới đến kube-proxy.
Bài sau chúng ta sẽ đi sâu vào phần mà bài này vừa chạm tới: Cluster DNS và troubleshoot service discovery. Cụ thể là vì sao web-svc gọi được mà web-svc.default.svc.cluster.local lại fail, cách đọc /etc/resolv.conf trong Pod, và cách kiểm tra CoreDNS khi cả cluster mất phân giải tên.
[
{ "front": "Service type mặc định khi không khai báo `type` là gì?", "back": "ClusterIP." },
{ "front": "Dải nodePort mặc định là bao nhiêu?", "back": "30000 đến 32767." },
{ "front": "`port` trong Service có nghĩa là gì?", "back": "Port mà Service lắng nghe, client gọi vào đây." },
{ "front": "`targetPort` trong Service có nghĩa là gì?", "back": "Port của container trong Pod mà traffic được chuyển tới." },
{ "front": "Lệnh xem endpoints của một Service?", "back": "kubectl get endpoints <ten-svc> (viết tắt: kubectl get ep)." },
{ "front": "Nguyên nhân phổ biến nhất khiến endpoints rỗng?", "back": "Selector của Service không khớp label của Pod đang Ready." },
{ "front": "Pod đang Running nhưng chưa Ready thì có nằm trong endpoints không?", "back": "Không, chỉ Pod ở trạng thái Ready mới được đưa vào endpoints." },
{ "front": "Service type nào chỉ trả về CNAME và không có ClusterIP?", "back": "ExternalName." },
{ "front": "Service type nào được cloud provider cấp IP ngoài và có nodePort bên dưới?", "back": "LoadBalancer." },
{ "front": "Lệnh imperative tạo Service từ một Deployment?", "back": "kubectl expose deployment <ten> --port=... --target-port=... --type=..." },
{ "front": "Có ping được ClusterIP không?", "back": "Không, ClusterIP là IP ảo xử lý ở tầng TCP/UDP, phải test bằng curl hoặc wget." },
{ "front": "Service không có selector thì lấy endpoints từ đâu?", "back": "Từ Endpoints object tạo thủ công cùng tên và cùng namespace." },
{ "front": "Headless Service khai báo thế nào?", "back": "spec.clusterIP: None." },
{ "front": "EndpointSlice gắn với Service qua label nào?", "back": "kubernetes.io/service-name." }
]
Ôn nhanh bằng cloze:
- Mặc định Service được tạo với type {{c3::ClusterIP}}.
- Pod chỉ được đưa vào endpoints khi vượt qua {{c4::readinessProbe}}.
ExternalNamedùng bản ghi {{c5::CNAME}} để trỏ ra ngoài cluster.