Mở đầu — vì sao bài này quan trọng
Ở bài trước bạn đã hiểu vì sao Kubernetes ra đời và kiến trúc tổng thể của nó — control plane điều phối, các node chạy workload. Nhưng khi ngồi trước một cluster thật và cần đưa ứng dụng của mình lên chạy, bạn sẽ ngay lập tức va vào một câu hỏi rất cụ thể: "Tôi phải khai báo cái gì để container của tôi thực sự chạy, tự phục hồi khi chết, và được người khác truy cập tới?"
Câu trả lời nằm ở ba object nền tảng: Pod, Deployment và Service. Đây là bộ ba mà bất kỳ DevOps Engineer nào cũng phải nắm vững trước khi động tới ConfigMap, Ingress, Helm hay autoscaling ở các bài sau. Nếu ví Kubernetes như một hệ điều hành cho cả cụm máy chủ, thì ba object này chính là "process", "supervisor tự khởi động lại process", và "load balancer nội bộ". Hiểu sai một trong ba, bạn sẽ gặp những lỗi kinh điển: pod chạy rồi chết mà không ai khởi động lại, deploy version mới làm sập cả hệ thống, hoặc ứng dụng chạy hoàn hảo nhưng không service nào gọi tới được.
Điều quan trọng cần nhớ ngay từ đầu: trong Kubernetes, bạn không "ra lệnh" (imperative) mà bạn "khai báo trạng thái mong muốn" (declarative). Bạn viết ra một file YAML nói rằng "tôi muốn có 3 bản sao của ứng dụng này, dùng image này, mở cổng kia" — và Kubernetes sẽ liên tục làm mọi thứ để hiện trạng khớp với mong muốn đó. Đây là tư duy cốt lõi xuyên suốt bài học này.
Khái niệm cốt lõi
Pod — đơn vị nhỏ nhất được lập lịch
Nhiều người mới học lầm tưởng Kubernetes chạy container trực tiếp. Không phải. Đơn vị nhỏ nhất mà Kubernetes lập lịch (schedule) lên một node là Pod, không phải container.
Một Pod là một hoặc nhiều container chạy cùng nhau, chia sẻ chung network namespace và storage volume. "Chia sẻ network namespace" nghĩa là mọi container trong cùng một Pod có chung một địa chỉ IP, chung cổng, và có thể gọi nhau qua localhost. Nếu container A trong Pod nghe ở cổng 8080, container B cùng Pod gọi localhost:8080 là tới được ngay.
Trong thực tế, phần lớn Pod chỉ có một container chính. Trường hợp nhiều container thường là mẫu sidecar: một container phụ chạy bên cạnh để hỗ trợ container chính — ví dụ một container thu thập log, một container proxy, hoặc một container đồng bộ file. Chúng cần sống-chết cùng nhau và chia sẻ tài nguyên, nên gom vào một Pod là hợp lý.
Đặc điểm sống còn cần khắc cốt ghi tâm: Pod là phù du (ephemeral). Pod có thể chết bất cứ lúc nào — node hỏng, bạn deploy lại, scheduler dời workload. Khi một Pod chết, nó không tự sống lại, và Pod mới sẽ có tên khác, IP khác. Vì vậy bạn gần như không bao giờ tạo Pod trực tiếp trong môi trường production. Bạn để một object cấp cao hơn quản lý vòng đời của chúng — đó là Deployment.
Một file Pod tối giản trông như sau:
apiVersion: v1
kind: Pod
metadata:
name: web-pod
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
Deployment — quản lý ReplicaSet, ReplicaSet quản lý Pod
Nếu Pod là process thì Deployment là "người giám sát" đảm bảo luôn có đúng số process đang chạy, và giúp bạn cập nhật version một cách an toàn.
Cấu trúc phân tầng rất quan trọng phải hiểu: Deployment → ReplicaSet → Pod. Bạn khai báo Deployment. Deployment tạo ra một ReplicaSet. ReplicaSet có nhiệm vụ duy nhất là đảm bảo luôn có đúng số lượng Pod đang chạy (ví dụ replicas: 3). Nếu một Pod chết, ReplicaSet lập tức tạo Pod mới thay thế. Nếu bạn lỡ tay xóa một Pod, vài giây sau nó lại xuất hiện — đó chính là ReplicaSet đang làm việc.
Vậy tại sao không dùng thẳng ReplicaSet mà phải qua Deployment? Vì Deployment cho bạn thêm khả năng rolling update và rollback. Khi bạn đổi image từ v1 sang v2, Deployment sẽ tạo một ReplicaSet mới (cho v2), rồi từ từ tăng Pod v2 và giảm Pod v1 — thay vì tắt hết rồi bật lại, gây downtime. Nếu v2 lỗi, bạn gõ một lệnh là quay về ReplicaSet cũ.
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.25
ports:
- containerPort: 80
Chú ý mối liên kết bằng label: selector.matchLabels của Deployment phải khớp với template.metadata.labels của Pod. Đây là cách Kubernetes biết Pod nào "thuộc về" Deployment nào. Label là chất keo gắn kết các object với nhau — và đây cũng là nguồn lỗi phổ biến nhất với người mới.
Service — địa chỉ ổn định cho tập Pod luôn thay đổi
Giờ ta có 3 Pod chạy nhờ Deployment. Nhưng mỗi Pod có IP riêng, và IP đó thay đổi mỗi khi Pod bị thay thế. Làm sao một ứng dụng khác gọi tới được, khi địa chỉ cứ nhảy loạn?
Service giải quyết đúng vấn đề này. Service cung cấp một địa chỉ ổn định (một IP nội bộ cố định và một tên DNS) đại diện cho một nhóm Pod, đồng thời cân bằng tải (load balance) request tới các Pod đó. Service cũng dùng label selector để biết những Pod nào nằm trong nhóm của mình. Pod mới sinh ra mang đúng label sẽ tự động được đưa vào; Pod chết sẽ tự động bị loại. Bạn không cần cấu hình lại gì cả.
Có mấy loại Service chính bạn cần phân biệt:
- ClusterIP (mặc định): tạo IP nội bộ, chỉ truy cập được từ trong cluster. Dùng cho giao tiếp service-to-service, ví dụ backend gọi database.
- NodePort: mở một cổng cố định trên mọi node để truy cập từ bên ngoài. Đơn giản nhưng thô, thường chỉ dùng cho dev/test.
- LoadBalancer: yêu cầu cloud provider (AWS, GCP, Azure) cấp một load balancer thật với IP public. Đây là cách phổ biến để expose service ra Internet trên cloud.
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
type: ClusterIP
Ở đây port là cổng của Service, targetPort là cổng trên Pod. Nhờ selector: app: web, Service này tự động gom mọi Pod có label app=web — chính là các Pod do Deployment ở trên tạo ra. Bên trong cluster, các service khác chỉ cần gọi http://web-svc là tới, không cần quan tâm có bao nhiêu Pod hay IP của chúng là gì.
Tình huống thực tế
Ví dụ 1 — Tiki xử lý flash sale với rolling update
Hãy tưởng tượng đội backend của một sàn thương mại điện tử như Tiki chạy service order-api trên Kubernetes với Deployment replicas: 12. Trước đợt flash sale 12/12, họ cần deploy một bản vá tối ưu truy vấn giỏ hàng. Nếu tắt hết 12 Pod cũ rồi bật 12 Pod mới, sẽ có vài chục giây hệ thống không nhận đơn — điều không thể chấp nhận khi mỗi giây có hàng nghìn đơn.
Nhờ Deployment, họ chỉ cần đổi image tag từ order-api:2.3.1 sang order-api:2.4.0. Kubernetes tạo ReplicaSet mới và thực hiện rolling update: mỗi lần thay vài Pod, luôn giữ đủ số Pod khỏe mạnh phục vụ traffic (cấu hình qua maxUnavailable và maxSurge). Trong lúc đó, Service order-api-svc vẫn giữ nguyên IP và DNS, tự động chỉ request sang các Pod đang sẵn sàng. Client hoàn toàn không thấy gián đoạn.
Bài học: Deployment cho phép cập nhật liên tục không downtime, và Service là lớp đệm giữ địa chỉ ổn định trong khi các Pod bên dưới liên tục được thay mới. Đây là lý do bộ ba này luôn đi cùng nhau.
Ví dụ 2 — Startup fintech ở TP.HCM và cú xóa Pod hoảng loạn
Một startup fintech nhỏ ở TP.HCM, đội chỉ có 4 engineer, gặp sự cố: một Pod payment-worker bị treo. Một bạn junior, quen tư duy máy chủ truyền thống, SSH vào và... kubectl delete pod payment-worker-7d9f..., định "khởi động lại cho sạch". Bạn ấy hốt hoảng khi thấy vài giây sau một Pod mới với tên khác lại xuất hiện.
Thực ra đây là hành vi đúng và mong muốn. Pod này do một Deployment quản lý; ReplicaSet phát hiện số Pod thực tế ít hơn replicas mong muốn nên lập tức tạo Pod thay thế. Vấn đề thật sự nằm ở chỗ khác: Pod cứ bị treo lặp lại vì thiếu liveness probe để Kubernetes tự phát hiện và restart, và thiếu resource limit khiến Pod ngốn RAM rồi bị OOMKilled.
Bài học: Đừng chống lại cơ chế self-healing của Kubernetes. Trong môi trường declarative, bạn không "sửa" bằng cách can thiệp thủ công vào Pod — bạn sửa bằng cách chỉnh khai báo (thêm probe, thêm limit). Và tuyệt đối không tạo Pod trần trong production; luôn để Deployment quản lý.
Ví dụ 3 — Giao tiếp nội bộ giữa các service ở một công ty logistics
Một công ty logistics ở Đông Nam Á tách hệ thống thành nhiều service: tracking-api, notification, pricing. Service tracking-api cần gọi pricing để tính cước. Ban đầu, một engineer hard-code IP của Pod pricing vào config tracking-api. Hôm sau pricing deploy bản mới, Pod cũ chết, IP đổi — và toàn bộ tính năng tra cước sập, dù pricing vẫn chạy hoàn hảo.
Cách làm đúng là tạo một Service ClusterIP tên pricing. Từ đó tracking-api chỉ gọi http://pricing:8080 — một tên DNS ổn định. Dù Pod pricing có chết đi sống lại, scale từ 2 lên 10 bản, hay đổi IP bao nhiêu lần, tên pricing vẫn trỏ đúng và request được cân bằng tải sang các Pod khỏe.
Bài học: Không bao giờ gọi trực tiếp IP của Pod. Luôn giao tiếp qua Service để có địa chỉ ổn định và load balancing miễn phí. Đây là nguyên tắc bất di bất dịch trong kiến trúc microservices trên Kubernetes.
Hướng dẫn từng bước
Ta sẽ triển khai một ứng dụng web đơn giản đầy đủ ba object. Giả định bạn đã có một cluster (Minikube, kind, hoặc cloud) và kubectl trỏ đúng context.
Bước 1 — Tạo Deployment. Viết file deployment.yaml như mẫu ở trên (nginx, 3 replicas) rồi áp dụng:
kubectl apply -f deployment.yaml
Bước 2 — Kiểm tra Pod đã chạy. Deployment sẽ tự tạo ReplicaSet và Pod:
kubectl get deployments
kubectl get rs
kubectl get pods -l app=web
Bạn sẽ thấy 3 Pod ở trạng thái Running, tên dạng web-xxxxx-yyyyy.
Bước 3 — Thử nghiệm self-healing. Xóa một Pod và quan sát ReplicaSet tạo lại:
kubectl delete pod <tên-một-pod>
kubectl get pods -l app=web -w
Bạn sẽ thấy Pod mới xuất hiện gần như tức thì.
Bước 4 — Tạo Service. Áp dụng service.yaml (ClusterIP như mẫu):
kubectl apply -f service.yaml
kubectl get svc web-svc
Bước 5 — Kiểm tra Service gom đúng Pod. Lệnh này liệt kê các endpoint (IP:port của từng Pod) mà Service đang trỏ tới:
kubectl get endpoints web-svc
Bạn phải thấy 3 endpoint tương ứng 3 Pod. Nếu thấy <none>, selector của Service không khớp label Pod — lỗi kinh điển.
Bước 6 — Thử truy cập. Từ trong cluster, tạo một Pod tạm để curl:
kubectl run tmp --rm -it --image=busybox -- sh
# trong shell:
wget -qO- http://web-svc
Bước 7 — Thực hiện rolling update và rollback. Đổi image rồi theo dõi:
kubectl set image deployment/web web=nginx:1.26
kubectl rollout status deployment/web
kubectl rollout undo deployment/web # nếu cần quay lui
Lỗi thường gặp & mẹo
- Selector không khớp label. Đây là lỗi số một.
Deployment.spec.selector.matchLabelsphải khớptemplate.metadata.labels;Service.spec.selectorphải khớp label Pod. Sai một ký tự là Service không gom được Pod nào,kubectl get endpointstrả về rỗng. Luôn kiểm tra endpoints khi Service "không hoạt động".
- Nhầm
port,targetPort,containerPort.containerPortlà cổng app nghe trong container;targetPortcủa Service phải khớpcontainerPort;portlà cổng bạn gọi Service. Nhầm lẫn khiến kết nối bị treo dù mọi Pod đềuRunning.
- Tạo Pod trần trong production. Pod tạo trực tiếp không có ai quản lý — chết là mất luôn. Luôn dùng Deployment.
- Quên đặt resource requests/limits. Không có limit, một Pod lỗi có thể ngốn hết RAM node, kéo sập cả các Pod khác. Đây là mảng bạn sẽ đào sâu ở bài về autoscaling, nhưng cần có thói quen đặt ngay từ đầu.
- Mẹo debug nhanh:
kubectl describe pod <tên>xem sự kiện và lý do lỗi (ImagePullBackOff, CrashLoopBackOff, OOMKilled);kubectl logs <tên>xem log ứng dụng;kubectl get events --sort-by=.lastTimestampxem toàn bộ diễn biến gần nhất.
- Mẹo tư duy: khi có gì đó "không đúng", đừng sửa Pod. Sửa file YAML rồi
kubectl applylại. Trạng thái mong muốn nằm trong file, không phải trong cluster.
Bài tập thực hành
- Cơ bản: Tạo một Deployment chạy image
httpd:2.4với 2 replicas. Dùngkubectl get pods -w, xóa một Pod và ghi lại thời gian ReplicaSet tạo Pod thay thế.
- Nối Service: Tạo một Service ClusterIP trỏ tới Deployment trên. Chạy
kubectl get endpointsvà xác nhận có đúng 2 endpoint. Sau đó cố tình sửa sai một ký tự trong selector của Service, apply lại, và quan sát endpoints trở về rỗng — để tự tay trải nghiệm lỗi phổ biến nhất.
- Rolling update: Scale Deployment lên 4 replicas bằng
kubectl scale. Sau đó đổi image sang một version khác, dùngkubectl rollout statustheo dõi, rồikubectl rollout undođể quay lui. Ghi lại điều bạn quan sát về số Pod cũ/mới trong quá trình.
- Suy ngẫm: Viết 3–5 câu giải thích vì sao ứng dụng khác nên gọi Service
web-svcthay vì gọi trực tiếp IP của một Pod. Liên hệ với ví dụ công ty logistics ở trên.
Tóm tắt
- Pod là đơn vị nhỏ nhất được lập lịch: một hoặc nhiều container chia sẻ network namespace và volume, giao tiếp nội bộ qua
localhost. Pod là phù du, không tự sống lại, và gần như không bao giờ được tạo trực tiếp trong production. - Deployment quản lý theo tầng: Deployment → ReplicaSet → Pod. ReplicaSet giữ đúng số Pod (self-healing); Deployment thêm rolling update và rollback để cập nhật không downtime.
- Service cung cấp địa chỉ ổn định (IP nội bộ + DNS) và load balancing cho một nhóm Pod luôn thay đổi, dùng label selector để gom Pod. Ba loại phổ biến: ClusterIP (nội bộ), NodePort, LoadBalancer.
- Label là chất keo gắn kết ba object; selector sai là nguyên nhân lỗi số một.
- Tư duy xuyên suốt: khai báo trạng thái mong muốn trong YAML, để Kubernetes tự đưa hiện trạng về khớp — đừng can thiệp thủ công vào Pod.