Buổi 24 – Kubernetes Services
Tuần 12 · Giai đoạn 4 – Kubernetes Engineering · Slide: Slide buổi 24
Tóm tắt: Dùng Service (ClusterIP, NodePort, LoadBalancer) và DNS nội bộ của Kubernetes để nối Frontend với Backend, rồi chẩn đoán Service có Endpoints rỗng.
Mục tiêu học tập
- Giải thích vì sao Pod cần Service và Service chọn Pod bằng label selector như thế nào.
- Phân biệt được ba loại Service ClusterIP, NodePort, LoadBalancer và nói khi nào dùng loại nào trong cluster kind.
- Chạy được lệnh
kubectl get svc,kubectl get endpoints,kubectl describe svcvà đọc đúng kết quả. - Kiểm thử được service discovery bằng tên DNS ngắn và tên đầy đủ từ một Pod debug.
- Phân biệt
port,targetPort,containerPortvànodePort. - Chẩn đoán được Service có Endpoints rỗng do selector sai và sửa lại.
Điều kiện cần
- Đã hoàn thành buổi 22 (Pods và Deployments) và buổi 23 (kubectl cơ bản): biết
kubectl apply,get,describe,logs,exec. - Đã có cluster kind đang chạy và các image
club-api:1.0.0,club-web:1.0.0đã được nạp vào kind (kind load docker-image). - Namespace
club, ConfigMapclub-config, Secretclub-secretsvà Postgres (k8s/postgres.yaml) đã được tạo từ các buổi trước. - Làm việc trong filesystem của WSL (ví dụ
~/devops-labs), không dùng/mnt/c.
Kiểm tra nhanh:
kubectl cluster-info --context kind-kind
kubectl get pods -n clubKết quả mong đợi (rút gọn): control plane đang chạy; Pod postgres-0, club-api-... và club-web-... ở trạng thái Running. Nếu thiếu, apply lại theo thứ tự: namespace.yaml, configmap.yaml, Secret (từ secret.example.yaml với password của bạn), postgres.yaml, backend.yaml, frontend.yaml trong starter-project/k8s/.
Vấn đề thực tế
CLB deploy API với 2 replica. Mỗi lần Pod bị xóa, Deployment tạo Pod mới và Pod mới có IP khác. Nếu Frontend hard-code IP của Pod backend, chỉ cần một lần rolling update là trang thành viên báo lỗi 502. Còn nếu có hai replica, Frontend phải biết gửi request cho Pod nào?
Service giải quyết cả hai vấn đề: nó cho một tên DNS và một IP ảo ổn định, đồng thời tự cân bằng tải giữa các Pod đang sẵn sàng. Trong dự án CLB, Nginx của frontend chỉ cần proxy_pass http://backend:3000; vì có một Service tên backend. Hiểu Service là hiểu vì sao cấu hình đó chạy được ở Kubernetes mà không cần sửa gì so với Docker Compose.
Khái niệm cốt lõi
Service là gì
Service là một object Kubernetes cung cấp địa chỉ mạng ổn định cho một nhóm Pod. Hai thành phần chính:
- selector: tập label dùng để chọn Pod (ví dụ
app: club-api). - ports: cổng Service lắng nghe (
port) và cổng trên Pod nhận traffic (targetPort).
Service không chạy process nào. Khi bạn tạo Service, control plane tạo một ClusterIP ảo, rồi theo dõi các Pod khớp selector và đang Ready để duy trì danh sách địa chỉ (Endpoints/EndpointSlice). kube-proxy trên mỗi node dùng danh sách này để chuyển traffic đến ClusterIP thành traffic tới một Pod cụ thể.
Các loại Service
| Loại | Ai truy cập được | Dùng khi | Trong kind |
|---|---|---|---|
| ClusterIP (mặc định) | Chỉ trong cluster | Backend, database, service nội bộ | Dùng được, truy cập từ máy bằng kubectl port-forward |
| NodePort | Trong cluster và qua IP của node ở cổng 30000-32767 | Thử nghiệm nhanh, nền tảng cho LoadBalancer | Cổng nằm trên container node của kind, không tự mở ra máy host (cần extraPortMappings khi tạo cluster) |
| LoadBalancer | Qua load balancer bên ngoài do cloud cấp | Cluster cloud (EKS, AKS, GKE) | EXTERNAL-IP ở trạng thái pending vì không có cloud provider |
Với bài này: backend dùng ClusterIP (không cần ai ngoài cluster gọi trực tiếp), frontend sẽ được xem qua port-forward, và ở buổi 27 bạn dùng Ingress để vào bằng club.localhost.
Bốn loại cổng dễ nhầm
| Tên | Nằm ở đâu | Ý nghĩa |
|---|---|---|
containerPort | Pod spec | Khai báo cổng container lắng nghe (chủ yếu để tài liệu hóa) |
targetPort | Service | Cổng trên Pod mà Service chuyển traffic tới |
port | Service | Cổng của Service (client gọi backend:3000) |
nodePort | Service NodePort | Cổng mở trên mọi node |
Nếu bỏ targetPort, Kubernetes dùng giá trị bằng port. Nếu targetPort không khớp cổng process thật sự lắng nghe, Service vẫn có Endpoints nhưng kết nối bị từ chối.
Service discovery và DNS
CoreDNS trong cluster tạo bản ghi cho mỗi Service theo dạng ten-service.namespace.svc.cluster.local. Từ Pod, bạn có thể gọi:
| Cách gọi | Hoạt động khi |
|---|---|
backend | Pod cùng namespace với Service |
backend.club | Pod ở namespace khác |
backend.club.svc.cluster.local | Luôn đúng (tên đầy đủ, FQDN) |
Tên ngắn hoạt động nhờ search domain trong /etc/resolv.conf của Pod. Đây là lý do proxy_pass http://backend:3000 trong starter-project/frontend/nginx.conf chạy được khi frontend và backend cùng namespace club.
Endpoints
Endpoints là danh sách IP:port của các Pod đang Ready khớp selector. Kubernetes hiện dùng EndpointSlice làm cơ chế chính, nhưng kubectl get endpoints vẫn dùng được và đủ cho bài này. Từ Kubernetes 1.33 lệnh này in thêm cảnh báo v1 Endpoints is deprecated (không phải lỗi); xem tương đương bằng kubectl get endpointslices -l kubernetes.io/service-name=backend. Quy tắc quan trọng: Pod chưa Ready (readiness probe fail) bị loại khỏi Endpoints. Endpoints rỗng nghĩa là Service không có nơi nào để gửi traffic.
Kiến trúc và cách hoạt động
Từng bước:
- Trình duyệt gọi
http://localhost:8081(port-forward tới Serviceclub-web) và nhận trang HTML từ Nginx. - Trang gọi
fetch("/api/members"). Nginx khớplocation /api/vàproxy_pass http://backend:3000. - Nginx hỏi CoreDNS tên
backend. Nhờ search domain, tên được mở rộng thànhbackend.club.svc.cluster.localvà trả về ClusterIP. kube-proxychuyển traffic tới ClusterIP thành traffic tới IP của một trong hai Podclub-apicó trong Endpoints.- API truy vấn database qua
DB_HOST=postgres(từ ConfigMapclub-config), tức là lại đi qua một Service nữa.
Demo của mentor
Mục tiêu: cho lớp thấy Service là ranh giới ổn định phía trước các Pod thay đổi liên tục. Các manifest của dự án có sẵn trong starter-project/k8s/; ví dụ rút gọn nằm ở examples/kubernetes/03-service.yaml.
Xem Service hiện có.
bashkubectl get svc -n clubKết quả mong đợi (rút gọn):
textNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE backend ClusterIP 10.96.x.x <none> 3000/TCP 20m club-web ClusterIP 10.96.x.x <none> 80/TCP 20m postgres ClusterIP 10.96.x.x <none> 5432/TCP 25mHỏi lớp: vì sao database cũng cần Service dù chỉ có một Pod
postgres-0? (Để API gọi tênpostgresthay vì IP Pod;DB_HOST=postgrestrong ConfigMap.) Câu hỏi mở rộng: StatefulSet khai báoserviceName: postgres, vì sao thường dùng headless Service (clusterIP: None) cho loại workload này?Xem Endpoints khớp với Pod.
bashkubectl get endpoints backend -n club kubectl get pods -n club -l app=club-api -o wideKết quả mong đợi: cột
ENDPOINTScủabackendcó hai địa chỉ dạng10.244.x.x:3000, trùng IP của hai Podclub-api.Xóa một Pod rồi quan sát.
bashkubectl delete pod -n club -l app=club-api --wait=false kubectl get endpoints backend -n club -wKết quả mong đợi: danh sách địa chỉ thay đổi khi Pod mới Ready; ClusterIP của Service không đổi. Nhấn Ctrl+C để thoát.
Hỏi lớp: nếu frontend gọi thẳng IP Pod thì chuyện gì xảy ra? Pod mới có Ready ngay không, và vì sao readiness probe quan trọng với Endpoints?
Cho lớp thấy DNS từ một Pod tạm.
bashkubectl run dns-demo -n club --rm -it --restart=Never --image=busybox:1.36 -- nslookup backendKết quả mong đợi:
Name: backend.club.svc.cluster.localvà mộtAddresslà ClusterIP của Service.
Hands-on lab
Thời gian dự kiến: 60 phút (Chuẩn bị 5', Phần 1-4 khoảng 40', L1/L2 và dọn dẹp 15').
Chuẩn bị
Tạo thư mục nộp bài và đặt biến namespace:
mkdir -p ~/devops-labs/lab-24/{source,screenshots,evidence}
cd ~/devops-labs/lab-24
kubectl config set-context --current --namespace=club
kubectl get podsKết quả mong đợi: các Pod postgres-0, hai Pod club-api-..., một Pod club-web-... đều Running và READY 1/1.
Vì sao đặt namespace mặc định: tránh gõ -n club ở mọi lệnh. Nhớ khôi phục sau lab nếu bạn dùng namespace khác (kubectl config set-context --current --namespace=default).
Từ đây ghi lại output các lệnh quan trọng vào evidence/command-output.txt (ví dụ kubectl get svc | tee -a evidence/command-output.txt).
Phần 1 – Đọc Service của backend (10 phút)
Xem manifest Service đang chạy:
bashkubectl get svc backend -o yamlKết quả mong đợi (rút gọn):
yamlspec: clusterIP: 10.96.x.x ports: - port: 3000 protocol: TCP targetPort: 3000 selector: app: club-api type: ClusterIPVì sao:
selectorlà liên kết duy nhất giữa Service và Pod. Không có tham chiếu tới tên Deployment.So sánh selector với label của Pod:
bashkubectl get pods --show-labels kubectl describe svc backendKết quả mong đợi: Pod
club-api-...có labelapp=club-api; phầnEndpoints:trongdescribeliệt kê hai địa chỉ10.244.x.x:3000.Ghi lại: Service
backendchọn những Pod nào, vàport,targetPortbằng bao nhiêu.
Phần 2 – Gọi Service bằng DNS từ Pod debug (10 phút)
Chạy một Pod debug tạm thời (tự xóa khi thoát):
bashkubectl run debug --rm -it --restart=Never --image=busybox:1.36 -- shKết quả mong đợi: dấu nhắc
/ #. Lệnh trong các bước sau chạy bên trong shell này.Phân giải tên bằng ba cách:
bashnslookup backend nslookup backend.club nslookup backend.club.svc.cluster.localKết quả mong đợi: cả ba cùng trả về một địa chỉ ClusterIP. Vì sao: Pod debug cùng namespace
clubnên tên ngắn được mở rộng nhờ search domain.Xem cấu hình DNS của Pod:
bashcat /etc/resolv.confKết quả mong đợi (rút gọn):
textsearch club.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10Gọi API qua Service:
bashwget -qO- http://backend:3000/health wget -qO- http://backend:3000/api/membersKết quả mong đợi:
{"status":"healthy","service":"club-api"}và một mảng JSON các thành viên (rỗng[]nếu bảng chưa có dữ liệu mẫu).Thoát shell bằng
exit. API không trả tên Pod nên việc quan sát cân bằng tải được làm bằng log ở Phần 4.
Phần 3 – Nối Frontend với Backend và xem từ máy (10 phút)
Xem Service frontend và Endpoints:
bashkubectl get svc club-web kubectl get endpoints club-webKết quả mong đợi: ClusterIP cổng
80/TCP; một địa chỉ endpoint cổng 80.Kiểm tra Nginx trong Pod frontend phân giải được
backend:bashkubectl exec deploy/club-web -- wget -qO- http://backend:3000/healthKết quả mong đợi:
{"status":"healthy","service":"club-api"}. Vì sao: đây chính là đường màproxy_pass http://backend:3000đi.Mở cổng từ máy tới Service frontend (để terminal này chạy):
bashkubectl port-forward svc/club-web 8081:80Kết quả mong đợi:
Forwarding from 127.0.0.1:8081 -> 80.Ở terminal khác:
bashcurl -s http://localhost:8081/api/members curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081/Kết quả mong đợi: JSON danh sách thành viên đi qua Nginx rồi tới backend; mã HTTP
200. Dùng cổng 8081 để không đụng 8080 (Compose) trên máy. Nhấn Ctrl+C ở terminal đầu để dừng.
Phần 4 – NodePort và LoadBalancer (10 phút)
Tạo Service NodePort thử nghiệm cho frontend (không sửa Service gốc):
bashkubectl expose deployment club-web --name=club-web-np --type=NodePort --port=80 --target-port=80 kubectl get svc club-web-npKết quả mong đợi:
TYPElàNodePort, cộtPORT(S)dạng80:3xxxx/TCP.Tạo Service LoadBalancer thử nghiệm:
bashkubectl expose deployment club-web --name=club-web-lb --type=LoadBalancer --port=80 --target-port=80 kubectl get svc club-web-lbKết quả mong đợi:
EXTERNAL-IPlà<pending>mãi mãi. Vì sao: kind không có cloud load balancer. (Có thể cài cloud-provider-kind hoặc MetalLB, nằm ngoài phạm vi buổi này.)Quan sát log của hai Pod API khi gọi lặp lại qua Service:
bashfor i in 1 2 3 4 5 6; do kubectl exec deploy/club-web -- wget -qO- http://backend:3000/health > /dev/null; done kubectl logs -l app=club-api --prefix --tail=5Kết quả mong đợi: log có tiền tố tên Pod. Starter API không log từng request nên có thể ít dòng; nếu muốn đếm request, đây là gợi ý cho L2.
Dọn hai Service thử nghiệm:
bashkubectl delete svc club-web-np club-web-lb
L1 – Foundation
- Giải thích được Service
backendchọn Pod nào (chỉ ra selector và label). - Phân giải thành công
backend,backend.clubvàbackend.club.svc.cluster.localtừ Pod debug. - Gọi được
/healthvà/api/membersqua tênbackend. - Xem được frontend qua
port-forwardvà/api/memberstrả JSON. - Tạo được Service NodePort và LoadBalancer thử nghiệm, nêu được vì sao LoadBalancer
pendingtrong kind. - Đã lưu output các lệnh chính vào
evidence/command-output.txt.
L2 – Engineering
Thử thách 1 – Service viết tay. Tự viết manifest source/backend-svc-lab.yaml tạo Service tên backend-lab cho backend, port: 8080, targetPort: 3000, loại ClusterIP. Không copy từ backend.yaml.
Tiêu chí hoàn thành: kubectl get endpoints backend-lab có hai địa chỉ cổng 3000, và wget -qO- http://backend-lab:8080/health từ Pod debug trả về JSON healthy. Giải thích trong report.md vì sao client gọi 8080 nhưng Pod nghe 3000.
Gợi ý
Cần đúng ba thứ: metadata.name, selector khớp label app: club-api của Pod, và cặp port/targetPort đúng. Dùng kubectl apply --dry-run=client -f source/backend-svc-lab.yaml để kiểm tra cú pháp trước.
Lời giải
apiVersion: v1
kind: Service
metadata:
name: backend-lab
namespace: club
spec:
type: ClusterIP
selector:
app: club-api
ports:
- port: 8080
targetPort: 3000kubectl apply -f source/backend-svc-lab.yaml
kubectl get endpoints backend-labThử thách 2 – Truy cập chéo namespace. Tạo namespace lab24, chạy Pod debug ở đó và gọi được backend ở namespace club.
Tiêu chí hoàn thành: giải thích tên nào chạy được (backend hay backend.club) và vì sao.
Gợi ý
Search domain của Pod chỉ chứa namespace của chính nó. Tên ngắn backend sẽ tìm trong lab24.svc.cluster.local và không thấy.
Lời giải
kubectl create namespace lab24
kubectl run debug -n lab24 --rm -it --restart=Never --image=busybox:1.36 -- sh
# trong shell:
wget -qO- http://backend:3000/health # lỗi: bad address
wget -qO- http://backend.club:3000/health # chạy được
exit
kubectl delete namespace lab24Thử thách 3 – Endpoints rỗng. Làm theo phần "Phá lỗi và debug" bên dưới, ghi lại từng bước điều tra vào report.md.
Phá lỗi và debug
Tình huống: Service không có Endpoints
Cách phá: sửa selector của Service backend thành giá trị sai (mô phỏng gõ nhầm label).
kubectl patch svc backend -p '{"spec":{"selector":{"app":"club-apii"}}}'Triệu chứng:
kubectl get endpoints backend
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081/api/membersKết quả: cột ENDPOINTS là <none>. Nếu port-forward tới club-web đang chạy (Phần 3), curl trả 502 (Nginx không kết nối được upstream) hoặc treo rồi lỗi. Các Pod vẫn Running và Deployment vẫn khỏe, nên kubectl get pods không cho thấy gì bất thường.
Hướng điều tra, đi từ ngoài vào trong:
- Lỗi nằm ở tầng nào? Gọi trực tiếp từ Pod debug:
wget -qO- http://backend:3000/health. Nếu lỗi kết nối nhưng DNS vẫn phân giải được (nslookup backendra IP) thì Service tồn tại, vấn đề nằm sau Service. kubectl get endpoints backend: thấy<none>. Service không có Pod nào để chuyển traffic tới.kubectl describe svc backend: đọcSelector:(ví dụapp=club-apii).kubectl get pods --show-labels: so label thật (app=club-api). Hai bên lệch nhau.- Kiểm tra bằng
kubectl get pods -l app=club-apii: trảNo resources found, xác nhận selector không khớp Pod nào. - Loại trừ nguyên nhân khác: nếu selector đúng nhưng Endpoints vẫn rỗng, kiểm tra Pod có
READY 1/1không (readiness probe fail cũng làm Pod bị loại khỏi Endpoints).
Nguyên nhân gốc: selector của Service không khớp label của Pod, nên không Pod nào được chọn.
Sửa:
kubectl patch svc backend -p '{"spec":{"selector":{"app":"club-api"}}}'
kubectl get endpoints backendKết quả mong đợi: hai địa chỉ 10.244.x.x:3000 xuất hiện lại và curl trả 200.
Phòng tránh: luôn chạy kubectl get endpoints sau khi apply Service; giữ label và selector trong cùng một repo/manifest; dùng tên label thống nhất (app). Với các lỗi liên quan, xem thêm examples/troubleshooting/ (scenario-03 API không kết nối được DB và scenario-10 readiness probe fail).
Tình huống phụ: targetPort sai
Đổi targetPort thành 3001 rồi gọi wget -qO- http://backend:3000/health từ Pod debug. Endpoints vẫn có địa chỉ (cổng 3001) nhưng kết nối bị từ chối (Connection refused). Dấu hiệu phân biệt với selector sai: Endpoints không rỗng nhưng cổng sai. Khôi phục bằng kubectl patch svc backend -p '{"spec":{"ports":[{"port":3000,"targetPort":3000}]}}'.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
kubectl get endpoints cho <none> | Selector Service không khớp label Pod, hoặc Pod chưa Ready | So describe svc với --show-labels; kiểm tra readiness probe |
Có Endpoints nhưng Connection refused | targetPort khác cổng process lắng nghe | Sửa targetPort cho đúng cổng ứng dụng (3000 với API) |
bad address 'backend' / could not resolve host | Pod ở namespace khác, hoặc sai tên Service | Dùng backend.club hoặc FQDN; kiểm tra kubectl get svc -n club |
LoadBalancer EXTERNAL-IP là pending | kind không có cloud provider | Dùng port-forward hoặc Ingress (buổi 27) |
Mở localhost:NodePort không được | NodePort nằm trên container node của kind, chưa map ra host | Dùng port-forward, hoặc tạo cluster với extraPortMappings |
port-forward báo address already in use | Cổng local đã bị dùng (ví dụ 8080 của Compose) | Đổi cổng local, ví dụ 8082:80 |
Nginx trả 502 khi gọi /api/ | Service backend không có Endpoints, hoặc API đang CrashLoopBackOff | get endpoints backend, logs của Pod API |
Tự kiểm tra
Vì sao không nên dùng IP của Pod để các service gọi nhau?
Đáp án
Pod là tạm thời; mỗi lần bị tạo lại nó có IP mới. Service cung cấp ClusterIP và tên DNS ổn định và tự cập nhật danh sách Pod phía sau.
Service liên kết với Pod bằng cơ chế nào?
Đáp án
Label selector. Service chọn mọi Pod cùng namespace có label khớp và đang Ready; không liên quan tới tên Deployment.
port,targetPortvàcontainerPortkhác nhau thế nào?Đáp án
portlà cổng của Service mà client gọi;targetPortlà cổng trên Pod nhận traffic;containerPortlà khai báo cổng container lắng nghe trong Pod spec (mang tính tài liệu hóa).Pod ở namespace
lab24muốn gọi Servicebackendởclub. Dùng tên nào?Đáp án
backend.club(hoặcbackend.club.svc.cluster.local). Tên ngắnbackendchỉ tìm trong namespace của Pod gọi.Vì sao Service LoadBalancer trong kind có
EXTERNAL-IPlàpending?Đáp án
Không có cloud controller nào cấp load balancer bên ngoài. Trong kind dùng
port-forward, Ingress hoặc công cụ bổ sung như cloud-provider-kind/MetalLB.Tình huống: Frontend trả 502 khi gọi
/api/members, mọi Pod đềuRunning. Bạn kiểm tra gì đầu tiên?Đáp án
kubectl get endpoints backend. Nếu rỗng, so selector của Service với label của Pod, rồi kiểm tra Pod có Ready không. Nếu Endpoints có địa chỉ, kiểm tratargetPortvà log của API.Một Pod
club-apifail readiness probe. Điều gì xảy ra với Servicebackend?Đáp án
Pod đó bị loại khỏi Endpoints, Service chỉ chuyển traffic tới các Pod Ready còn lại. Pod không bị restart (đó là việc của liveness probe).
Nộp bài
Nộp vào thư mục lab-24/ trong repository cá nhân, theo templates/lab-template.md.
-
README.mdtrả lời đủ 7 câu hỏi của template (vấn đề giải quyết, kiến thức dùng, các bước, kết quả mong đợi, lỗi đã gặp, cách khắc phục, cải thiện). -
evidence/command-output.txtcó output củakubectl get svc,kubectl get endpoints backend,nslookupba dạng tên, vàcurlquaport-forward. -
report.mdmô tả quá trình điều tra tình huống Endpoints rỗng: triệu chứng, từng bước, nguyên nhân gốc, cách sửa. -
source/chứabackend-svc-lab.yaml(nếu làm L2) và mọi manifest bạn đã sửa. -
screenshots/có ảnhkubectl get svcvà trang frontend (ảnh chỉ bổ trợ, không thay evidence). - Ghi rõ đã làm L1 hay L2 ở cuối
README.md. - Không commit secret: Secret thật, password và file
.envkhông được có trong repo.
Recap
- Service cho Pod một IP ảo và tên DNS ổn định; Pod đến rồi đi, Service ở lại.
- Service chọn Pod bằng label selector; chỉ Pod Ready mới nằm trong Endpoints.
- ClusterIP cho truy cập nội bộ, NodePort mở cổng trên node, LoadBalancer cần cloud (trong kind thì
pending). - Tên DNS:
backend(cùng namespace),backend.club,backend.club.svc.cluster.local(FQDN). portlà cổng Service,targetPortlà cổng Pod; saitargetPortgâyConnection refused.- Khi nghi ngờ Service:
get endpointsđầu tiên, sau đódescribe svcvà so với--show-labels. - Nginx của frontend gọi
http://backend:3000thành công nhờ Service và CoreDNS, không cần IP.