Smoke Test: Cả Cluster Chạy Cùng Nhau

K
Kai··7 min read

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.

Related Posts