# 🔄 Deployment, rolling update và rollback

## 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 2 đến 3 phút mỗi việc, đúng kiểu đề CKA:

- Tạo Deployment bằng lệnh imperative và bằng YAML.
- Cập nhật image an toàn, hiểu `maxSurge` và `maxUnavailable` đang làm gì.
- Theo dõi rollout bằng `kubectl rollout status` và `kubectl get rs`.
- Rollback về revision trước, hoặc về một revision cụ thể bằng `--to-revision`.

## Ôn nhanh

Ở Module 1 bạn đã dùng `kubectl create ... --dry-run=client -o yaml` để sinh YAML nhanh và `kubectl explain` để tra field. Hai thói quen đó quay lại ngay trong bài này: mọi Deployment trong đề thi đều nên bắt đầu bằng dry-run rồi sửa YAML, thay vì gõ tay từ đầu.

Một điểm nhắc lại: `kubectl` trên cluster lab của bạn có quyền admin, nhưng đề thi luôn ghi rõ namespace. Mất điểm vì quên `-n` là lỗi phổ biến nhất của người mới.

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

Đề bài kiểu thi: "Tạo Deployment tên `web`, image `nginx:1.25`, 3 replicas. Cập nhật lên `nginx:1.26` theo kiểu rolling update với `maxSurge=1`, `maxUnavailable=0`. Sau đó rollback về revision 1."

**Bước 1. Tạo Deployment bằng lệnh imperative.**

```bash
kubectl create deployment web --image=nginx:1.25 --replicas=3
kubectl get deploy,rs,pod -l app=web
```

Bạn sẽ thấy một ReplicaSet tên dạng `web-7d9f8c6b5` và 3 Pod. Tên ReplicaSet chính là `tên-deployment` + hash của pod template.

**Bước 2. Đặt change-cause trước khi sửa, để history có ý nghĩa.**

```bash
kubectl annotate deployment/web kubernetes.io/change-cause="nginx:1.26"
```

`kubectl rollout history` chỉ hiển thị cột CHANGE-CAUSE khi bạn gắn annotation này. Cờ `--record` ngày xưa đã bị bỏ, đừng dùng nữa.

**Bước 3. Cấu hình rolling update.**

```bash
kubectl patch deployment web -p '{"spec":{"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'
```

Lưu ý: sửa `strategy` **không** làm Pod restart, nó chỉ có hiệu lực ở lần update template tiếp theo.

**Bước 4. Cập nhật image và theo dõi.**

```bash
kubectl set image deployment/web nginx=nginx:1.26
kubectl rollout status deployment/web
```

Tên container ở đây là `nginx` (lấy từ tên image khi tạo bằng `kubectl create deployment`). Nếu sai tên container, lệnh báo `unable to find container named ...`.

Kết quả mong đợi:

```
Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been updated...
deployment "web" successfully rolled out
```

Trong lúc chạy, mở terminal thứ hai:

```bash
kubectl get rs -w
```

Bạn sẽ thấy ReplicaSet cũ giảm dần, ReplicaSet mới tăng dần. Với `maxUnavailable=0`, cluster luôn giữ đủ 3 Pod Ready, nên ReplicaSet mới phải scale lên trước (surge thêm 1 Pod) rồi mới hạ Pod cũ.

**Bước 5. Xem history và rollback.**

```bash
kubectl rollout history deployment/web
kubectl rollout undo deployment/web --to-revision=1
kubectl rollout status deployment/web
```

Kiểm tra lại image đang chạy:

```bash
kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[0].image}'
```

Kết quả: `nginx:1.25`.

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

Làm lần lượt 4 bài, bấm giờ như thi thật. Bài nào quá thời gian thì ghi lại lệnh đã quên.

### Bài tập 1: Tạo Deployment bằng YAML từ dry-run (5 phút)

1. Sinh YAML:

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

2. Mở file, sửa `replicas` thành 4, thêm `strategy` rolling update với `maxSurge: 25%`, `maxUnavailable: 25%`.
3. Apply và kiểm tra:

```bash
kubectl apply -f api.yaml
kubectl get deploy api -o wide
kubectl describe deploy api | grep -A5 StrategyType
```

**Kết quả mong đợi:** Deployment `api` có 4 Pod Ready, phần Strategy hiển thị `RollingUpdateStrategy: 25% max unavailable, 25% max surge`.

**Tự kiểm tra:** `maxSurge` tính theo đơn vị gì khi bạn ghi `25%`? (Trả lời: phần trăm của số replicas, làm tròn lên.)

### Bài tập 2: Rolling update có kiểm soát (8 phút)

1. Đặt `maxSurge=1`, `maxUnavailable=0` cho `api`.
2. Chạy song song hai lệnh: `kubectl rollout status deployment/api` và `kubectl get pod -w`.
3. Trong lúc rollout đang chạy, thực hiện `kubectl set image deployment/api nginx=nginx:1.26`.

Quan sát: số Pod Ready không bao giờ xuống dưới 4, và tổng số Pod có lúc lên 5.

4. Thử tiếp `kubectl rollout pause deployment/api`, đổi image một lần nữa, rồi `kubectl rollout resume deployment/api`.

**Kết quả mong đợi:** Khi pause, ReplicaSet mới bị đứng yên, không có Pod mới nào được tạo thêm. Sau resume, rollout chạy tiếp.

### Bài tập 3: Gây lỗi ImagePullBackOff rồi rollback (7 phút)

Đây là tình huống đề thi rất hay gặp: "Deployment đang lỗi, hãy đưa nó về trạng thái chạy tốt trước đó".

1. Gây lỗi:

```bash
kubectl set image deployment/api nginx=nginx:1.99-khong-ton-tai
kubectl rollout status deployment/api --timeout=30s
```

Lệnh thứ hai sẽ treo rồi thoát với lỗi `timed out waiting for the condition`.

2. Chẩn đoán:

```bash
kubectl get pods
kubectl describe pod <pod-mới-nhất> | tail -n 15
```

Bạn sẽ thấy `ErrImagePull` rồi `ImagePullBackOff` trong phần Events.

3. Rollback:

```bash
kubectl rollout undo deployment/api
kubectl rollout status deployment/api
```

**Điểm quan trọng:** các Pod cũ vẫn còn chạy trong suốt thời gian lỗi, vì `maxUnavailable` chặn việc hạ Pod cũ khi Pod mới chưa Ready. Đây chính là lý do rolling update an toàn hơn Recreate.

### Bài tập 4: Rollback về revision cụ thể (5 phút)

1. Thực hiện 3 lần đổi image, mỗi lần đặt `kubernetes.io/change-cause` mô tả rõ (ví dụ `nginx:1.26`, `nginx:1.27`).
2. Xem history:

```bash
kubectl rollout history deployment/api
```

3. Xem chi tiết một revision:

```bash
kubectl rollout history deployment/api --revision=2
```

4. Quay về revision 2:

```bash
kubectl rollout undo deployment/api --to-revision=2
```

**Kết quả mong đợi:** image trở về đúng giá trị của revision 2. History sẽ có một revision mới mang template cũ, nên đừng ngạc nhiên nếu số revision cũ không còn xuất hiện với đúng số đó nữa.

```mermaid
flowchart TD
    D["🚀<br/>Deployment api"]
    RS1["📦<br/>ReplicaSet cũ<br/>nginx:1.25"]
    RS2["📦<br/>ReplicaSet mới<br/>nginx:1.26"]
    P1["🟢<br/>Pod cũ Ready"]
    P2["🟡<br/>Pod mới đang khởi động"]
    D --> RS1
    D --> RS2
    RS1 --> P1
    RS2 --> P2
```

## Bảng lệnh rollout cần thuộc lòng

| Lệnh | Tác dụng |
| --- | --- |
| `kubectl rollout status deployment/web` | Theo dõi tiến trình, thoát khi xong hoặc fail |
| `kubectl rollout history deployment/web` | Liệt kê các revision |
| `kubectl rollout history deployment/web --revision=2` | Xem chi tiết image của revision 2 |
| `kubectl rollout undo deployment/web` | Quay về revision ngay trước |
| `kubectl rollout undo deployment/web --to-revision=1` | Quay về revision chỉ định |
| `kubectl rollout pause` / `resume` | Tạm dừng / chạy tiếp |
| `kubectl rollout restart deployment/web` | Tạo revision mới, thay toàn bộ Pod, giữ nguyên image |

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

| Hiện tượng | Nguyên nhân | Cách xử lý |
| --- | --- | --- |
| `unable to find container named ...` | Sai tên container trong `kubectl set image` | Lấy tên đúng: `kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[*].name}'` |
| Rollout treo mãi, `kubectl get pods` thấy `ImagePullBackOff` | Sai tag image hoặc private registry thiếu imagePullSecret | `kubectl rollout undo` để thoát nhanh, sau đó sửa tag |
| `field is immutable` khi apply | Bạn sửa `spec.selector` | Xoá và tạo lại Deployment, selector của `apps/v1` không đổi được |
| History chỉ hiện `<none>` ở CHANGE-CAUSE | Chưa gắn annotation `kubernetes.io/change-cause` | Annotate trước mỗi lần đổi template |
| `invalid: spec.strategy.rollingUpdate.maxUnavailable: Invalid value: 0 ... maxSurge: 0` | Cả hai field cùng bằng 0 | Phải để ít nhất một field lớn hơn 0 |
| Rollout xong nhưng Pod vẫn restart liên tục | Lỗi application, không phải lỗi Deployment | Kiểm tra `kubectl logs` và `kubectl describe pod` |
| Sửa strategy mà không thấy Pod đổi | Strategy không nằm trong pod template | Bình thường, strategy chỉ áp dụng ở lần update sau |

> [!NOTE]
> Mẹo thi: khi đề cho một Deployment đang lỗi và yêu cầu "khôi phục phiên bản trước", đừng mất thời gian đọc log application. Chạy `kubectl rollout history`, chọn revision đúng, `kubectl rollout undo --to-revision=N`, rồi `kubectl rollout status`. Xong trong khoảng 60 giây.

> [!WARNING]
> Sửa Deployment bằng `kubectl edit` rất dễ tạo revision rác, vì mỗi lần lưu có thể đổi template nhiều lần. Trong thi, dùng `kubectl set image` hoặc `kubectl patch` để mỗi thao tác là một revision rõ ràng.

## Góc chuyên gia

Vài điều mình ước biết trước khi thi:

- `kubectl set image` nhanh hơn `kubectl edit` rất nhiều, và không sợ lưu nhầm. Dùng nó làm mặc định.
- ==`rollout undo` không xoá revision, nó tạo revision mới từ template cũ.== Nên bạn có thể undo nhiều lần liên tiếp mà vẫn quay lại được các bản trước, miễn là chưa vượt `revisionHistoryLimit` (mặc định 10).
- Luôn đặt `kubernetes.io/change-cause` trước khi đổi template. Đây là điểm "dễ lấy" trong các đề yêu cầu "ghi lại lý do thay đổi".
- Trước khi nộp, chạy đúng 3 lệnh kiểm tra: `kubectl get deploy -o wide`, `kubectl rollout status deployment/<tên>`, `kubectl get pod`. Ba lệnh này bắt được gần hết lỗi.
- Với Deployment gắn PVC `ReadWriteOnce` nhiều replica, hãy dùng `strategy.type: Recreate`, vì rolling update sẽ làm nhiều Pod tranh nhau mount cùng volume và bị `Multi-Attach error`.
- Đề có thể yêu cầu "scale rồi update". Thứ tự an toàn: scale trước, update sau, để `maxUnavailable` tính trên số replica cuối cùng.
- Alias tiết kiệm thời gian thật: `alias k=kubectl` và `export do="--dry-run=client -o yaml"`. Gõ `k create deploy api --image=nginx $do > api.yaml` nhanh hơn hẳn.

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

Bốn điều cần mang ra khỏi bài này:

1. Deployment quản lý ReplicaSet, ReplicaSet quản lý Pod. Mỗi lần đổi pod template là một ReplicaSet mới, tức một revision mới.
2. `maxSurge` và `maxUnavailable` quyết định tốc độ và độ an toàn của rolling update, mặc định 25% mỗi cái.
3. Bộ lệnh `kubectl rollout status / history / undo / pause / resume / restart` là công cụ chính, thuộc lòng cả 6.
4. Khi rollout lỗi, `kubectl rollout undo` là cách thoát nhanh nhất, và Pod cũ vẫn an toàn nhờ `maxUnavailable`.

Kiểm tra nhanh bằng cloze dưới đây trước khi sang bài sau:

```cloze
Deployment quản lý Pod thông qua object trung gian là {{c1::ReplicaSet}}.
Với strategy RollingUpdate, hai tham số kiểm soát tốc độ là maxSurge và {{c2::maxUnavailable}}, mặc định đều bằng {{c3::25%}}.
Lệnh theo dõi tiến trình rollout là: kubectl rollout {{c4::status}} deployment/web.
Quay về revision 1 bằng: kubectl rollout undo deployment/web {{c5::--to-revision=1}}.
Ghi lý do thay đổi vào history bằng annotation {{c6::kubernetes.io/change-cause}}.
```

### Flashcard ôn tập

```flashcard
[
  { "front": "Deployment quản lý Pod thông qua object nào?", "back": "ReplicaSet, và ReplicaSet quản lý trực tiếp các Pod." },
  { "front": "Hai field điều khiển rolling update nằm ở đâu?", "back": "spec.strategy.rollingUpdate với maxSurge và maxUnavailable." },
  { "front": "Giá trị mặc định của maxSurge và maxUnavailable?", "back": "25% cho cả hai." },
  { "front": "Lệnh theo dõi tiến trình rollout?", "back": "kubectl rollout status deployment/<tên>" },
  { "front": "Lệnh xem lịch sử revision?", "back": "kubectl rollout history deployment/<tên>, thêm --revision=N để xem chi tiết." },
  { "front": "Lệnh rollback về revision ngay trước?", "back": "kubectl rollout undo deployment/<tên>" },
  { "front": "Lệnh rollback về một revision cụ thể?", "back": "kubectl rollout undo deployment/<tên> --to-revision=N" },
  { "front": "Làm sao để cột CHANGE-CAUSE có dữ liệu?", "back": "Gắn annotation kubernetes.io/change-cause trước khi đổi pod template." },
  { "front": "Strategy nào gây downtime?", "back": "Recreate: xoá hết Pod cũ rồi mới tạo Pod mới." },
  { "front": "Field giới hạn số revision được giữ lại?", "back": "spec.revisionHistoryLimit, mặc định là 10." },
  { "front": "Điều gì xảy ra khi sửa spec.selector của Deployment?", "back": "Báo lỗi field is immutable, phải xoá và tạo lại Deployment." },
  { "front": "Lệnh restart toàn bộ Pod mà không đổi image?", "back": "kubectl rollout restart deployment/<tên>" },
  { "front": "Vì sao rollout bị treo khi image sai tag?", "back": "Pod mới ImagePullBackOff và không Ready, maxUnavailable giữ Pod cũ nên rollout chờ mãi." },
  { "front": "Lệnh tạm dừng và chạy tiếp rollout?", "back": "kubectl rollout pause deployment/<tên> và kubectl rollout resume deployment/<tên>" }
]
```

**Bài tiếp theo: DaemonSet, Job, CronJob.** Deployment phù hợp với workload chạy dài hạn và cần rolling update, nhưng CKA còn hỏi hai kiểu khác: chạy đúng một Pod trên mỗi node (DaemonSet) và workload chạy một lần hoặc theo lịch (Job, CronJob). Bài sau sẽ đi vào cú pháp và các bẫy thường gặp của hai loại này.
