# 🧩 Kiến trúc control plane, node và static pod

> Sign in to view the full content of this lesson.

Chào bạn, mình đã ngồi thi CKA và câu hỏi đầu tiên mình gặp không phải "viết YAML" mà là "sửa cái gì đó trong cluster". Muốn sửa được, bạn phải biết thứ mình cần sửa đang nằm ở đâu. Bài này chính là bản đồ đó: control plane gồm những gì, node gồm những gì, và vì sao 4 component quan trọng nhất của cluster lại không do Kubernetes tạo ra mà do kubelet đọc từ một thư mục trên đĩa.

## Mục tiêu bài học

- Vẽ lại được kiến trúc cluster: control plane gồm component nào, node gồm component nào, ai gọi ai.
- Chỉ đúng file manifest của từng static pod trong `/etc/kubernetes/manifests` và biết cách đọc nó.
- Tự tay sửa một manifest static pod (đổi flag, đổi image, sửa lỗi cú pháp) và xác nhận pod được restart.
- Phân biệt static pod với mirror pod, giải thích được vì sao `kubectl delete` không xoá vĩnh viễn được static pod.
- Tránh 3 lỗi tốn thời gian nhất trong phòng thi: sửa mirror pod, quên backup manifest, làm chết kube-apiserver.

## Ôn nhanh (2 phút)

Bài trước bạn đã có `alias k=kubectl`, `--dry-run=client -o yaml` và `kubectl explain`. Bài này dùng lại gần như toàn bộ:

- `kubectl explain pod.spec.containers.command` để nhớ nhanh field trước khi viết manifest.
- `kubectl get pods -n kube-system -o wide` để biết pod nào đang chạy trên node nào.
- `--dry-run=client -o yaml` để sinh manifest rồi dán vào thư mục static pod.

Nếu ba thứ trên còn mờ, mở lại bài 1.3 trước khi đọc tiếp, vì bài này thao tác liên tục trên file YAML.

## Lý thuyết

### Bức tranh tổng thể

```mermaid
flowchart TD
  subgraph cp["Control plane node"]
    API["🧠<br/>kube-apiserver"]
    ETCD["🗄️<br/>etcd"]
    SCHED["📅<br/>kube-scheduler"]
    CM["🔁<br/>kube-controller-manager"]
  end
  subgraph wn["Worker node"]
    KL["⚙️<br/>kubelet"]
    CR["📦<br/>containerd"]
    KP["🌐<br/>kube-proxy"]
    PODS["🚀<br/>Pods"]
  end
  ADMIN["⌨️<br/>kubectl"] --> API
  API <--> ETCD
  SCHED --> API
  CM --> API
  KL --> API
  KL --> CR
  CR --> PODS
  API --> KP
```

Điểm cần khắc vào đầu: ==mọi thứ trong cluster đều đi qua kube-apiserver==. kubectl, scheduler, controller manager, kubelet, kube-proxy, tất cả đều là client của apiserver. Không có đường tắt.

### Control plane gồm những gì

| Component | Nhiệm vụ | Nếu chết thì sao |
| --- | --- | --- |
| kube-apiserver | Cửa ngõ duy nhất, xử lý mọi request, xác thực, ghi vào etcd | `kubectl` chết ngay, không deploy được gì |
| etcd | Key-value store giữ toàn bộ trạng thái cluster | Mất dữ liệu cluster, apiserver lỗi |
| kube-scheduler | Quyết định pod chạy ở node nào | Pod mới ở trạng thái Pending |
| kube-controller-manager | Chạy các controller (Deployment, Node, Job, ...) | Replica không được tạo lại, node không bị đánh dấu NotReady |
| cloud-controller-manager | Tích hợp với cloud provider (nếu có) | Không có trên cluster lab |

Bạn sẽ gặp lại từng component này trong đề thi, dưới dạng "scheduler đang down, sửa đi" hoặc "tại sao pod Pending". Nhớ bảng trên là đủ để đoán bệnh.

### Node gồm những gì

- **kubelet**: agent chạy trên mỗi node, nhận PodSpec từ apiserver rồi bảo container runtime khởi động container. Kubelet cũng là người đọc static pod manifest từ đĩa.
- **container runtime**: thường là containerd. Đây là lý do trong thi bạn dùng `crictl`, không dùng `docker ps`.
- **kube-proxy**: lập trình iptables/IPVS để Service hoạt động (sẽ học ở Module 4).

### Static pod là gì và vì sao phải học

Static pod là pod do **kubelet** quản lý trực tiếp, không qua apiserver, không qua scheduler. Kubelet quét thư mục manifest (mặc định `/etc/kubernetes/manifests`), thấy file YAML thì tạo container từ file đó, xoá file thì xoá container.

Thư mục manifest của kubelet được khai báo trong file cấu hình kubelet:

```yaml
staticPodPath: /etc/kubernetes/manifests
```

Có một vấn đề: nếu bạn không thấy static pod qua `kubectl get pods`, bạn sẽ không bao giờ biết nó tồn tại. Vì vậy kubelet tạo thêm một **mirror pod** trên apiserver, cùng tên nhưng có hậu tố là tên node, kèm annotation `kubernetes.io/config.mirror`. Mirror pod chỉ để hiển thị. Sửa hay xoá nó đều vô nghĩa, kubelet sẽ đè lại ngay.

Một vài đặc điểm cần nhớ:

- Static pod luôn gắn với node chứa file manifest, không bao giờ bị scheduler di chuyển.
- Tên pod trong apiserver có dạng `<tên-trong-file>-<tên-node>`. Ví dụ file khai báo `metadata.name: kube-apiserver`, node tên `cp1` thì pod hiện ra là `kube-apiserver-cp1`.
- Static pod **không** dùng được ConfigMap, Secret hay ServiceAccount của cluster (vì nó không đi qua apiserver để resolve).
- Kubelet kiểm tra lại thư mục manifest khoảng mỗi 20 giây, nên sửa file xong phải đợi một chút mới thấy pod restart.

### Vì sao control plane lại chạy dạng static pod

Đây là chỗ hay bị hỏi và cũng rất logic. kubeadm cần kubelet khởi động kube-apiserver trước khi có bất kỳ API nào. Nếu apiserver phải chờ apiserver thì không bao giờ khởi động được. Giải pháp: kubeadm ghi manifest vào `/etc/kubernetes/manifests`, kubelet đọc file và chạy container luôn. Không cần apiserver, không cần scheduler, không cần etcd đã sẵn sàng từ trước.

Một cluster dựng bằng kubeadm có 4 static pod ở control plane node:

```tree
/etc/kubernetes/
  manifests/
    etcd.yaml
    kube-apiserver.yaml
    kube-controller-manager.yaml
    kube-scheduler.yaml
  kubelet.conf
  admin.conf
/var/lib/kubelet/
  config.yaml
```

Trong bài này mình chỉ cần bạn xác định đúng file. Nội dung etcd và backup sẽ sang bài 2.3.

Điền nhanh để tự kiểm tra:

- Pod static được kubelet đọc trực tiếp từ thư mục {{c1::/etc/kubernetes/manifests}}.
- Mọi thao tác của kubectl đều đi qua {{c2::kube-apiserver}}.
- Trên node tên `cp1`, file `kube-scheduler.yaml` hiện ra trong apiserver với tên pod là {{c3::kube-scheduler-cp1}}.
- Component giữ toàn bộ trạng thái cluster dưới dạng key-value là {{c4::etcd}}.
- Trên cluster dùng containerd, lệnh liệt kê container đang chạy trên node là {{c5::crictl ps}}.
- Kubelet đọc cấu hình chính từ file {{c6::/var/lib/kubelet/config.yaml}}.

## Ví dụ từng bước

### Ví dụ 1: Đọc sơ đồ cluster thật

Mục tiêu: từ bên ngoài, xác định được node nào là control plane, component nào đang chạy ở đâu.

```bash
# 1. Xem node và role
kubectl get nodes -o wide

# 2. Xem toàn bộ pod hệ thống kèm node
kubectl get pods -n kube-system -o wide

# 3. Lọc riêng các pod trên control plane node
kubectl get pods -n kube-system --field-selector spec.nodeName=cp1
```

Kết quả bước 3 thường có 4 dòng: `etcd-cp1`, `kube-apiserver-cp1`, `kube-controller-manager-cp1`, `kube-scheduler-cp1`. Hậu tố `-cp1` chính là dấu hiệu đây là mirror pod của static pod.

Kiểm tra bằng chứng:

```bash
kubectl -n kube-system get pod kube-apiserver-cp1 \
  -o jsonpath='{.metadata.annotations.kubernetes\.io/config\.mirror}'
```

Có output (một chuỗi hash) nghĩa là mirror pod. Không có output nghĩa là pod thường do scheduler xếp chỗ.

### Ví dụ 2: Tạo một static pod mới trên node

Đề thi dạng này rất hay gặp: "Tạo static pod tên `web` chạy image `nginx` trên node `node01`".

```bash
# Bước 1: ssh vào đúng node
ssh node01

# Bước 2: xem thư mục manifest trước khi làm gì
ls -l /etc/kubernetes/manifests/

# Bước 3: tạo file YAML tối giản, tên file không quan trọng
vi /etc/kubernetes/manifests/web.yaml
```

Nội dung file:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    app: web
spec:
  containers:
  - name: nginx
    image: nginx:1.25
```

```bash
# Bước 4: đợi kubelet nhận file (khoảng 20 giây), rồi kiểm tra từ máy có kubectl
kubectl get pods -A | grep web
```

Kết quả mong đợi: `web-node01`. Lưu ý tên pod có hậu tố node, không phải `web`. Nếu đề bài yêu cầu "pod tên web", bạn cần đọc kỹ xem họ chấp nhận hậu tố hay không; thực tế CKA chấp nhận mirror pod có hậu tố.

Muốn xoá: `rm /etc/kubernetes/manifests/web.yaml`, **không** dùng `kubectl delete pod web-node01`.

### Ví dụ 3: Sửa manifest và xử lý khi làm hỏng

Tình huống: ai đó sửa `kube-scheduler.yaml`, gõ sai đường dẫn kubeconfig. Scheduler chết, pod mới tạo ra nằm Pending.

```bash
# 1. Nhìn triệu chứng
kubectl get pods -n kube-system | grep scheduler
# kube-scheduler-cp1   0/1   CrashLoopBackOff

kubectl get events -A --sort-by=.lastTimestamp | tail -5
# FailedScheduling ... no nodes available

# 2. Vào node, backup trước khi sửa. Đây là bước đừng bao giờ bỏ
ssh cp1
cp /etc/kubernetes/manifests/kube-scheduler.yaml \
   /etc/kubernetes/manifests/kube-scheduler.yaml.bak

# 3. Xem chỗ sai
grep -n "kubeconfig" /etc/kubernetes/manifests/kube-scheduler.yaml
# thấy: - --kubeconfig=/etc/kubernetes/scheduler-config.conf  (sai)

# 4. Sửa lại thành /etc/kubernetes/scheduler.conf
vi /etc/kubernetes/manifests/kube-scheduler.yaml

# 5. Đợi kubelet restart, kiểm tra
kubectl get pods -n kube-system | grep scheduler
# kube-scheduler-cp1   1/1   Running
```

Bài học rút ra: scheduler chết **không** làm cluster sập. Cluster vẫn chạy pod cũ, chỉ là pod mới không được xếp chỗ. Ngược lại, nếu bạn làm hỏng `kube-apiserver.yaml` thì `kubectl` chết hoàn toàn, và bạn phải sửa qua SSH. Đó là lý do luôn backup.

## Luyện tập có hướng dẫn

Làm trên cluster lab của bạn. Mỗi bài tự làm trước, chỉ mở đáp án khi đã thử.

**Bài 1.** Liệt kê tên 4 static pod của control plane và cho biết mỗi pod chạy trên node nào. Không dùng `kubectl describe`.

<details><summary>Xem gợi ý</summary>

```bash
kubectl get pods -n kube-system -o wide | grep -E "etcd|apiserver|controller-manager|scheduler"
```

Cột `NODE` cho biết vị trí, hậu tố tên pod cho biết tên node gốc.

</details>

**Bài 2.** Chứng minh rằng `kube-apiserver-<node>` là mirror pod, dùng jsonpath, không dùng `describe`.

<details><summary>Xem đáp án</summary>

```bash
kubectl -n kube-system get pod kube-apiserver-cp1 \
  -o jsonpath='{.metadata.annotations.kubernetes\.io/config\.mirror}'
```

Có giá trị trả về là mirror pod.

</details>

**Bài 3.** Tạo static pod tên `logger` chạy image `busybox:1.36`, command là `sh -c "while true; do date; sleep 5; done"` trên node `node01`. Sau đó xem log của nó bằng `crictl`, không dùng `kubectl logs`.

<details><summary>Xem đáp án</summary>

```bash
ssh node01
vi /etc/kubernetes/manifests/logger.yaml
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: logger
spec:
  containers:
  - name: logger
    image: busybox:1.36
    command: ["sh", "-c", "while true; do date; sleep 5; done"]
```

```bash
# đợi ~20 giây
crictl ps | grep logger
crictl logs <container-id>
```

Lưu ý: `kubectl logs logger-node01` vẫn chạy được, nhưng đề bài yêu cầu `crictl` để bạn quen với công cụ trên node.

</details>

**Bài 4.** Sửa `kube-controller-manager` để bật log level 4 bằng flag `--v=4`. Xác nhận thay đổi có hiệu lực.

<details><summary>Xem gợi ý</summary>

```bash
ssh cp1
cp /etc/kubernetes/manifests/kube-controller-manager.yaml /tmp/kcm.bak
vi /etc/kubernetes/manifests/kube-controller-manager.yaml
```

Thêm dòng `- --v=4` vào mảng `command`, cùng cấp với các flag khác. Đợi kubelet restart rồi kiểm tra:

```bash
kubectl -n kube-system get pod kube-controller-manager-cp1 -o yaml | grep -- "--v=4"
```

Sau khi xong, khôi phục từ `/tmp/kcm.bak` để cluster về trạng thái sạch.

</details>

**Bài 5.** Giải thích tại sao `kubectl delete pod kube-scheduler-cp1` không xoá được scheduler, và mô tả đúng hai cách để tắt nó.

<details><summary>Xem đáp án</summary>

Vì `kube-scheduler-cp1` là mirror pod. Kubelet vẫn còn file manifest, nên nó tạo lại container và mirror pod ngay.

Hai cách tắt đúng:

1. Xoá hoặc đổi tên file `/etc/kubernetes/manifests/kube-scheduler.yaml`.
2. Chuyển file ra ngoài thư mục manifest, ví dụ `mv kube-scheduler.yaml /root/`.

Cả hai đều phải làm qua SSH trên node chứa file.

</details>

**Bài 6.** Tìm `staticPodPath` đang được kubelet dùng trên node của bạn.

<details><summary>Xem đáp án</summary>

```bash
ssh cp1
grep staticPodPath /var/lib/kubelet/config.yaml
```

Kết quả mặc định: `/etc/kubernetes/manifests`. Nếu ai đó đổi giá trị này, kubelet sẽ đọc manifest ở thư mục khác, và đây là một bẫy thi khá khó chịu.

</details>

## Góc chuyên gia

Vài thứ mình ước mình biết sớm hơn:

- **Đừng bao giờ sửa mirror pod.** `kubectl edit pod kube-apiserver-cp1` sẽ trả về lỗi hoặc bị ghi đè ngay. Luôn sửa file trong `/etc/kubernetes/manifests`.
- **Backup trước khi sửa, luôn luôn.** Một lệnh `cp file.yaml file.yaml.bak` mất 2 giây, cứu bạn 10 phút trong phòng thi. Riêng `kube-apiserver.yaml` thì backup là bắt buộc.
- **kubelet không phản ứng tức thì.** Sau khi sửa file, chờ khoảng 20 giây. Dùng `watch kubectl -n kube-system get pods` để theo dõi thay vì đoán.
- **Sửa `kube-apiserver.yaml` sai là mất kubectl.** Lúc đó bạn chỉ còn SSH vào node, sửa file, hoặc chép lại từ `.bak`. Trong đề thi đây là kiểu tự bắn vào chân phổ biến nhất.
- **Thứ tự ưu tiên khi control plane có vấn đề:** etcd, rồi kube-apiserver, rồi controller-manager, rồi scheduler. Càng lên trên càng nghiêm trọng.
- **Không dùng `docker ps`.** Cluster CKA dùng containerd. Dùng `crictl ps`, `crictl logs`, `crictl pods`. Nếu `crictl` báo thiếu endpoint, kiểm tra `/var/lib/kubelet/config.yaml` hoặc chạy với `--runtime-endpoint unix:///var/run/containerd/containerd.sock`.
- **Đọc đề trước khi ssh.** Nhiều bạn ssh vào node rồi mới phát hiện đề yêu cầu làm từ máy có kubectl. Đọc đề 10 giây tiết kiệm 1 phút.
- **Đặt giới hạn thời gian.** Bài về static pod không nên quá 4 phút. Nếu quá, đánh dấu, làm bài khác, quay lại.

> [!TIP]
> Câu thần chú khi gặp bài "control plane component bị lỗi": vào node, `ls /etc/kubernetes/manifests`, backup, sửa file, chờ 20 giây, kiểm tra bằng `kubectl -n kube-system get pods`.

## Thẻ ôn tập nhanh

```flashcard
[
  { "front": "Control plane gồm những component nào?", "back": "kube-apiserver, etcd, kube-scheduler, kube-controller-manager (và cloud-controller-manager nếu chạy trên cloud)." },
  { "front": "Component nào là cửa ngõ duy nhất mà kubectl nói chuyện tới?", "back": "kube-apiserver. Mọi client trong cluster đều đi qua nó." },
  { "front": "Static pod do component nào quản lý?", "back": "kubelet, đọc trực tiếp file manifest trên đĩa, không qua scheduler hay apiserver." },
  { "front": "Thư mục manifest static pod mặc định của kubelet?", "back": "/etc/kubernetes/manifests, khai báo qua staticPodPath trong /var/lib/kubelet/config.yaml." },
  { "front": "Sửa file manifest static pod xong thì cần làm gì để áp dụng?", "back": "Không cần gì thêm, kubelet tự phát hiện và restart container trong khoảng 20 giây." },
  { "front": "Mirror pod là gì?", "back": "Pod đại diện do kubelet tạo trên apiserver để hiển thị static pod, có annotation kubernetes.io/config.mirror." },
  { "front": "Static pod khai báo name: web, node tên node01. Pod hiện ra trong apiserver tên gì?", "back": "web-node01, tức tên trong file cộng thêm hậu tố tên node." },
  { "front": "Xoá vĩnh viễn một static pod bằng cách nào?", "back": "Xoá hoặc di chuyển file manifest khỏi /etc/kubernetes/manifests. kubectl delete chỉ xoá mirror pod và kubelet tạo lại." },
  { "front": "Trên node dùng containerd, lệnh nào liệt kê container đang chạy?", "back": "crictl ps (không phải docker ps)." },
  { "front": "File cấu hình chính của kubelet nằm ở đâu?", "back": "/var/lib/kubelet/config.yaml, kèm theo /etc/kubernetes/kubelet.conf." },
  { "front": "Component nào quyết định pod chạy trên node nào?", "back": "kube-scheduler. Nếu nó chết, pod mới sẽ ở trạng thái Pending." },
  { "front": "Hậu quả khi sửa hỏng file kube-apiserver.yaml?", "back": "Mất kết nối API, kubectl không dùng được. Phải SSH vào node sửa file hoặc chép lại từ bản backup." },
  { "front": "Vì sao control plane lại chạy dưới dạng static pod?", "back": "Để kubelet khởi động được apiserver trước khi có API, giải quyết vòng lặp con gà và quả trứng khi bootstrap cluster." },
  { "front": "Controller nào giữ đúng số replica của một Deployment?", "back": "kube-controller-manager, nơi chạy các controller vòng lặp điều chỉnh trạng thái." }
]
```

## Tóm tắt và bước tiếp theo

Tóm gọn bài này trong 4 câu:

1. Cluster chia làm control plane (apiserver, etcd, scheduler, controller-manager) và node (kubelet, container runtime, kube-proxy).
2. kube-apiserver là trung tâm, mọi thứ khác là client của nó.
3. Control plane chạy dưới dạng static pod vì kubelet phải khởi động apiserver trước khi có API.
4. Static pod đọc từ `/etc/kubernetes/manifests`, hiện ra ngoài dưới dạng mirror pod có hậu tố tên node, và chỉ bị xoá vĩnh viễn khi xoá file.

Bài tiếp theo, **kubeadm init cluster và join node**, sẽ dùng chính bức tranh này: khi bạn chạy `kubeadm init`, nó sinh ra đúng thư mục `/etc/kubernetes/manifests` mà bạn vừa học, rồi sinh certificate, kubeconfig và token join. Bạn sẽ thấy 4 file YAML kia xuất hiện từ đâu và node mới join vào bằng cách nào. Trước khi sang bài đó, hãy chắc rằng bạn làm được bài 1 và bài 2 ở phần luyện tập mà không cần mở đáp án.
