Docker for Developers · Part 9 — Kubernetes Fundamentals
From a single host to a cluster: why Kubernetes exists, its architecture, and the core objects — Pods, Deployments, and Services. Spin up a local cluster with kind and deploy your first app from Compose.
Part 9 of 10 in the Docker → Compose → Kubernetes series {Phần 9/10 trong series Docker → Compose → Kubernetes}. Previous {Trước}: Part 8 — Debugging & Troubleshooting Docker · Next {Tiếp}: Part 10 — Kubernetes in Practice & Debugging. Every part ends with exercises; do them, don’t just read {Mỗi phần kết thúc bằng bài tập; hãy làm, đừng chỉ đọc}.
Docker Compose runs containers on one machine — you declare services in YAML and docker compose up brings the stack online (Part 5, Part 6) {Docker Compose chạy container trên một máy — bạn khai báo service trong YAML và docker compose up bật cả stack (Phần 5, Phần 6)}. That is enough for local dev and many small deployments {Đủ cho dev local và nhiều triển khai nhỏ}. When you need many machines, self-healing (restart crashed workloads), rolling updates without downtime, and horizontal scaling across nodes, you need an orchestrator — and the industry default is Kubernetes (often shortened k8s) {Khi cần nhiều máy, tự phục hồi, rolling update không downtime và scale ngang trên nhiều node, bạn cần orchestrator — chuẩn công nghiệp là Kubernetes (k8s)}.
Be honest: Kubernetes is heavier than Docker alone — more concepts, YAML, and moving parts {Thẳng thắn: Kubernetes nặng hơn Docker — nhiều khái niệm, YAML và bộ phận chuyển động}. You are in the right place because you already understand images, containers, networks, volumes, and Compose from Parts 1–8 {Bạn đúng chỗ vì đã hiểu image, container, mạng, volume và Compose từ Phần 1–8}. This part introduces the mental model and your first cluster on a laptop with kind {Phần này giới thiệu mô hình tư duy và cluster đầu tiên trên laptop bằng kind}. Part 10 goes deeper on config, probes, and debugging k8s {Phần 10 đi sâu config, probe và debug k8s}.
Cluster architecture {Kiến trúc cluster}
A Kubernetes cluster is a set of machines (physical or virtual) cooperating to run your workloads {Một cluster Kubernetes là tập máy (vật lý hoặc ảo) phối hợp chạy workload của bạn}. One side is the control plane (the brain); the other is worker nodes (where containers actually run) {Một phía là control plane (bộ não); phía kia là worker node (nơi container thực sự chạy)}.
- kube-apiserver — REST API;
kubectltalks here {API REST;kubectlgọi vào đây} - scheduler — picks a node for each new Pod {Chọn node cho Pod mới}
- controller-manager — keeps Deployments/ReplicaSets matching spec {Giữ Deployment/ReplicaSet khớp spec}
- etcd — cluster state store {Lưu trạng thái cluster}
- kubelet — runs Pods on the node via the container runtime {Chạy Pod trên node qua runtime}
- kube-proxy — Service → Pod networking rules {Quy tắc mạng Service → Pod}
The declarative model {Mô hình khai báo}
You do not SSH in and docker run on node 3 {Bạn không SSH vào node 3 rồi docker run}. You describe desired state in YAML (or Helm, etc.) and apply it; Kubernetes reconciles until reality matches {Bạn mô tả trạng thái mong muốn trong YAML rồi apply; Kubernetes điều chỉnh đến khi thực tế khớp}. If a Pod dies, the controller creates another {Pod chết thì controller tạo cái mới}. If you ask for three replicas, the system keeps three {Bạn yêu cầu ba replica thì hệ thống giữ ba}.
The core objects {Các object lõi}
You will live in three kinds for a long time: Pod, Deployment, Service {Bạn sẽ sống với ba loại này khá lâu: Pod, Deployment, Service}.
Pod — smallest schedulable unit {Pod — đơn vị lập lịch nhỏ nhất}
A Pod wraps one or more containers that share network and storage {Pod bọc một hoặc nhiều container dùng chung mạng và storage}. In practice, one container per Pod is the common case {Thực tế một container mỗi Pod là phổ biến}. Pods are ephemeral — when they die, their IP is gone; you rarely create bare Pods by hand {Pod ngắn hạn — chết thì IP mất; hiếm khi tạo Pod trần bằng tay}.
Deployment — desired replicas & rolling updates {Deployment — replica mong muốn & rolling update}
A Deployment owns a ReplicaSet, which owns Pods {Deployment sở hữu ReplicaSet, ReplicaSet sở hữu Pod}. You set replicas: 2 and the Deployment ensures two matching Pods run, replaces failed ones, and rolls out image changes gradually {Bạn đặt replicas: 2 và Deployment đảm bảo hai Pod khớp, thay Pod hỏng, và rollout image từ từ}.
Deployment "web"
│
│ manages
▼
ReplicaSet "web-7d4f9c8b6"
│
│ owns
├──────────┬──────────┐
▼ ▼ ▼
Pod web-1 Pod web-2 (scale adds more)
Service — stable network in front of Pods {Service — mạng ổn định trước Pod}
Pod IPs change when Pods restart {IP Pod đổi khi Pod restart}. A Service is a stable DNS name + virtual IP that load-balances traffic to healthy Pods matching a label selector {Service là tên DNS + IP ảo ổn định, cân bằng tải tới Pod khỏe khớp label selector}.
ClusterIP (default) — internal DNS only; NodePort — port on every node; LoadBalancer — cloud LB in prod {ClusterIP — DNS nội bộ; NodePort — cổng trên mọi node; LoadBalancer — LB cloud trên prod}.
kubectl basics {Cơ bản kubectl}
kubectl is the CLI for the Kubernetes API — like docker for a single daemon, but for the cluster {kubectl là CLI cho API Kubernetes — giống docker với một daemon, nhưng cho cả cluster}. Install alongside kind or minikube {Cài cùng kind hoặc minikube}. Everyday commands {Lệnh hằng ngày}:
kubectl get pods # list Pods (default namespace)
kubectl get pods -n kube-system # -n = namespace
kubectl get pods -o wide # node IP, which node, etc.
kubectl describe pod <name> # events, state, why Pending/CrashLoop
kubectl logs <pod> # stdout/stderr (like docker logs)
kubectl logs -f <pod> # follow
kubectl exec -it <pod> -- sh # shell inside (like docker exec)
kubectl apply -f manifest.yaml # create or update from YAML
kubectl delete -f manifest.yaml # remove resources in file
kubectl delete pod <name> # delete one Pod (Deployment recreates it)
Use -n <namespace> to target kube-system, staging, etc. — tutorials often stay in default {Dùng -n cho kube-system, staging… — tutorial thường ở default}. Workflow: edit YAML → kubectl apply -f … → get / describe / logs while controllers reconcile {Quy trình: sửa YAML → apply → get/describe/logs, controller điều chỉnh nền}.
A local cluster with kind {Cluster local với kind}
kind runs a cluster inside Docker on your laptop; minikube is a common alternative — same kubectl flow {kind chạy cluster trong Docker; minikube là lựa chọn khác — cùng quy trình kubectl}.
# Install: https://kind.sigs.k8s.io/docs/user/quick-start/
kind create cluster --name dev
kubectl cluster-info --context kind-dev
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# dev-control-plane Ready control-plane 1m v1.33.x
Your kubectl context now points at kind-dev {context kubectl giờ trỏ tới kind-dev}. Check with kubectl config current-context {Kiểm tra bằng kubectl config current-context}.
Loading local images into kind {Nạp image local vào kind}
Images you build with docker build exist on your host Docker, not inside the kind nodes {Image build bằng docker build nằm trên Docker host, không có sẵn trong node kind}. Before a Deployment can pull myapp:local, load it {Trước khi Deployment kéo myapp:local, nạp vào}:
docker build -t myapp:local .
kind load docker-image myapp:local --name dev
Without this step, Pods often sit in ImagePullBackOff {Không bước này, Pod thường kẹt ImagePullBackOff}.
Teardown when done {Dọn khi xong}:
kind delete cluster --name dev
Your first deployment {Deployment đầu tiên}
Create a folder k8s-lab-09/ and save two files {Tạo thư mục k8s-lab-09/ và lưu hai file}.
deployment.yaml — nginx, two replicas, labels the Service will select {nginx, hai replica, label để Service chọn}:
# deployment.yaml — desired state: 2 nginx Pods
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy # Deployment name (kubectl get deploy)
labels:
app: nginx
spec:
replicas: 2 # controller keeps 2 Pods running
selector:
matchLabels:
app: nginx # must match template labels below
template: # Pod template — each Pod looks like this
metadata:
labels:
app: nginx # Service selector targets this label
spec:
containers:
- name: nginx
image: nginx:1.27-alpine # public image; no kind load needed
ports:
- containerPort: 80 # container listens here (not published to host yet)
service.yaml — ClusterIP Service (reach it with port-forward) {Service ClusterIP (truy cập bằng port-forward)}:
# service.yaml — stable VIP + DNS name → Pods with app=nginx
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
type: ClusterIP # default; internal cluster IP
selector:
app: nginx # must match Pod labels from Deployment
ports:
- port: 80 # Service port (what clients connect to)
targetPort: 80 # Pod containerPort
Apply and inspect {Apply và kiểm tra}:
kubectl apply -f deployment.yaml -f service.yaml
kubectl get deploy,pods,svc
kubectl get pods -o wide
# wait until both Pods are Running (READY 1/1)
Reach the app from your laptop {Truy cập từ laptop}:
kubectl port-forward svc/nginx-svc 8080:80
# leave running; in another terminal:
curl -s http://localhost:8080 | head -5
port-forward tunnels to the Service — simplest way to curl from your laptop; NodePort works too but needs extra port mapping on kind {port-forward tunnel tới Service — cách đơn giản nhất để curl từ laptop; NodePort cũng được nhưng cần map cổng thêm trên kind}.
From Compose to Kubernetes {Từ Compose sang Kubernetes}
You already think in services from Compose {Bạn đã nghĩ theo service từ Compose}. Kubernetes splits responsibilities differently {Kubernetes tách trách nhiệm khác}:
Compose (compose.yaml) | Kubernetes {Kubernetes} |
|---|---|
services.web | Deployment (Pods) + Service (network) {Deployment + Service} |
ports: "8080:80" | Service (port / NodePort) or kubectl port-forward {Service hoặc port-forward} |
volumes: | PersistentVolume + PersistentVolumeClaim (Part 10) {PV + PVC (Phần 10)} |
environment: / env_file: | env in Pod spec, ConfigMap, Secret (Part 10) {env, ConfigMap, Secret (Phần 10)} |
depends_on + health | liveness/readiness probes (Part 10) {probe (Phần 10)} |
docker compose up | kubectl apply -f … {kubectl apply} |
docker compose scale web=3 | kubectl scale deploy/nginx-deploy --replicas=3 {kubectl scale} |
kompose can bootstrap manifests from Compose; hand-writing YAML teaches labels, selectors, and Deployment ↔ Service wiring {kompose khởi tạo manifest từ Compose; viết tay dạy label, selector và nối Deployment ↔ Service}.
Common pitfalls {Các bẫy thường gặp}
ImagePullBackOff— cluster cannot find your image. Public images (nginx:alpine) work; local tags needkind load docker-imageor push to a registry {Cluster không tìm thấy image. Image public ổn; tag local cầnkind loadhoặc push registry}.- Hitting Pod IP directly — Pod IPs are ephemeral; always go through a Service (or Ingress) for stable access {Gọi thẳng IP Pod — IP ngắn hạn; luôn qua Service (hoặc Ingress)}.
- Wrong kubectl context —
kubectltalks to whichever clustercurrent-contextpoints at; verify withkubectl config get-contextsbefore deleting things {context sai — xác nhậnkubectl config get-contextstrước khi xóa}. - NodePort on kind vs port-forward — NodePort needs extra port mapping on kind;
port-forwardis simpler for “curl from my Mac” {NodePort trên kind cần map cổng thêm;port-forwardđơn giản hơn để curl từ máy bạn}. - Expecting Compose networking verbatim — in-cluster DNS is
http://<service-name>(same namespace), nothttp://localhostinside another Pod {Không copy nguyên mạng Compose — DNS trong cluster làhttp://<tên-service>(cùng namespace)}.
Cheat sheet {Bảng tra nhanh}
# cluster & context
kubectl get nodes
kubectl config current-context
kubectl config use-context kind-dev
# workloads
kubectl apply -f .
kubectl get deploy,rs,pods,svc
kubectl describe deploy nginx-deploy
kubectl scale deploy/nginx-deploy --replicas=4
# debug (mirror Part 8 docker habits)
kubectl logs <pod> -f
kubectl exec -it <pod> -- sh
kubectl get events --sort-by='.lastTimestamp'
# access
kubectl port-forward svc/nginx-svc 8080:80
# cleanup
kubectl delete -f deployment.yaml -f service.yaml
kind delete cluster --name dev
Bài tập / Exercises
Work in k8s-lab-09/ with the manifests from this post (or your own names) {Làm trong k8s-lab-09/ với manifest trong bài (hoặc tên riêng)}. Requires kind (or minikube) and kubectl {Cần kind (hoặc minikube) và kubectl}.
1. Create a kind cluster named lab09, verify nodes are Ready, and confirm your context is kind-lab09 {Tạo cluster kind tên lab09, xác nhận node Ready, context là kind-lab09}.
Solution {Lời giải}
kind create cluster --name lab09
kubectl get nodes
kubectl config current-context # kind-lab092. Apply the nginx Deployment with replicas: 2 and confirm two Pods are Running with kubectl get pods {Apply Deployment nginx replicas: 2, xác nhận hai Pod Running}.
Solution {Lời giải}
kubectl apply -f deployment.yaml
kubectl get pods -w
# Ctrl+C when both show 1/1 Running
kubectl get deploy nginx-deploy3. Delete one Pod by name (kubectl delete pod …) and watch a new Pod appear — self-healing from the Deployment {Xóa một Pod, xem Pod mới xuất hiện — tự phục hồi từ Deployment}.
Solution {Lời giải}
kubectl get pods
kubectl delete pod nginx-deploy-xxxxxxxxxx-xxxxx # pick one Pod name from get pods
kubectl get pods -w
# a new Pod is created; replica count stays at 24. Apply the Service, port-forward to port 8080, and curl the nginx welcome page {Apply Service, port-forward cổng 8080, curl trang chào nginx}.
Solution {Lời giải}
kubectl apply -f service.yaml
kubectl port-forward svc/nginx-svc 8080:80 &
curl -s http://localhost:8080 | head -3
kill %1 # stop port-forward job5. Scale the Deployment to 4 replicas, confirm four Pods, then scale back to 2 {Scale Deployment lên 4 replica, xác nhận bốn Pod, rồi scale về 2}.
Solution {Lời giải}
kubectl scale deploy/nginx-deploy --replicas=4
kubectl get pods
kubectl scale deploy/nginx-deploy --replicas=2
kubectl get podsStretch {Nâng cao}: build a tiny local image (docker build -t lab09-app:local .), kind load docker-image lab09-app:local --name lab09, change the Deployment image to lab09-app:local with imagePullPolicy: Never, apply, and confirm Pods run {Build image local, kind load, đổi image Deployment thành lab09-app:local với imagePullPolicy: Never, apply, xác nhận Pod chạy}.
Solution {Lời giải}
# in deployment.yaml container spec:
image: lab09-app:local
imagePullPolicy: Neverkind load docker-image lab09-app:local --name lab09
kubectl apply -f deployment.yaml
kubectl get podsKey takeaways {Điểm chính}
- Compose = one host; Kubernetes = many nodes with reconciliation, scaling, and rolling updates {Compose = một host; Kubernetes = nhiều node với điều chỉnh, scale và rolling update}.
- Pod runs containers; Deployment keeps replica count and heals failures; Service gives a stable name/IP in front of Pods {Pod chạy container; Deployment giữ số replica và phục hồi; Service cho tên/IP ổn định trước Pod}.
kubectl applyis the daily loop;get/describe/logs/execmirror Docker debugging from Part 8 {kubectl applylà vòng lặp hằng ngày;get/describe/logs/execgiống debug Docker ở Phần 8}.- kind (+
kind load docker-image) is the fastest way to practice without a cloud bill {kind (+kind load) là cách nhanh nhất để luyện không tốn tiền cloud}.
Next up {Tiếp theo}
Part 10 — Kubernetes in Practice & Debugging — ConfigMaps and Secrets, liveness/readiness probes, persistent volumes, Ingress basics, and a systematic kubectl debug checklist when Pods won’t start {Phần 10 — Kubernetes thực hành & Debug — ConfigMap và Secret, probe liveness/readiness, volume bền, Ingress cơ bản, và checklist debug kubectl khi Pod không lên}. ← Part 8