# ⬆️ Upgrade cluster bằng kubeadm và drain node

Cluster của bạn đang chạy v1.30, đề thi yêu cầu nâng lên v1.31. Nghe thì đơn giản, nhưng đây là dạng bài mà người mới hay mất điểm nhất, không phải vì lệnh khó, mà vì **sai thứ tự** hoặc **quên uncordon**. Bài này tôi đi qua đúng trình tự tôi vẫn làm trong phòng thi, kèm những chỗ tôi từng làm hỏng cluster lab của mình.

## Ôn lại nhanh

Bài trước bạn đã biết cách `etcdctl snapshot save` và restore. Nhắc lại một câu: **backup etcd là bước 0 bắt buộc trước khi upgrade**. Nếu `kubeadm upgrade apply` hỏng giữa chừng và control plane không lên được, snapshot etcd là đường lui duy nhất của bạn.

> [!WARNING]
> Trong môi trường thi thật, bạn thường không cần backup etcd vì cluster lab dùng được ngay. Nhưng nếu đề có yêu cầu backup trước khi upgrade, hãy làm trước, đừng làm sau.

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

Sau bài này, bạn phải làm được:

1. Đọc đúng `kubeadm upgrade plan` và xác định phiên bản đích hợp lệ (chỉ nhảy **một** minor version).
2. Nâng cấp control plane node đúng thứ tự: kubeadm, apply, drain, kubelet, uncordon.
3. Nâng cấp từng worker node bằng `kubeadm upgrade node` và đưa node về trạng thái phục vụ.
4. Xử lý được 4 lỗi drain/upgrade thường gặp nhất trong phòng thi.

## Nguyên tắc không được phá

| Nguyên tắc | Chi tiết |
| --- | --- |
| Thứ tự | Control plane trước, worker sau, mỗi lần một node |
| Số minor | Mỗi lần chỉ nâng {{c1::một}} minor version (1.30 → 1.31, không nhảy 1.30 → 1.32) |
| Version skew | kubelet không được mới hơn kube-apiserver, chỉ được cũ hơn tối đa 3 minor; kubectl lệch tối đa 1 minor |
| Ai nâng gì | `kubeadm upgrade` nâng control plane components và image; **kubelet/kubectl do bạn cài bằng package manager** |

```mermaid
flowchart TD
    Start["🚀 Bắt đầu"] --> Plan["📋 kubeadm upgrade plan"]
    Plan --> CP1["🖥️ Control plane đầu tiên<br/>kubeadm upgrade apply"]
    CP1 --> CPN["🖥️ Control plane còn lại<br/>kubeadm upgrade node"]
    CPN --> W["⚙️ Từng worker node<br/>drain → upgrade node → uncordon"]
    W --> Done["✅ kubectl get nodes"]
```

## Ví dụ mẫu: nâng cấp cụm 1 control plane + 2 worker

Bối cảnh: `controlplane`, `node01`, `node02` đều đang chạy v1.30.5, mục tiêu là v1.31.4. Bạn đang đứng trên `controlplane`.

### Bước 1: Kiểm tra hiện trạng

```bash
kubectl get nodes -o wide
kubeadm version -o short
kubectl version --short
```

Ghi lại phiên bản hiện tại. Nếu các node lệch nhau sẵn (ví dụ `node02` còn 1.29), xử lý node đó trước.

### Bước 2: Xem phiên bản có thể cài

```bash
apt-get update
apt-cache madison kubeadm | head
```

Bạn sẽ thấy các dòng kiểu `kubeadm | 1.31.4-1.1 | https://pkgs.k8s.io/...`. Ghi nhớ chuỗi `1.31.4-1.1`, đó là tên package đầy đủ cần dùng.

### Bước 3: Nâng kubeadm trên control plane đầu tiên

```bash
apt-mark unhold kubeadm
apt-get install -y kubeadm=1.31.4-1.1
apt-mark hold kubeadm
kubeadm version
```

Nếu package đang bị hold mà bạn quên `unhold`, apt sẽ báo lỗi và gợi ý `--allow-change-held-packages`. Cả hai cách đều được.

### Bước 4: Xem kế hoạch

```bash
kubeadm upgrade plan
```

Đọc kỹ output: nó liệt kê component nào sẽ lên version nào, và nhắc rằng **kubelet phải nâng thủ công**. Nếu thấy dòng cảnh báo version skew, dừng lại kiểm tra trước khi apply.

### Bước 5: Nâng cấp control plane

```bash
kubeadm upgrade apply v1.31.4
```

Chờ tới dòng `[upgrade/successful] SUCCESS!`. Lệnh này restart các static pod `kube-apiserver`, `kube-controller-manager`, `kube-scheduler`, `etcd` bằng image mới. Nếu fail giữa chừng, đọc log, sửa nguyên nhân rồi chạy lại, lệnh này tương đối an toàn khi chạy lại.

### Bước 6: Drain control plane node

```bash
kubectl drain controlplane --ignore-daemonsets
```

### Bước 7: Nâng kubelet và kubectl

```bash
apt-mark unhold kubelet kubectl
apt-get install -y kubelet=1.31.4-1.1 kubectl=1.31.4-1.1
apt-mark hold kubelet kubectl
systemctl daemon-reload
systemctl restart kubelet
```

### Bước 8: Đưa control plane về phục vụ

```bash
kubectl uncordon controlplane
kubectl get nodes
```

`controlplane` phải hiện `v1.31.4` và không còn chữ `SchedulingDisabled`.

### Bước 9: Nâng worker `node01`

Từ control plane, đẩy pod ra khỏi node:

```bash
kubectl drain node01 --ignore-daemonsets --delete-emptydir-data
```

SSH vào `node01` rồi làm 3 việc:

```bash
apt-mark unhold kubeadm && apt-get install -y kubeadm=1.31.4-1.1 && apt-mark hold kubeadm
kubeadm upgrade node
apt-mark unhold kubelet kubectl && apt-get install -y kubelet=1.31.4-1.1 kubectl=1.31.4-1.1 && apt-mark hold kubelet kubectl
systemctl daemon-reload && systemctl restart kubelet
```

Quay lại control plane:

```bash
kubectl uncordon node01
kubectl get nodes
```

### Bước 10: Lặp lại với `node02` và kiểm tra cuối

```bash
kubectl get nodes
kubectl -n kube-system get pods
kubectl get pods -A | grep -v Running
```

Cả 3 node phải cùng version, mọi pod hệ thống `Running`, không còn node nào `SchedulingDisabled`.

> [!NOTE]
> Nếu cụm có nhiều control plane node, các node phụ dùng `kubeadm upgrade node` (không phải `apply`), giống worker. Chỉ node control plane **đầu tiên** mới dùng `kubeadm upgrade apply`.

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

Làm lần lượt, đừng mở đáp án trước khi thử. Mỗi bài đều có bước tự kiểm tra.

### Bài 1: Đọc hiện trạng và lập kế hoạch

1. Chạy `kubectl get nodes -o wide` và ghi ra giấy: tên node, role, version.
2. Chạy `kubectl get node <node> -o jsonpath='{.status.nodeInfo.kubeletVersion}'` cho từng node.
3. Chạy `kubeadm upgrade plan` và trả lời: version đích là gì, kubelet có tự lên theo không.

<details><summary>Gợi ý và đáp án</summary>

- `kubectl get nodes -o wide` cho bạn cột VERSION, nhưng dùng jsonpath thì chắc chắn hơn khi đề yêu cầu version chính xác của kubelet.
- `kubeadm upgrade plan` in ra bảng các component (`kube-apiserver`, `kube-controller-manager`, `kube-scheduler`, `kube-proxy`, `CoreDNS`) và version đích.
- kubelet **không** tự lên. Bạn phải cài package và restart kubelet thủ công.
- Nếu output có dòng `Unable to check for upgrade` hoặc lỗi mạng, kiểm tra repo `pkgs.k8s.io` trong `/etc/apt/sources.list.d/`.

</details>

### Bài 2: Drain an toàn một worker node

1. Chạy thử trước: `kubectl drain node01 --ignore-daemonsets --delete-emptydir-data --dry-run=client`.
2. Chạy thật, rồi kiểm tra trạng thái node.
3. Kiểm tra pod đã được chuyển sang node khác chưa.

```bash
kubectl get nodes
kubectl get pods -A -o wide | grep node01
```

<details><summary>Gợi ý và đáp án</summary>

- Sau khi drain, node hiện `SchedulingDisabled` trong cột STATUS, đó là bình thường.
- Pod còn lại trên node chỉ nên là DaemonSet pod (log agent, kube-proxy) và static pod nếu có.
- Nếu drain báo `cannot delete Pods with local storage`, thêm `--delete-emptydir-data`.
- Nếu drain báo `cannot delete DaemonSet-managed Pods`, thêm `--ignore-daemonsets`.
- Nếu có pod "trần" (không do controller quản lý), drain sẽ chặn, dùng `--force` sau khi chắc chắn pod đó bỏ được.

</details>

### Bài 3: Nâng cấp control plane node

Thực hiện Bước 3 đến Bước 8 ở ví dụ mẫu trên cluster lab của bạn. Tự kiểm tra bằng:

```bash
kubectl get nodes
kubectl -n kube-system get pods
```

<details><summary>Gợi ý và đáp án</summary>

- Sau `kubeadm upgrade apply`, các static pod trong `kube-system` sẽ restart, đợi 1 đến 2 phút rồi mới kết luận là lỗi.
- Nếu `kubectl get nodes` vẫn hiện version cũ cho control plane, gần như chắc chắn bạn quên `systemctl restart kubelet`.
- Kiểm tra nhanh image đang chạy: `kubectl -n kube-system get pod kube-apiserver-controlplane -o jsonpath='{.spec.containers[0].image}'`.

</details>

### Bài 4: Nâng cấp worker node và uncordon

Làm đủ 3 bước trên `node01`: drain từ control plane, `kubeadm upgrade node` + cài kubelet trên node, uncordon từ control plane.

<details><summary>Gợi ý và đáp án</summary>

- Nhớ `systemctl daemon-reload` trước `restart kubelet`, thiếu bước này kubelet có thể vẫn dùng binary cũ.
- Trên worker không bắt buộc cài `kubectl`, nhưng trong đề thi cứ cài cho đồng bộ.
- Sau `uncordon`, chạy `kubectl get nodes` tới khi node trả về `Ready` và version mới.

</details>

### Bài 5: Tình huống lỗi tổng hợp

Cluster lab của bạn đang ở trạng thái: `node02` hiện `SchedulingDisabled`, version vẫn 1.30.5, và `kubectl drain node02 --ignore-daemonsets` báo lỗi về local storage. Hãy đưa `node02` lên 1.31.4 và trở lại phục vụ.

<details><summary>Gợi ý và đáp án</summary>

Trình tự đúng:

1. `kubectl drain node02 --ignore-daemonsets --delete-emptydir-data` (thêm flag còn thiếu).
2. SSH vào `node02`, nâng `kubeadm`, chạy `kubeadm upgrade node`.
3. Nâng `kubelet` và `kubectl`, sau đó `systemctl daemon-reload && systemctl restart kubelet`.
4. `kubectl uncordon node02`.
5. Kiểm tra `kubectl get nodes`. Node phải `Ready` và `v1.31.4`, không còn `SchedulingDisabled`.

Lỗi hay gặp ở bài này: drain xong nhưng quên uncordon, hoặc uncordon sai node.

</details>

## Lỗi thường gặp và cách xử lý

| Triệu chứng | Nguyên nhân | Cách sửa |
| --- | --- | --- |
| `cannot delete DaemonSet-managed Pods` | Thiếu flag | Thêm `--ignore-daemonsets` |
| `cannot delete Pods with local storage` | Pod dùng emptyDir | Thêm `--delete-emptydir-data` |
| Drain treo ở pod không có controller | Pod "trần" | Thêm `--force` |
| Node cứ ở `SchedulingDisabled` | Chưa uncordon | `kubectl uncordon <node>` |
| `kubectl get nodes` vẫn version cũ | Chưa restart kubelet hoặc chưa cài package | `systemctl daemon-reload && systemctl restart kubelet` |
| `kubeadm upgrade apply` báo version skew | Nhảy quá một minor, hoặc kubelet mới hơn apiserver | Nâng tuần tự từng minor, control plane trước |
| Node `NotReady` sau upgrade | kubelet không khởi động được | `systemctl status kubelet`, `journalctl -u kubelet -n 50` |
| `kubeadm upgrade plan` lỗi kết nối | Repo package chưa đúng | Kiểm tra file trong `/etc/apt/sources.list.d/` |

> [!TIP]
> Trước khi drain thật, luôn chạy `kubectl drain <node> --dry-run=client` để biết ngay thiếu flag nào. Mất 5 giây, tiết kiệm vài phút.

## Góc chuyên gia

Vài thứ tôi ước mình biết trước khi thi:

- **Học thuộc 3 lệnh, không học thuộc 30 lệnh**: `kubeadm upgrade plan`, `kubeadm upgrade apply`, `kubeadm upgrade node`. Chỉ vậy thôi. Phần còn lại là `apt-get install` và `kubectl drain/uncordon`.
- **Đề cho sẵn repo package**. Đừng mất thời gian cấu hình `pkgs.k8s.io`, chỉ cần `apt-cache madison kubeadm` là thấy version.
- **`apt-mark hold` là bạn của bạn**. Nếu bạn từng hold `kubelet kubeadm kubectl` để tránh `apt upgrade` tự nâng, nhớ `unhold` trước khi cài.
- **Luôn kết thúc bằng `kubectl get nodes`**. Đây là bước chấm điểm thực tế: node đúng version và không `SchedulingDisabled`.
- **Đừng drain cả 3 node cùng lúc**. Nếu cluster chỉ có 1 worker và bạn drain nó, pod hệ thống có thể không có chỗ chạy.
- **Thời gian hợp lý**: đề 1 control plane + 2 worker nên xong trong 10 đến 12 phút. Nếu quá 15 phút, kiểm tra xem có đang drain nhầm node hay đợi pod eviction quá lâu (`--timeout=60s`).
- **Nếu bị kẹt ở `kubeadm upgrade apply`**, đọc dòng log cuối cùng. Phần lớn lỗi là do etcd không healthy hoặc certificate hết hạn, cả hai đều đã học ở bài trước.

## Thẻ ôn tập nhanh

```flashcard
[
  { "front": "Thứ tự nâng cấp chuẩn của một cluster kubeadm?", "back": "Control plane node đầu tiên, rồi các control plane node còn lại, cuối cùng là từng worker node." },
  { "front": "Lệnh nâng cấp control plane node đầu tiên?", "back": "kubeadm upgrade apply v1.31.x" },
  { "front": "Lệnh nâng cấp một worker node hoặc control plane node phụ?", "back": "kubeadm upgrade node" },
  { "front": "Trước khi nâng cấp kubelet trên một node phải làm gì với pod?", "back": "kubectl drain node đó để đẩy pod sang node khác." },
  { "front": "Flag bắt buộc khi drain node có DaemonSet?", "back": "--ignore-daemonsets" },
  { "front": "Flag cần thêm khi pod trên node dùng emptyDir?", "back": "--delete-emptydir-data" },
  { "front": "Sau khi nâng cấp xong trên node, lệnh nào đưa node nhận pod trở lại?", "back": "kubectl uncordon <node>" },
  { "front": "kubectl cordon khác kubectl drain ở điểm nào?", "back": "cordon chỉ đánh dấu SchedulingDisabled; drain cordon rồi evict pod ra khỏi node." },
  { "front": "Có được nhảy từ 1.29 lên 1.31 trong một lần không?", "back": "Không, mỗi lần chỉ nâng một minor version, phải làm hai lần." },
  { "front": "Thành phần nào không được nâng cấp bởi kubeadm upgrade apply?", "back": "kubelet và kubectl, phải cài bằng package manager rồi restart kubelet." },
  { "front": "Lệnh xem kế hoạch nâng cấp trước khi thực hiện?", "back": "kubeadm upgrade plan" },
  { "front": "Sau khi cài kubelet bản mới cần chạy gì để kubelet dùng binary mới?", "back": "systemctl daemon-reload && systemctl restart kubelet" },
  { "front": "Quy tắc version skew giữa kubelet và kube-apiserver?", "back": "kubelet không được mới hơn kube-apiserver, chỉ được cũ hơn tối đa 3 minor version." },
  { "front": "Node hiển thị SchedulingDisabled nghĩa là gì?", "back": "Node đang bị cordon, không nhận pod mới cho tới khi uncordon." }
]
```

## Kiểm tra nhanh bằng cloze

Điền từ còn thiếu:

1. Để đẩy pod ra khỏi node trước khi nâng cấp, ta dùng `kubectl {{c2::drain}}`.
2. Control plane node đầu tiên dùng `kubeadm upgrade {{c3::apply}}`.
3. Các node còn lại dùng `kubeadm upgrade {{c4::node}}`.
4. Sau khi nâng cấp xong, phải chạy `kubectl {{c5::uncordon}}` để node nhận pod trở lại.
5. Node có DaemonSet cần thêm flag `--{{c6::ignore-daemonsets}}` khi drain.

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

Tóm gọn trong 5 dòng:

1. `kubeadm upgrade plan` trước, `kubeadm upgrade apply` cho control plane đầu tiên, `kubeadm upgrade node` cho phần còn lại.
2. Drain trước khi chạm vào kubelet, uncordon sau khi xong. Không bao giờ bỏ qua bước cuối.
3. `kubeadm` lo control plane, `kubelet` và `kubectl` do bạn cài tay.
4. Mỗi lần một minor version, control plane luôn đi trước.
5. Kết thúc bằng `kubectl get nodes`: đúng version, `Ready`, không `SchedulingDisabled`.

Bài tiếp theo là **Checkpoint: Cluster architecture và upgrade**, một bài tổng hợp gồm dựng cluster bằng kubeadm, backup/restore etcd và nâng cấp cluster, đúng dạng bài bạn sẽ gặp trong phần 25% của đề CKA. Trước khi sang đó, hãy tự chạy lại Bài 5 mà không mở đáp án.
