# 📦 Pod, ReplicaSet, Deployment: cơ chế điều khiển, scheduling và rollout

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

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

- Viết được manifest Deployment đầy đủ và giải thích được vai trò của `replicas`, `selector`, `template`, `strategy`.
- Mô tả được chuỗi sở hữu Deployment → ReplicaSet → Pod và vòng lặp reconcile đứng sau nó.
- Giải thích được scheduler chọn node như thế nào, và vì sao một Pod rơi vào trạng thái `Pending`.
- Theo dõi, dừng và rollback một lần rollout bằng `kubectl` mà không đoán mò.

## Ôn tập nhanh

Bài trước bạn đã đi hết đường đi: `kubectl` → API server → `etcd` → controller tạo Pod → scheduler gán node → kubelet start container. Điểm cần nhớ: API server chỉ ghi trạng thái mong muốn vào `etcd`, còn **controller manager** mới là thứ liên tục nhìn trạng thái thực tế và sửa cho khớp. Bài này chính là mổ xẻ mấy controller đó.

## Lý thuyết

### Pod: đơn vị nhỏ nhất, không phải container

Pod là một nhóm 1 hoặc nhiều container dùng chung network namespace (chung IP, chung port space) và có thể dùng chung volume. Bạn không tạo container trực tiếp, bạn tạo Pod. `restartPolicy` mặc định là `Always`, nên container chết sẽ được kubelet restart tại chỗ. Pod có các phase: `Pending`, `Running`, `Succeeded`, `Failed`.

Mẹo thực chiến: hãy tạo Pod trực tiếp một lần để hiểu, rồi **đừng bao giờ làm vậy nữa** trong môi trường thật. Pod trần không được ai giám sát, chết là mất.

```mermaid
flowchart TD
    D["🚀<br/>Deployment web"] --> R1["📦<br/>ReplicaSet web-7d9f (rev 2)"]
    D --> R0["🗄️<br/>ReplicaSet web-5c2a (rev 1, đã scale 0)"]
    R1 --> P1["🟢<br/>Pod web-7d9f-x1"]
    R1 --> P2["🟢<br/>Pod web-7d9f-x2"]
```

### ReplicaSet: giữ đúng số lượng

ReplicaSet (RS) đảm bảo luôn có đúng `replicas` Pod khớp `selector.matchLabels` đang tồn tại. Hai field phải khớp nhau: label trong `spec.template.metadata.labels` và `spec.selector.matchLabels`. Lệch nhau thì API server từ chối manifest, đây là lỗi newcomer hay gặp nhất.

RS nhận nuôi Pod qua `ownerReferences`. Khi bạn xóa Deployment, garbage collector đi theo chuỗi owner và dọn luôn RS cùng Pod. Đó là lý do `kubectl get pods` sạch bóng sau khi xóa.

### Deployment: quản lý RS, không quản lý Pod

Deployment là lớp trên RS. Mỗi khi `spec.template` thay đổi (image, env, label trong template...), Deployment controller tạo một RS mới với hash mới, rồi scale RS mới lên và RS cũ xuống theo `strategy`. RS cũ được giữ lại với `replicas: 0` để làm bản rollback.

Hai chiến lược:

| Strategy | Hành vi | Downtime |
| --- | --- | --- |
| `RollingUpdate` (mặc định) | Thay Pod dần, tôn trọng `maxSurge` và `maxUnavailable` | Không, nếu có readinessProbe |
| `Recreate` | Tắt hết Pod cũ rồi mới tạo mới | Có |

`maxSurge: 1` cho phép tạo thêm 1 Pod vượt số replica. `maxUnavailable: 0` cấm mất Pod Ready nào trong lúc rollout. Với `replicas: 3`, cấu hình này nghĩa là tối đa 4 Pod tồn tại, tối thiểu 3 Pod Ready. ==Rollout không bao giờ vượt quá `replicas + maxSurge` và không bao giờ xuống dưới `replicas - maxUnavailable`.==

### Vòng lặp reconcile: level-triggered

Controller không phản ứng kiểu "có event thì làm một lần rồi quên". Nó liên tục: đọc trạng thái mong muốn, đọc trạng thái thực tế, tính hiệu, rồi hành động. Vì vậy nếu bạn xóa tay một Pod của RS, RS sẽ tạo lại ngay, và nếu ai đó sửa tay, controller vẫn kéo về đúng số.

### Scheduler chọn node thế nào

Scheduler chạy hai pha:

1. **Filtering**: loại node không hợp lệ. Lý do thường gặp: không đủ `resources.requests`, `nodeSelector`/`nodeAffinity` không khớp, `taints` mà Pod không có `tolerations`, node `NotReady` hoặc đang cordon.
2. **Scoring**: chấm điểm các node còn lại (ví dụ cân bằng tải theo mức sử dụng, ưu tiên node đã có image). Điểm cao nhất thắng.

Nếu mọi node bị loại ở pha 1, Pod nằm `Pending` và `kubectl describe pod` sẽ in ra lý do trong phần Events. Đặt `nodeName` trực tiếp trong spec sẽ bỏ qua scheduler hoàn toàn, chỉ dùng khi debug.

## Ví dụ có hướng dẫn

Manifest dưới đây đủ dùng cho 90% workload stateless:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  revisionHistoryLimit: 5
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27.1
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
```

Đọc từ trên xuống: `replicas: 3` là trạng thái mong muốn. `selector` và label `app: web` trong template phải khớp. `maxUnavailable: 0` chỉ an toàn khi có `readinessProbe`, vì nếu không có probe, Pod được coi là Ready ngay khi container start, dù app chưa sẵn sàng nhận traffic. `resources.requests` là con số scheduler dùng để lọc node, thiếu nó thì mọi Pod đều "vừa" mọi node cho tới khi node hết tài nguyên thật.

Rollout và rollback:

```bash
kubectl apply -f web.yaml
kubectl rollout status deployment/web
kubectl set image deployment/web web=nginx:1.27.2
kubectl rollout history deployment/web
kubectl rollout undo deployment/web
```

`rollout undo` không "chạy lại lệnh cũ". Nó tìm RS của revision trước và scale RS đó lên lại, scale RS hiện tại xuống 0. Vì vậy manifest cũ phải còn trong lịch sử, đừng đặt `revisionHistoryLimit: 0`.

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

**Bài 1.** Viết `web.yaml` như ví dụ trên nhưng đổi image thành `nginx:1.27.0` và thêm label `tier: frontend` cho cả `selector` lẫn template. Apply, kiểm tra `kubectl get rs` và `kubectl get pods -l app=web`.

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

Nhớ sửa label ở **hai chỗ** trong cùng một lần apply, nếu chỉ sửa một chỗ API server báo `selector does not match template labels`. Sau khi apply, bạn sẽ thấy đúng 1 RS và 3 Pod. Chạy `kubectl get pods -o wide` để xem scheduler đặt chúng lên node nào.
</details>

**Bài 2.** Theo dõi rollout: mở terminal 1 chạy `kubectl get pods -w`, terminal 2 chạy `kubectl set image deployment/web web=nginx:1.27.1`. Đếm số Pod tối đa bạn thấy tại một thời điểm.

<details><summary>Đáp án</summary>

Tối đa 4 Pod (3 + `maxSurge: 1`), và không lúc nào dưới 3 Pod Ready vì `maxUnavailable: 0`. Bạn sẽ thấy RS mới được tạo với hash khác, RS cũ tụt dần về 0. Nếu thấy 6 Pod cùng lúc, bạn đang dùng giá trị mặc định `maxSurge: 25%`, tức Deployment chưa nhận `strategy` của bạn.
</details>

**Bài 3.** Gây lỗi có kiểm soát: chạy `kubectl set image deployment/web web=nginx:khong-ton-tai`. Sau 30 giây, xem `kubectl get pods`, `kubectl describe pod <tên>`, rồi `kubectl rollout undo deployment/web`.

<details><summary>Đáp án</summary>

Pod mới ở trạng thái `ImagePullBackOff`, nhưng vì `maxUnavailable: 0`, 3 Pod cũ vẫn chạy phục vụ traffic. `kubectl get pods` cho thấy cả Pod cũ lẫn Pod lỗi. `rollout undo` đưa bạn về revision trước và dọn Pod lỗi. Đây là lý do `maxUnavailable: 0` đáng giá: rollout hỏng không kéo sập dịch vụ.
</details>

**Bài 4 (scheduler).** Thêm `resources.requests.cpu: 4000m` vào container rồi apply. Giải thích vì sao Pod `Pending`.

<details><summary>Đáp án</summary>

Pha filtering loại mọi node vì không node nào còn đủ 4 CPU khả dụng. `kubectl describe pod` sẽ in `0/3 nodes are available: 3 Insufficient cpu`. Xóa hoặc giảm request rồi apply lại, Pod sẽ được schedule.
</details>

## Góc chuyên gia

- **Đừng dùng tag `latest`.** Deployment không tạo RS mới khi nội dung image đổi mà tag không đổi. Bạn sẽ `apply` mà chẳng có rollout nào.
- **`kubectl set image` và `kubectl apply` dễ đá nhau.** `set image` sửa trên cluster, file YAML vẫn ghi image cũ. Lần `apply` sau sẽ âm thầm rollback. Chọn một nguồn sự thật và bám theo nó.
- **Thiếu readinessProbe = rollout nhanh nhưng rủi ro.** Không có probe, K8s đẩy traffic vào Pod ngay khi container start. Có probe, rollout chậm hơn nhưng an toàn.
- **`selector` là immutable.** Đổi `selector.matchLabels` trên Deployment đang tồn tại sẽ bị từ chối. Cách xử lý: xóa và tạo lại, hoặc tạo tên mới rồi chuyển dần.
- **`kubectl rollout pause/resume`** hữu ích khi bạn muốn sửa vài field rồi mới cho rollout chạy một lần, tránh tạo nhiều revision rác.
- Quy tắc ngón tay: stateless → Deployment; cần tên ổn định, thứ tự, storage riêng từng bản → đó là bài của StatefulSet, chưa cần trong module này.

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

Bạn vừa nắm chuỗi Deployment → ReplicaSet → Pod, vòng lặp reconcile kéo trạng thái thực tế về mong muốn, hai pha filtering/scoring của scheduler, và cách `RollingUpdate` phối hợp với `maxSurge`/`maxUnavailable` để rollout không gây downtime. Rollback chỉ là scale RS cũ lên lại.

Pod của bạn giờ đã chạy, nhưng chưa ai nói chuyện được với chúng: IP Pod đổi mỗi lần restart. Bài tiếp theo, **Service và Ingress**, sẽ giải quyết đúng vấn đề đó: làm sao đưa traffic ổn định vào một tập Pod luôn thay đổi.

```cloze
Deployment quản lý ReplicaSet, còn ReplicaSet quản lý trực tiếp {{c1::Pod}}.
Mỗi khi spec.template thay đổi, Deployment tạo ra một {{c2::ReplicaSet}} mới và giữ RS cũ lại để rollback.
Scheduler chọn node qua hai pha: {{c3::filtering}} để loại node không hợp lệ, rồi scoring để chấm điểm.
Với replicas: 3, maxSurge: 1 và maxUnavailable: 0, số Pod tối đa tồn tại cùng lúc là {{c4::4}}.
Lệnh {{c5::kubectl rollout undo}} đưa Deployment về revision trước đó bằng cách scale ReplicaSet cũ lên lại.
Thiếu {{c6::resources.requests}} thì scheduler không có cơ sở để lọc node theo tài nguyên.
```

```flashcard
[
  { "front": "Pod là gì?", "back": "Đơn vị nhỏ nhất K8s schedule được, gồm 1 hoặc nhiều container chung network namespace và volume." },
  { "front": "Khi bạn apply một Deployment, ai tạo Pod?", "back": "Deployment tạo ReplicaSet, ReplicaSet controller tạo Pod." },
  { "front": "Hai field nào phải khớp nhau trong ReplicaSet?", "back": "spec.selector.matchLabels và label trong spec.template.metadata.labels." },
  { "front": "ownerReferences dùng để làm gì?", "back": "Đánh dấu Pod thuộc ReplicaSet nào, để garbage collector xóa theo chuỗi khi owner bị xóa." },
  { "front": "Vòng lặp reconcile hoạt động theo kiểu gì?", "back": "Level-triggered: liên tục so sánh trạng thái mong muốn với thực tế và sửa sai lệch." },
  { "front": "Scheduler chọn node qua mấy pha?", "back": "Hai pha: filtering (loại node không hợp lệ) rồi scoring (chấm điểm node còn lại)." },
  { "front": "Pod Pending thường do nguyên nhân gì?", "back": "Không node nào qua được filter: thiếu CPU/memory, taint không tolerate, nodeSelector hoặc affinity không khớp." },
  { "front": "maxSurge là gì?", "back": "Số Pod (hoặc phần trăm) được phép vượt quá replicas trong lúc rollout." },
  { "front": "maxUnavailable là gì?", "back": "Số Pod (hoặc phần trăm) được phép không Ready trong lúc rollout." },
  { "front": "Deployment tạo ReplicaSet mới khi nào?", "back": "Khi spec.template thay đổi: image, env, label trong template, resources..." },
  { "front": "Lệnh rollback Deployment về revision trước?", "back": "kubectl rollout undo deployment/<tên>" },
  { "front": "Lệnh xem lịch sử revision của Deployment?", "back": "kubectl rollout history deployment/<tên>" },
  { "front": "Chiến lược Recreate khác RollingUpdate thế nào?", "back": "Recreate tắt hết Pod cũ rồi mới tạo mới, gây downtime; RollingUpdate thay dần từng Pod." },
  { "front": "Vì sao tránh tag latest trong Deployment?", "back": "Không đổi tag thì không có spec.template mới, nên không có rollout dù image nội dung đã đổi." }
]
```
