Smoke Test: Cả Cluster Chạy Cùng Nhau
Mười lăm bài qua dựng từng mảnh: control plane, worker, container runtime, mạng pod, Service, DNS. Mỗi mảnh đã được kiểm chứng riêng lúc lắp. Bài này làm việc khác — chạy một ứng dụng thật như người dùng cuối sẽ làm, và quan sát mọi mảnh phối hợp. Đây vừa là phần thưởng (cuối cùng cũng kubectl apply một app), vừa là cách kiểm tra có hệ thống: mỗi thao tác dưới đây, nếu chạy đúng, chứng minh một thành phần cụ thể hoạt động.
Ta sẽ triển khai một Deployment, lộ ra qua Service, rồi lần lượt: gọi bằng tên, xem lưu lượng chia tải, đọc log, vào trong container, mở cổng về laptop, xóa pod để xem tự chữa lành, và scale. Cuối bài là một bảng map mỗi quan sát về đúng bài đã dựng nó.
kubectl apply ─► api-server ─► etcd (lưu mong muốn)
│
controller-mgr ┤ Deployment→ReplicaSet→Pod
scheduler ──────┤ gán Pod vào node
▼
kubelet ─► containerd ─► pod (CNI cấp IP)
▲
client pod ─DNS(CoreDNS)─► ClusterIP ─kube-proxy─► một pod web
Bước 1 — Triển khai một ứng dụng thật
Dùng agnhost — một image test của Kubernetes, ở chế độ netexec nó dựng một HTTP server trả về tên pod tại đường dẫn /hostname. Tên pod trong câu trả lời chính là thứ giúp ta thấy lưu lượng rơi vào bản sao nào.
cat <<'EOF' | kubectl apply -f -
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: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080
EOF
kubectl apply chỉ gửi mong muốn tới api-server (3 bản sao của image này). Phần biến mong muốn thành hiện thực là chuỗi controller ta dựng ở Bài 8: controller-manager thấy Deployment, tạo ReplicaSet, ReplicaSet tạo 3 Pod; scheduler gán mỗi Pod vào một node; kubelet trên node đó gọi containerd chạy container; CNI cấp IP. Xem kết quả:
kubectl rollout status deploy/web
kubectl get pods -l app=web -o wide
deployment "web" successfully rolled out
NAME READY STATUS RESTARTS AGE IP NODE
web-578474b6fc-8rsb8 1/1 Running 0 4s 10.200.0.6 worker-0
web-578474b6fc-b67xb 1/1 Running 0 4s 10.200.1.5 worker-1
web-578474b6fc-hp4jj 1/1 Running 0 4s 10.200.0.5 worker-0
Ba pod Running, trải trên cả hai node, mỗi pod một IP trong dải của node mình. Service web gom chúng lại:
kubectl get svc web
kubectl get endpointslices -l kubernetes.io/service-name=web
NAME TYPE CLUSTER-IP PORT(S) AGE
web ClusterIP 10.32.0.93 80/TCP 5s
NAME ADDRESSTYPE PORTS ENDPOINTS
web-jv5dr IPv4 8080 10.200.0.5,10.200.0.6,10.200.1.5
Service nhận một ClusterIP (10.32.0.93, lần này để Kubernetes tự cấp), và EndpointSlice đã tự gom đúng ba IP pod — đây là controller endpoint theo dõi nhãn app=web và cập nhật danh sách. Mọi mảnh đã vào đúng chỗ; giờ thử dùng.
Bước 2 — Gọi bằng tên, và thấy chia tải
Tạo một pod client, rồi gọi Service bằng tên web (không phải IP):
kubectl run client --image=busybox:1.36 --restart=Never --command -- sleep 3600
kubectl exec client -- sh -c 'for i in 1 2 3 4 5 6; do wget -qO- http://web/hostname; echo; done'
web-578474b6fc-8rsb8
web-578474b6fc-b67xb
web-578474b6fc-8rsb8
web-578474b6fc-b67xb
web-578474b6fc-8rsb8
web-578474b6fc-b67xb
Một lời gọi wget http://web/... gọn gàng này thực ra kéo theo cả chuỗi: busybox hỏi CoreDNS (Bài 15) phân giải web thành 10.32.0.93; gói tới ClusterIP đó bị kube-proxy (Bài 12) DNAT sang một pod endpoint; gói đi tới pod đích qua mạng Bài 14. Tên pod đổi giữa các lần gọi cho thấy chia tải đang chạy — mỗi kết nối mới rơi vào một endpoint khác theo xác suất của iptables. Phân giải tên xác nhận luôn:
kubectl exec client -- nslookup web.default.svc.cluster.local
Name: web.default.svc.cluster.local
Address: 10.32.0.93
Bước 3 — logs và exec
Hai lệnh quen tay nhất khi vận hành, và cả hai đi qua đường api-server → kubelet mà ta đã cấp RBAC ở Bài 9 và bật webhook authz ở Bài 11:
POD=$(kubectl get pod -l app=web -o jsonpath='{.items[0].metadata.name}')
kubectl logs $POD --tail=3
kubectl exec $POD -- hostname
I0523 14:55:35.269860 1 log.go:245] hostname: web-578474b6fc-8rsb8
I0523 14:55:35.277355 1 log.go:245] GET /hostname
I0523 14:55:35.277394 1 log.go:245] hostname: web-578474b6fc-8rsb8
web-578474b6fc-8rsb8
Log hiện đúng những lời gọi /hostname ta vừa bắn ở Bước 2. exec chạy được lệnh bên trong container và trả kết quả về. Nếu RBAC apiserver-to-kubelet hay phần xác thực của kubelet sai, hai lệnh này sẽ báo lỗi quyền — chúng chạy nghĩa là đường đó thông.
Bước 4 — port-forward về laptop
kubectl port-forward mở một đường hầm từ máy bạn qua api-server xuống pod — cách truy cập một Service nội bộ mà không phải lộ nó ra ngoài. Nó dùng subresource portforward của kubelet, lại là đường api-server → kubelet:
kubectl port-forward svc/web 18080:80 &
curl -s http://127.0.0.1:18080/hostname; echo
web-578474b6fc-hp4jj
Từ laptop, curl localhost:18080 chạm tới một pod web nằm sâu trong VPC. Đây là cách tiện nhất để thử một dịch vụ in-cluster trong lúc phát triển.
Bước 5 — Xóa một pod, xem cluster tự chữa lành
Đây là phép thử cho vòng điều khiển (control loop) — thứ phân biệt Kubernetes với việc chạy container bằng tay. Trạng thái mong muốn là 3 bản sao; nếu thực tế lệch đi, controller kéo nó về. Xóa thẳng một pod:
kubectl delete pod $POD
sleep 4
kubectl get pods -l app=web
pod "web-578474b6fc-8rsb8" deleted
NAME READY STATUS RESTARTS AGE
web-578474b6fc-b67xb 1/1 Running 0 33s
web-578474b6fc-hp4jj 1/1 Running 0 33s
web-578474b6fc-n7npb 1/1 Running 0 5s
Pod 8rsb8 biến mất, nhưng một pod mới n7npb (5 giây tuổi) đã thế chỗ — ReplicaSet thấy chỉ còn 2/3 bản sao và lập tức tạo bù. Không ai can thiệp; đó là controller-manager làm đúng việc của nó. EndpointSlice và rule kube-proxy cũng tự cập nhật theo, nên Service không hề gãy trong lúc thay pod.
Bước 6 — Scale bằng khai báo
Cuối cùng, đổi số bản sao mong muốn từ 3 lên 4:
kubectl scale deploy/web --replicas=4
kubectl rollout status deploy/web
kubectl get deploy web
deployment.apps/web scaled
deployment "web" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
web 4/4 4 4 51s
Cùng vòng điều khiển ở Bước 5, lần này đi theo hướng tăng: ta khai 4, controller tạo thêm một pod, scheduler tìm node cho nó, và 4/4 sẵn sàng. Ta không nói làm thế nào, chỉ nói muốn gì — phần còn lại là việc của các controller.
Mỗi phép thử soi mảnh nào
Gom lại, smoke test này không phải một thao tác mà là một lát cắt ngang toàn hệ thống:
| Quan sát | Thành phần được xác nhận | Dựng ở bài |
|---|---|---|
| Pod trải trên hai node | scheduler | 8 |
| Container chạy, pod có IP | kubelet + containerd + CNI | 10, 11, 14 |
| EndpointSlice tự gom IP pod | endpoint controller | 8 |
Gọi http://web được |
CoreDNS | 15 |
| Tên pod đổi giữa các lần gọi | kube-proxy (chia tải) | 12 |
logs / exec / port-forward |
api-server → kubelet + RBAC | 9, 11 |
| Xóa pod tự tạo lại | controller-manager (control loop) | 8 |
scale về đúng số |
Deployment/ReplicaSet controller | 8 |
Nếu bất kỳ dòng nào hỏng, ta biết ngay phải quay lại bài nào để soi. Cả tám đều xanh nghĩa là cluster tự dựng này không chỉ "lên" mà thực sự hoạt động như một Kubernetes đầy đủ.
🧹 Dọn dẹp
Xóa ứng dụng thử và pod client; CoreDNS và các thành phần hệ thống giữ nguyên:
kubectl delete deploy web
kubectl delete svc web
kubectl delete pod client
Không có gì để dọn trên node hay VPC — tất cả tài nguyên ở bước này đều là object trong cluster, xóa là sạch. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 16-smoke-test.
Tổng kết
Cluster dựng tay từ con số không giờ chạy một ứng dụng thật, chia tải, tự chữa lành và scale — đúng những gì người ta mong đợi ở Kubernetes. Quan trọng hơn việc nó chạy là việc ta biết vì sao nó chạy: mỗi thao tác trong bài map về một thành phần cụ thể đã tự tay dựng, nên khi có gì hỏng, việc gỡ rối không còn là đoán mò.
Đến đây phần "dựng" khép lại. Hai bài tới chuyển sang hiểu sâu hơn cái đã dựng. Bài 17 lần theo vòng đời một request từ lúc kubectl apply cho tới khi pod chạy — đi qua đúng những thành phần trên, nhưng lần này theo trình tự thời gian của một object, để thấy chúng chuyền tay nhau ra sao. Đó là bước đệm trước khi ta thay lớp mạng bằng Cilium eBPF ở các bài sau.