---
id: case-stale
title: "Deploy xanh, user thấy bản cũ"
shortTitle: "Bản cũ vẫn chạy"
track: cases
trackTitle: "Tình huống vận hành"
order: 5
level: important
status: published
estimatedMinutes: 30
prerequisites:
  - docker-registry
  - kubernetes-kubectl
next: case-alert
reviewedAgainst: 2026-10-12
sources:
  - title: "Kubernetes Deployments"
    url: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
  - title: "Docker image digests"
    url: https://docs.docker.com/reference/cli/docker/image/pull/#pull-an-image-by-digest-immutable-identifier
---

# Deploy xanh, user thấy bản cũ

Pipeline xanh chỉ có nghĩa là các bước trong file pipeline chạy không lỗi. Nó không chứng minh user đang dùng đúng bản bạn vừa build. Tag `latest` là một cái tên có thể trỏ đi chỗ khác bất cứ lúc nào, còn cache và replica cũ thì vẫn giữ bản trước.

## Để làm gì

Bạn cần chỉ ra bản nào đang phục vụ user bằng một định danh không đổi, rồi tìm xem tầng nào còn giữ bản cũ: image, rollout, CDN hay trình duyệt.

## Gồm những gì

- Tag và digest.
- Rollout chưa xong, Pod cũ vẫn nhận traffic.
- `Cache-Control` và CDN.

Nếu app chưa có endpoint trả version, bài không yêu cầu bạn sửa app để thêm. Một header hay một file tĩnh có nội dung thay đổi theo bản build là đủ để chứng minh bản nào đang chạy.

## Dùng khi nào

Bạn merge xong, GitHub Actions xanh, mà nút mới vẫn không có trên trang. Hoặc chỉ một phần user thấy bản mới. Việc cần làm ở đây là đối chiếu danh tính của artifact, không phải đọc stack trace.

Đừng build lại ngay. Build lại chỉ tạo thêm một bản nữa, và bạn vẫn không biết bản nào đang chạy.

## Kiến thức cốt lõi

Hãy trả lời câu "bản nào đang chạy" bằng một thứ không đổi: git sha trong label của image, hoặc digest `sha256:...`. Docker hỗ trợ pull theo digest chính vì digest là định danh bất biến. Còn tag `latest` trỏ tới digest nào là do registry quyết định tại thời điểm pull. Node đã pull `latest` từ hôm qua sẽ không tự pull lại.

Kiểm tra theo bốn tầng, từ gần cluster ra tới user:

1. **Context cluster.** Chạy `kubectl config current-context` trước mọi lệnh khác. Nếu pipeline deploy vào cluster A mà user đang dùng cluster B, mọi kiểm tra phía sau đều vô nghĩa, nên bước này phải đứng đầu.
2. **Rollout và image trên Pod.** Đổi tag trong Deployment là chưa đủ, `kubectl rollout status` phải báo xong. Pod của ReplicaSet cũ vẫn nhận traffic cho tới khi Pod mới sẵn sàng và strategy cho phép xóa Pod cũ. Dùng `kubectl get pods -o wide` và xem image của từng Pod để biết có Pod nào còn chạy tag cũ.
3. **Digest so với bản CI vừa push.** Image trên Pod phải trỏ đúng digest mà CI vừa đẩy lên registry. Tag giống nhau chưa chắc digest giống nhau.
4. **Cache phía trước user.** Chỉ khi mọi Pod đã chạy digest mới mà trình duyệt vẫn hiện bản cũ, bạn mới chuyển sang tầng cache. Thủ phạm có thể là HTML trả về `Cache-Control: public` với TTL dài, hoặc CDN còn giữ object cũ. Gửi một request kèm header bypass cache, hoặc thêm một query string không đổi nghĩa vào URL, để xác nhận origin đã trả bản mới.

Thứ tự này có lý do. Đừng purge CDN trước khi chứng minh origin đã mới: nếu origin vẫn cũ, CDN sẽ lấy lại bản cũ ngay sau khi purge, và bạn chỉ tốn công một lần purge.

Bạn xác nhận sự cố đã xử lý khi mọi Pod chạy cùng digest với bản CI, và một request đi qua CDN (không bypass cache) trả về đúng bản mới.

## Lệnh và code mẫu

Chạy các lệnh đọc sau khi bạn chắc chắn đang ở đúng cluster lab:

```bash
kubectl config current-context
kubectl rollout status deploy/api
kubectl get pods -l app=api -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[0].image}{"\n"}{end}'
```

Gửi một request không dùng cache của trình duyệt:

```bash
curl -sS -D - -o /dev/null -H 'Cache-Control: no-cache' https://example.com/
```

Nếu phía trước có CDN, hãy nhìn header `age` hoặc `x-cache`. Response không có những header đó thì bạn chưa đủ bằng chứng để kết luận lỗi nằm ở CDN.

## Thực hành

1. Viết bốn tầng theo thứ tự kiểm tra: context cluster, image trên Pod, digest so với bản CI vừa push, rồi cache phía trước user.
2. Giải thích vì sao khi một service có hai Pod mà một Pod còn image cũ, user lúc thấy bản mới, lúc lại không.
3. Không rollout lên cluster thật từ bài này.

## Kết quả mong đợi

Bạn có thứ tự bốn tầng. Bạn không nói "đã deploy" khi mới chỉ có pipeline xanh.

## Checklist

- [ ] Tag deploy là git sha, hoặc bạn so sánh digest; không kết luận dựa trên `latest`.
- [ ] `rollout status` đã xong và mọi Pod chạy cùng một image.
- [ ] Đã kiểm tra đúng context trước khi nói về "production".
- [ ] Chỉ đổ lỗi cho cache sau khi origin đã trả bản mới.

## Lỗi thường gặp

**Sửa xong, push lại `latest`, nhưng không đổi spec của Deployment.** Kubernetes không pull lại image khi spec không đổi và `imagePullPolicy` là `IfNotPresent`, nên Pod cũ vẫn giữ layer cũ. Khi bạn đổi tag theo git sha, spec thay đổi và rollout sẽ chạy.

**Hard refresh trên một máy rồi báo đã xong.** Máy đó bỏ qua cache, nhưng máy khác và CDN thì chưa. Hãy kiểm tra bằng `curl` tới origin, đừng dựa vào một trình duyệt.

## Nguồn

1. [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) — rollout và ReplicaSet.
2. [Pull an image by digest](https://docs.docker.com/reference/cli/docker/image/pull/#pull-an-image-by-digest-immutable-identifier) — digest không đổi như tag.

## Tiếp theo

[Alert không khớp với user](./06-alert.md)
