# 🧭 Kiến trúc control plane, node và đường đi của một lệnh kubectl

## Mục tiêu học tập

- Kể tên và mô tả đúng vai trò của bốn thành phần control plane: `kube-apiserver`, `etcd`, `kube-scheduler`, `kube-controller-manager`.
- Mô tả vai trò của `kubelet`, `kube-proxy` và container runtime trên một node.
- Vẽ lại được đường đi của một lệnh `kubectl` từ máy bạn tới container đang chạy, theo đúng thứ tự.
- Nhìn trạng thái Pod (`Pending`, `ContainerCreating`, `Running`, `CrashLoopBackOff`) và đoán được thành phần nào đang gặp vấn đề.

## Ôn nhanh (2 phút)

Bạn đã biết container: image là khuôn, container là tiến trình đang chạy, được cô lập bằng namespace và cgroup của Linux. Kubernetes **không** tạo container trực tiếp. Nó giao việc đó cho container runtime (containerd, CRI-O) qua chuẩn **CRI** (Container Runtime Interface). Nói cách khác: kubelet là người ra lệnh, runtime là người thi hành.

> [!NOTE]
> Chỉ có `kube-apiserver` được phép nói chuyện với `etcd`. Mọi thành phần khác, kể cả `kubectl`, đều đi qua API server. Đây là câu hỏi phỏng vấn kinh điển.

## Lý thuyết

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

Một cluster gồm **control plane** (bộ não, ra quyết định) và các **node** (cơ bắp, chạy container thật).

```mermaid
flowchart LR
  subgraph cp["Control plane"]
    API["🧠<br/>kube-apiserver"]
    ETCD["🗄️<br/>etcd"]
    SCH["📅<br/>kube-scheduler"]
    CM["🔁<br/>kube-controller-manager"]
  end
  subgraph wk["Worker node"]
    KL["🤖<br/>kubelet"]
    CR["📦<br/>container runtime"]
    POD["🟢<br/>Pod"]
    KP["🔀<br/>kube-proxy"]
  end
  KUBECTL["⌨️<br/>kubectl"] --> API
  API <--> ETCD
  CM --> API
  SCH --> API
  API --> KL
  KL --> CR
  CR --> POD
  KP --> POD
```

### Control plane

| Thành phần | Vai trò | Câu ghi nhớ |
| --- | --- | --- |
| `kube-apiserver` | Cổng vào duy nhất, xử lý authn/authz/admission, đọc ghi `etcd` | Không có nó thì cluster "câm" |
| `etcd` | Key-value store phân tán, nguồn sự thật duy nhất | Mất `etcd` là mất cluster |
| `kube-scheduler` | Chọn node cho Pod chưa được gán | Chỉ quyết định "ở đâu", không tạo gì cả |
| `kube-controller-manager` | Chạy các vòng reconcile (Deployment, ReplicaSet, Node, Job...) | Luôn kéo trạng thái thật về trạng thái mong muốn |

`kube-apiserver` là stateless nên bạn có thể chạy nhiều bản sao sau load balancer. `etcd` thì ngược lại: cần **quorum**, cụm 3 node chịu được 1 node hỏng, cụm 2 node thì không chịu được node nào.

### Node

- **kubelet**: agent chạy trên mỗi node. Nó watch API server để tìm Pod có `spec.nodeName` trùng tên node mình, rồi gọi runtime qua CRI để pull image và start container. Sau đó báo status ngược lại.
- **kube-proxy**: lập trình `iptables` hoặc `ipvs` để traffic vào Service VIP được chuyển tới Pod IP phía sau.
- **Container runtime**: containerd/CRI-O, thực thi việc tạo container thật.

### Đường đi của một lệnh kubectl

```steps
1. `kubectl` đọc `~/.kube/config`, lấy địa chỉ API server và thông tin xác thực (cert hoặc token).
2. `kubectl` dựng HTTP request (ví dụ `POST /apis/apps/v1/namespaces/default/deployments`) và gửi tới `kube-apiserver`.
3. API server chạy **authentication**: bạn là ai?
4. API server chạy **authorization** (RBAC): bạn có được phép `create` Deployment không?
5. **Admission controllers** chạy: gán giá trị mặc định (mutating), kiểm tra ràng buộc (validating), ví dụ giới hạn resource, chặn image từ registry lạ.
6. Object được ghi vào `etcd`, API server trả về `201 Created` cho `kubectl`.
7. Controller tương ứng (Deployment controller) đang watch, thấy object mới và tạo `ReplicaSet`.
8. ReplicaSet controller tạo các Pod, mỗi Pod chưa có `spec.nodeName`.
9. `kube-scheduler` thấy Pod "trần", chạy filter (đủ CPU/RAM, taint, affinity) rồi score, chọn node và ghi binding.
10. `kubelet` trên node đó thấy Pod được gán, pull image, tạo container, cập nhật status: `Pending` → `ContainerCreating` → `Running`.
11. Khi Pod Ready, EndpointSlice được cập nhật; `kube-proxy` trên các node đọc và sửa rules để traffic vào Service.
```

Điểm cốt lõi: ==mọi thứ là bất đồng bộ và dựa trên vòng reconcile==. `kubectl` chỉ ghi mong muốn vào `etcd`, không ai "ra lệnh" trực tiếp cho container cả.

## Ví dụ minh họa

Chạy `kubectl create deployment web --image=nginx --replicas=2` rồi lần theo từng bước:

1. `kubectl` gửi `POST .../deployments` tới API server.
2. API server xác thực, kiểm tra RBAC, chạy admission, ghi vào `etcd`, trả `201`.
3. Deployment controller tạo ReplicaSet tên `web-7d9c...`.
4. ReplicaSet controller tạo 2 Pod `web-7d9c-abcde` và `web-7d9c-fghij`, `nodeName` rỗng.
5. Scheduler chấm điểm, gán cả hai vào `node-1`.
6. kubelet trên `node-1` pull `nginx`, start container, báo status `Running`.
7. `kubectl get pods` sau đó chỉ đọc lại từ `etcd` qua API server, không hỏi kubelet.

Kiểm chứng bằng event, đây là nơi các thành phần "ký tên":

```bash
kubectl get pods -w
kubectl describe pod web-7d9c-abcde | grep -A10 Events
```

Bạn sẽ thấy `Scheduled` (do scheduler) đứng trước `Pulled`, `Created`, `Started` (do kubelet). Đó chính là bằng chứng của ranh giới trách nhiệm.

> [!TIP]
> Muốn thấy request thật mà `kubectl` gửi đi: `kubectl get pods -v=8`. Rất hữu ích khi debug lỗi 403 RBAC hoặc API version.

## Thực hành có hướng dẫn

**Bài 1: Sắp xếp luồng.** Sắp xếp lại cho đúng thứ tự: (a) scheduler gán node, (b) API server ghi vào etcd, (c) kubelet start container, (d) ReplicaSet controller tạo Pod, (e) `kubectl` gửi request.

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

e → b → d → a → c. Lưu ý: controller tạo Pod **trước** khi scheduler gán node, vì Pod sinh ra với `nodeName` rỗng.

</details>

**Bài 2: Quan sát bằng mắt.** Trên cluster local (kind/minikube), chạy:

```bash
kubectl create deployment api --image=nginx --replicas=3
kubectl get pods -o wide
kubectl describe pod <một-pod> | sed -n '/Events/,$p'
```

Ghi lại 3 loại event và chỉ rõ event nào do scheduler, event nào do kubelet sinh ra.

<details><summary>Gợi ý</summary>

`Scheduled` = scheduler. `Pulled`, `Created`, `Started` = kubelet + runtime. Nếu có `FailedScheduling` nghĩa là Pod chưa từng tới được node nào.

</details>

**Bài 3: Truy lỗi.** Ba tình huống, hãy đoán thành phần hỏng và trạng thái Pod tương ứng:

1. `kube-scheduler` chết.
2. `kube-controller-manager` chết.
3. `kubelet` trên một node chết.

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

1. Deployment và ReplicaSet vẫn tạo Pod, nhưng Pod kẹt ở `Pending` vì không ai gán node.
2. Deployment được tạo nhưng không có ReplicaSet, hoặc scale không xảy ra: `kubectl get rs` trống. Pod đang chạy vẫn chạy tiếp.
3. Node chuyển `NotReady` sau vài chu kỳ heartbeat, Pod trên node đó không được cập nhật status và dần bị đánh dấu lỗi; tài nguyên thì không giải phóng ngay.

</details>

## Góc chuyên gia

- **Đừng sửa `etcd` bằng tay.** Mọi thay đổi phải qua API server để admission và controller còn thấy được.
- **`kubectl get` đọc `etcd`, không đọc node.** Trạng thái có thể trễ vài giây so với thực tế. Đừng hoảng khi thấy Pod `Running` mà app chưa lên.
- **`Pending` là chuyện của scheduler, `ContainerCreating` là chuyện của kubelet.** Phân biệt được hai cái này là tiết kiệm hàng giờ debug.
- **`kubectl logs`, `exec`, `port-forward` đi qua API server rồi mới tới kubelet.** Vì vậy API server quá tải có thể làm `exec` chậm, dù app hoàn toàn khỏe.
- **Control plane mặc định bị taint `NoSchedule`**, nên đừng ngạc nhiên khi node control plane không chạy Pod workload.
- **Quy tắc ngón tay cái về `etcd`:** luôn chạy 3 hoặc 5 thành viên, backup snapshot định kỳ, và không đặt `etcd` chung ổ đĩa IOPS thấp với workload nặng.

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

Bạn vừa đi hết một vòng: `kubectl` → API server (authn, authz, admission) → `etcd` → controller tạo Pod → scheduler gán node → kubelet start container → kube-proxy mở đường. Mọi thứ nối với nhau bằng watch và vòng reconcile, không phải bằng lệnh trực tiếp.

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

```cloze
Thành phần duy nhất giao tiếp trực tiếp với etcd là {{c1::kube-apiserver}}.
{{c2::kube-scheduler}} chọn node cho Pod qua hai pha filter và score.
Trên node, {{c3::kubelet}} nhận Pod đã được gán và gọi CRI để tạo container.
{{c4::kube-controller-manager}} chạy các vòng reconcile, ví dụ tạo ReplicaSet từ Deployment.
Nếu etcd mất {{c5::quorum}}, cluster sẽ không nhận được thay đổi nào nữa.
```

**Bài tiếp theo:** Pod, ReplicaSet, Deployment. Chúng ta sẽ mổ xẻ chính cái vòng reconcile vừa nhắc ở trên: Deployment quản lý ReplicaSet thế nào, rollout và rollback diễn ra ra sao, và scheduler ra quyết định dựa trên những tiêu chí gì.

```flashcard
[
  { "front": "Điểm vào duy nhất của mọi lệnh kubectl là gì?", "back": "kube-apiserver; kubectl không bao giờ nói chuyện trực tiếp với node." },
  { "front": "Thành phần nào lưu trạng thái thật của cluster?", "back": "etcd, key-value store phân tán và nhất quán." },
  { "front": "Ai là thành phần duy nhất được giao tiếp với etcd?", "back": "kube-apiserver." },
  { "front": "kube-scheduler làm gì?", "back": "Chọn node cho Pod chưa có nodeName bằng filter rồi score, sau đó ghi binding." },
  { "front": "kube-controller-manager làm gì?", "back": "Chạy các controller trong vòng reconcile để kéo trạng thái thật về trạng thái mong muốn." },
  { "front": "kubelet làm gì?", "back": "Agent trên node, watch Pod được gán cho node mình, gọi container runtime qua CRI và báo status về API server." },
  { "front": "kube-proxy làm gì?", "back": "Lập trình iptables hoặc IPVS để traffic tới Service VIP được chuyển tới Pod IP." },
  { "front": "Pod kẹt ở Pending thường do đâu?", "back": "Chưa được scheduler gán node, hoặc không node nào thỏa điều kiện lọc." },
  { "front": "Pod ở ContainerCreating lâu thường do đâu?", "back": "kubelet đang pull image hoặc mount volume, không phải lỗi scheduler." },
  { "front": "Admission controller chạy ở đâu trong luồng?", "back": "Trong kube-apiserver, sau authentication và authorization, trước khi ghi vào etcd." },
  { "front": "Cụm etcd 3 node chịu được mấy node hỏng?", "back": "1 node, vì cần quorum 2/3." },
  { "front": "Vì sao kubectl get pods có thể trả về trạng thái hơi cũ?", "back": "Vì nó đọc từ etcd qua API server, không hỏi trực tiếp kubelet trên node." },
  { "front": "Lệnh nào in ra chi tiết HTTP request mà kubectl gửi?", "back": "Thêm cờ -v=8, ví dụ kubectl get pods -v=8." },
  { "front": "Control plane có chạy workload mặc định không?", "back": "Không, thường bị taint NoSchedule để chỉ chạy thành phần hệ thống." }
]
```
