# 🎯 nodeSelector, affinity và taint/toleration

Bài trước bạn đã học requests/limits và QoS class: đó là cách nói với scheduler "pod này cần bao nhiêu tài nguyên". Bài này là mảnh ghép còn lại: nói với scheduler **pod phải nằm ở đâu** và **node nào được phép nhận pod**. Đây là dạng bài xuất hiện gần như chắc chắn trong đề CKA, và cũng là chỗ nhiều người mất điểm oan vì làm đúng cú pháp nhưng sai logic.

> [!NOTE]
> Kiến thức cần nhớ từ bài trước: nếu pod không có node nào đủ CPU/memory theo `requests`, scheduler sẽ để pod ở trạng thái `Pending`. Bài này thêm một lý do nữa khiến pod `Pending`: label/affinity không khớp hoặc node bị taint. Khi troubleshoot, luôn đọc dòng `Events` của `kubectl describe pod` trước.

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

Sau bài này, bạn phải làm được 4 việc trong khoảng 35 phút, không cần tra Google:

- Dùng `nodeSelector` để ghim pod vào node có label chỉ định.
- Viết `nodeAffinity` dạng `required` (bắt buộc) và `preferred` (ưu tiên) bằng `matchExpressions`.
- Gắn và gỡ `taint` trên node, thêm `toleration` vào pod.
- Phân biệt được 3 effect `NoSchedule`, `PreferNoSchedule`, `NoExecute` và biết khi nào đề bài muốn cái nào.

## 2. Ví dụ có hướng dẫn (worked example)

**Đề bài kiểu CKA:** Cluster có `node01` và `node02`. Hãy tạo pod tên `ssd-pod`, image `nginx`, chỉ chạy trên node có label `disk=ssd`. Node `node02` phải bị chặn để pod thường không vào được, chỉ pod có toleration phù hợp mới vào.

**Bước 1. Khảo sát hiện trạng trước khi sửa bất cứ thứ gì.**

```bash
kubectl get nodes --show-labels
kubectl describe node node01 | grep -iA3 taints
```

`--show-labels` cho bạn thấy toàn bộ label hiện có, tránh ghi đè nhầm. Trong thi, đây là bước 10 giây nhưng cứu bạn khỏi việc tạo label trùng key.

**Bước 2. Gắn label cho node.**

```bash
kubectl label node node01 disk=ssd
kubectl get node node01 --show-labels
```

Nếu label đã tồn tại với giá trị khác, `kubectl` sẽ báo lỗi và yêu cầu `--overwrite`. Đây là chi tiết nhỏ nhưng hay xuất hiện trong đề.

**Bước 3. Tạo pod bằng dry-run rồi sửa YAML (mẹo ăn điểm).**

```bash
kubectl run ssd-pod --image=nginx --dry-run=client -o yaml > ssd-pod.yaml
```

Mở file và thêm `nodeSelector`:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: ssd-pod
spec:
  nodeSelector:
    disk: ssd
  containers:
  - name: web
    image: nginx
```

```bash
kubectl apply -f ssd-pod.yaml
kubectl get pod ssd-pod -o wide
```

Cột `NODE` phải là `node01`. Nếu là `node02` thì bạn gắn label sai node; nếu `Pending` thì đọc `kubectl describe pod ssd-pod`.

**Bước 4. Gắn taint lên `node02`.**

```bash
kubectl taint nodes node02 env=prod:NoSchedule
kubectl describe node node02 | grep -i taint
```

Kết quả mong đợi: `Taints: env=prod:NoSchedule`.

**Bước 5. Kiểm chứng logic toleration.** Tạo một pod không có toleration và một pod có toleration, sau đó xem pod nào vào được `node02`:

```yaml
spec:
  nodeSelector:
    kubernetes.io/hostname: node02
  tolerations:
  - key: env
    operator: Equal
    value: prod
    effect: NoSchedule
```

Pod không có `tolerations` sẽ `Pending` với event dạng `1 node(s) had untolerated taint {env: prod}`. Pod có toleration sẽ vào được. Đây chính là điểm mấu chốt của bài: ==toleration chỉ mở cửa, không kéo pod vào trong==.

**Bước 6. Nếu đề yêu cầu "ưu tiên" thay vì "bắt buộc"**, dùng `preferred` thay cho `nodeSelector`:

```yaml
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: disk
            operator: In
            values: ["ssd"]
```

Với `preferred`, nếu `node01` hết tài nguyên, pod vẫn có thể chạy trên `node02`. Với `nodeSelector` hoặc affinity `required`, pod sẽ nằm `Pending` thay vì chọn node khác.

```mermaid
flowchart TD
    Start["🚀 Pod được tạo"] --> NS{"Có nodeSelector?"}
    NS -->|Có| Filter["🔎 Scheduler lọc node theo label"]
    NS -->|Không| Aff{"Có required nodeAffinity?"}
    Aff -->|Có| Filter
    Aff -->|Không| Taint{"Node còn taint mà pod không tolerate?"}
    Filter --> Taint
    Taint -->|Có| Skip["⛔ Loại node đó"]
    Taint -->|Không| Bind["✅ Bind pod vào node"]
    Skip --> Pending["⏳ Hết node thì Pod Pending"]
```

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

Chuẩn bị: cluster có ít nhất 2 node. Nếu bạn lab bằng cluster 1 node, dùng `control-plane` làm node đích và nhớ nó đã có taint sẵn.

Bài tập dưới đây được thiết kế đúng thứ tự khó tăng dần. Làm xong mỗi bài, ghi lại lệnh kiểm chứng bạn đã dùng.

### Bài 1. nodeSelector cơ bản

Gắn label `tier=frontend` cho node bất kỳ có hostname `node01`, tạo pod `web-frontend` image `nginx` chỉ chạy trên node đó bằng `nodeSelector`.

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

```bash
kubectl label node node01 tier=frontend
kubectl run web-frontend --image=nginx --dry-run=client -o yaml > web.yaml
```

Thêm vào `spec`:

```yaml
  nodeSelector:
    tier: frontend
```

```bash
kubectl apply -f web.yaml
kubectl get pod web-frontend -o wide
```

</details>

### Bài 2. nodeAffinity required với operator In

Lặp lại bài 1 nhưng dùng `nodeAffinity` thay cho `nodeSelector`, với `requiredDuringSchedulingIgnoredDuringExecution` và operator `In`.

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

```yaml
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: tier
            operator: In
            values: ["frontend"]
```

Lưu ý cấu trúc: `nodeSelectorTerms` là **list**, bên trong mỗi term là `matchExpressions` cũng là **list**. Sai một cấp lồng nhau là `kubectl apply` báo lỗi ngay.

</details>

### Bài 3. preferred affinity

Tạo pod `prefer-ssd` image `nginx` với `preferred` affinity cho label `disk=ssd`, weight 1. Sau đó xóa label `disk` khỏi mọi node và quan sát: pod vẫn phải chạy được (khác với bài 2).

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

```bash
kubectl label node node01 disk-
kubectl get pod prefer-ssd -o wide
```

Với `preferred`, scheduler bỏ qua tiêu chí không khớp và vẫn đặt pod. Đây là cách phân biệt nhanh `required` và `preferred` khi làm bài.

</details>

### Bài 4. Taint NoSchedule và toleration

Gắn taint `gpu=true:NoSchedule` lên `node02`. Tạo pod `gpu-pod` image `nginx` có toleration tương ứng **và** `nodeSelector` ghim vào `node02`.

```yaml
spec:
  nodeSelector:
    kubernetes.io/hostname: node02
  tolerations:
  - key: gpu
    operator: Equal
    value: "true"
    effect: NoSchedule
```

Kiểm chứng: `kubectl get pod gpu-pod -o wide`. Sau đó thử bỏ `nodeSelector` và tạo lại: pod nhiều khả năng vẫn về `node01`, vì toleration không hề ép pod vào node bị taint.

> [!TIP]
> Muốn "chấp nhận mọi taint" (ví dụ đề bảo pod phải chạy được trên cả control-plane), dùng toleration không có `key`:
> ```yaml
>   tolerations:
>   - operator: Exists
> ```
> Cách này chỉ nên dùng khi đề nói rõ là chấp nhận mọi node.

### Bài 5. Gỡ taint và kiểm tra pod bị đuổi

Gỡ taint `env=prod:NoSchedule` khỏi `node02`. Sau đó gắn lại bằng effect `NoExecute` và quan sát pod đang chạy trên `node02` mà không có toleration.

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

```bash
kubectl taint nodes node02 env=prod:NoSchedule-
kubectl taint nodes node02 env=prod:NoExecute
kubectl get pods -o wide --field-selector spec.nodeName=node02
```

Với `NoExecute`, pod không tolerate bị **đuổi khỏi node** (terminated) chứ không chỉ bị chặn như `NoSchedule`. Đây là điểm khác biệt hay bị hỏi.

</details>

### Bài 6. Bài tổng hợp kiểu đề thi

Tạo Deployment `api` 2 replicas image `nginx` thỏa **đồng thời**: chỉ chạy trên node có label `tier=backend`, và chạy được trên node bị taint `dedicated=api:NoSchedule`.

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

```bash
kubectl create deployment api --image=nginx --replicas=2 --dry-run=client -o yaml > api.yaml
```

Sửa file, thêm cả `nodeSelector` và `tolerations` vào `spec.template.spec`, rồi:

```bash
kubectl apply -f api.yaml
kubectl get pods -l app=api -o wide
kubectl describe pod -l app=api | grep -i -A2 "Node-Selectors\|Tolerations"
```

`describe` in ra cả `Node-Selectors` và `Tolerations`, đây là cách tự chấm điểm nhanh trong phòng thi.

</details>

## 4. Lỗi thường gặp và troubleshooting

| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
| --- | --- | --- |
| Pod `Pending`, event `didn't match Pod's node affinity/selector` | Label node sai key hoặc sai value | `kubectl get nodes --show-labels`, sửa label hoặc sửa YAML |
| Pod `Pending`, event `had untolerated taint` | Thiếu `tolerations` hoặc sai `effect` | Thêm toleration đúng `key`, `value`, `effect` |
| Đã thêm toleration nhưng pod vẫn không vào node bị taint | Tưởng toleration ép được pod lên node | Thêm `nodeSelector` hoặc `nodeAffinity` để ghim |
| `kubectl taint ... -` báo lỗi | Quên dấu `-` hoặc gõ sai key | Kiểm tra lại bằng `kubectl describe node \| grep -i taint` |
| Toleration không có tác dụng | `operator: Equal` nhưng thiếu `value`, hoặc `value` kiểu số thiếu dấu nháy | Dùng `operator: Exists` nếu chỉ cần khớp key |
| Pod đang chạy bỗng bị terminate | Ai đó gắn taint `NoExecute` | Thêm toleration, hoặc gỡ taint |
| Sửa label node nhưng pod không di chuyển | Hậu tố `IgnoredDuringExecution`: label chỉ xét lúc schedule | Phải xóa và tạo lại pod nếu muốn scheduler xét lại |

Ba lỗi logic đáng nhớ nhất:

1. **Toleration không phải là bộ lọc chọn node.** Nó là "vé vào cửa". Muốn pod nằm đúng node, luôn cần `nodeSelector` hoặc `nodeAffinity` đi kèm.
2. **`nodeSelector` là AND tuyệt đối.** Khai báo 2 label thì node phải có đủ cả 2. Không có cú pháp OR trong `nodeSelector`, phải dùng `nodeAffinity` với nhiều `nodeSelectorTerms`.
3. **`nodeName` bỏ qua scheduler hoàn toàn.** Nếu đề bắt buộc dùng scheduler (ví dụ phải có event scheduling), đừng dùng `nodeName`. Nó cũng không kiểm tra taint.

## 5. Góc chuyên gia

**Ba câu hỏi để chọn đúng công cụ, đọc đề xong trả lời trong 5 giây:**

- Đề nói "chỉ chạy trên node có label X" → `nodeSelector`. Nhanh nhất, ít lỗi cú pháp nhất.
- Đề nói "ưu tiên", "nếu có thể", "prefer" → `preferredDuringSchedulingIgnoredDuringExecution`.
- Đề nói "node bị taint, pod phải chạy được ở đó" → `tolerations` cộng thêm `nodeSelector` để ghim đúng node.

**Mẹo tốc độ trong phòng thi:**

```bash
alias k=kubectl
export do="--dry-run=client -o yaml"
k run p1 --image=nginx $do > p1.yaml
```

Sau đó chỉ việc mở file, thêm 4-6 dòng YAML, apply. Không gõ tay cả manifest từ đầu.

**Kiểm tra nhanh sau khi làm:**

```bash
kubectl get pod <ten> -o wide          # xem cột NODE
kubectl describe pod <ten> | grep -i -A3 "node-selectors\|tolerations"
kubectl get events --sort-by=.lastTimestamp | tail -20
```

**Quy tắc ngón tay cái về effect:**

- `NoSchedule`: chặn pod mới, pod cũ ở lại. Dùng nhiều nhất trong đề.
- `PreferNoSchedule`: chỉ là gợi ý, scheduler sẽ tránh nếu còn chỗ khác.
- `NoExecute`: đuổi cả pod đang chạy. Chỉ dùng khi đề nói rõ "evict".

**Nhớ taint mặc định của control plane:** `node-role.kubernetes.io/control-plane:NoSchedule`. Nhiều bạn lab trên cluster 1 node, tạo pod bằng `nodeSelector` vào control-plane rồi thắc mắc tại sao `Pending`. Đó là taint, không phải label sai.

**Đừng quên dọn dẹp:** đề thi tính điểm trên trạng thái cuối cùng của cluster. Làm xong một task, xóa pod test và gỡ taint/label tạm bạn đã tạo, tránh ảnh hưởng task sau.

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

Chốt lại bốn dòng:

- `nodeSelector`: khớp label chính xác, đơn giản nhất, không khớp thì `Pending`. {{c1::nodeSelector}} là cách ghim pod vào node theo label.
- `nodeAffinity`: `required` là bắt buộc, `preferred` là ưu tiên, hỗ trợ operator `In`, `NotIn`, `Exists`, `DoesNotExist`, `Gt`, `Lt`. Affinity bắt buộc dùng field {{c2::requiredDuringSchedulingIgnoredDuringExecution}}.
- `taint` đặt trên node với effect {{c3::NoSchedule}}, `PreferNoSchedule` hoặc {{c4::NoExecute}}.
- `toleration` đặt trên pod, chỉ **cho phép** pod vào node bị taint, không ép. Effect {{c5::NoExecute}} sẽ đuổi pod đang chạy nếu không có toleration.

Bài tiếp theo là **Checkpoint: Workloads và scheduling**. Đó là bài tổng hợp, đề sẽ trộn Deployment với rolling update, DaemonSet, resource requests và chính nội dung bài này. Trước khi sang bài đó, hãy tự làm lại bài 4 và bài 6 mà không nhìn gợi ý: nếu làm được dưới 3 phút mỗi bài thì bạn đã sẵn sàng.

```flashcard
[
  { "front": "nodeSelector khớp label theo quy tắc nào?", "back": "Khớp chính xác key=value, tất cả label khai báo phải khớp cùng lúc, không khớp thì Pod Pending." },
  { "front": "Hai loại nodeAffinity là gì?", "back": "requiredDuringSchedulingIgnoredDuringExecution (bắt buộc) và preferredDuringSchedulingIgnoredDuringExecution (ưu tiên)." },
  { "front": "Các operator dùng trong matchExpressions của nodeAffinity?", "back": "In, NotIn, Exists, DoesNotExist, Gt, Lt." },
  { "front": "Ba effect hợp lệ của taint?", "back": "NoSchedule, PreferNoSchedule, NoExecute." },
  { "front": "Toleration có ép pod lên node bị taint không?", "back": "Không. Toleration chỉ cho phép pod vào; muốn ép phải thêm nodeSelector hoặc nodeAffinity." },
  { "front": "Lệnh gắn taint env=prod:NoSchedule lên node02?", "back": "kubectl taint nodes node02 env=prod:NoSchedule" },
  { "front": "Làm sao gỡ một taint khỏi node?", "back": "Thêm dấu trừ vào cuối: kubectl taint nodes node02 env=prod:NoSchedule-" },
  { "front": "NoExecute khác NoSchedule ở điểm nào?", "back": "NoExecute đuổi cả pod đang chạy không tolerate; NoSchedule chỉ chặn pod mới được schedule." },
  { "front": "Toleration được khai báo ở field nào trong Pod spec?", "back": "spec.tolerations, gồm các field key, operator, value, effect." },
  { "front": "Operator mặc định của một toleration là gì?", "back": "Equal." },
  { "front": "Taint mặc định trên node control-plane?", "back": "node-role.kubernetes.io/control-plane:NoSchedule" },
  { "front": "Lệnh xem taint của node?", "back": "kubectl describe node node01 | grep -i taint" },
  { "front": "nodeName khác nodeSelector ra sao?", "back": "nodeName bỏ qua scheduler, gán thẳng pod vào node và không kiểm tra taint; nodeSelector do scheduler xử lý." },
  { "front": "Lệnh gắn label disk=ssd cho node01?", "back": "kubectl label node node01 disk=ssd, thêm --overwrite nếu label đã tồn tại với giá trị khác." }
]
```
