Deployment: rollout và rollback
Suốt Part III ta tạo Pod trần bằng tay để mổ xẻ vòng đời của nó. Nhưng không ai chạy production kiểu đó: Pod trần không tự hồi sinh khi node chết, không cập nhật phiên bản gọn gàng, không scale. Việc đó giao cho controller, và phổ biến nhất là Deployment. Part IV mở đầu ở đây vì Deployment là thứ ta đụng hằng ngày, nhưng điều đáng học không phải cú pháp YAML mà là cơ chế đằng sau: Deployment không quản Pod trực tiếp, nó quản qua một lớp giữa tên ReplicaSet, và chính lớp giữa này làm cho rollout/rollback hoạt động.
Deployment → ReplicaSet → Pod
Tài liệu mô tả vai trò Deployment bằng một câu khai báo: "A Deployment provides declarative updates for Pods and ReplicaSets. You describe a desired state ... and the Deployment Controller changes the actual state to the desired state at a controlled rate." Chữ "ReplicaSets" ở đó không phải tình cờ: nó là lớp Deployment dùng để giữ đúng số bản sao. Tạo một Deployment 3 bản sao rồi soi xem có gì mọc ra:
apiVersion: apps/v1
kind: Deployment
metadata: {name: rollout-demo}
spec:
replicas: 3
selector: {matchLabels: {app: rollout-demo}}
template:
metadata: {labels: {app: rollout-demo}}
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep","3600"]
kubectl get deploy rollout-demo
kubectl get rs,pods -l app=rollout-demo
NAME READY UP-TO-DATE AVAILABLE AGE
rollout-demo 3/3 3 3 8s
NAME DESIRED CURRENT READY AGE
replicaset.apps/rollout-demo-7545d5669f 3 3 3 8s
NAME READY STATUS RESTARTS AGE
pod/rollout-demo-7545d5669f-8nh2r 1/1 Running 0 8s
pod/rollout-demo-7545d5669f-s6pps 1/1 Running 0 8s
pod/rollout-demo-7545d5669f-sv2fv 1/1 Running 0 8s
Ta khai một Deployment, nhưng có ba tầng object: Deployment rollout-demo, một ReplicaSet rollout-demo-7545d5669f, và ba Pod tên bắt đầu bằng đúng tên ReplicaSet đó. Cái hậu tố 7545d5669f không ngẫu nhiên, nó là pod-template-hash, băm của pod template. Lần theo ownerReferences (cơ chế sở hữu/GC ở Bài 17) để thấy chuỗi:
POD=$(kubectl get pods -l app=rollout-demo -o jsonpath='{.items[0].metadata.name}')
kubectl get pod $POD -o jsonpath='pod={.metadata.name} ownerRS={.metadata.ownerReferences[0].name}{"\n"}'
kubectl get rs rollout-demo-7545d5669f -o jsonpath='rs={.metadata.name} ownerDeploy={.metadata.ownerReferences[0].name} hash={.metadata.labels.pod-template-hash}{"\n"}'
pod=rollout-demo-7545d5669f-8nh2r ownerRS=rollout-demo-7545d5669f
rs=rollout-demo-7545d5669f ownerDeploy=rollout-demo hash=7545d5669f
Pod thuộc về ReplicaSet, ReplicaSet thuộc về Deployment. Phân vai rạch ròi: ReplicaSet chỉ lo một việc là giữ đúng số Pod cho một phiên bản template (chính nó là thứ đã tạo Pod bù ở Bài 23). Deployment lo việc cao hơn, điều phối nhiều ReplicaSet khi template đổi. Hiểu được sự phân vai này thì rollout/rollback trở nên hiển nhiên.
Đổi template là sinh ReplicaSet mới
Câu hỏi then chốt: khi nào Deployment làm gì? Tài liệu trả lời chính xác:
"A Deployment's rollout is triggered if and only if the Deployment's Pod template (that is,
.spec.template) is changed, for example if the labels or container images of the template are updated. Other updates, such as scaling the Deployment, do not trigger a rollout."
"if and only if" — chỉ khi .spec.template đổi. Đổi image là một ca như vậy. Và cách Deployment xử lý, vẫn theo tài liệu: "A new ReplicaSet is created, and the Deployment gradually scales it up while scaling down the old ReplicaSet, ensuring Pods are replaced at a controlled rate." Đổi image rồi theo dõi:
kubectl set image deployment/rollout-demo app=busybox:1.37
kubectl rollout status deployment/rollout-demo
deployment.apps/rollout-demo image updated
Waiting for deployment "rollout-demo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "rollout-demo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "rollout-demo" rollout to finish: 1 old replicas are pending termination...
deployment "rollout-demo" successfully rolled out
rollout status cho xem chính cái "controlled rate" đó: bản mới lên dần 1 → 2 → 3, bản cũ lui dần. Nhịp này do hai trường trong .spec.strategy.rollingUpdate điều khiển, mặc định đều 25%: maxUnavailable (tối đa bao nhiêu bản sao được phép thiếu trong lúc cập nhật) và maxSurge (tối đa bao nhiêu bản sao dư so với số mong muốn được tạo thêm). Với 3 bản sao, 25% làm tròn lên 1 — nên trong quá trình rollout có lúc chạy tới 4 pod (3 + 1 surge) và không bao giờ tụt dưới 2 sẵn sàng. (RollingUpdate là chiến lược mặc định; còn Recreate thì giết sạch bản cũ rồi mới dựng bản mới — chấp nhận downtime.) Xem hai ReplicaSet sau khi xong:
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'
NAME DESIRED READY IMAGE
rollout-demo-6bbbc96696 3 3 busybox:1.37
rollout-demo-7545d5669f 0 <none> busybox:1.36
Giờ có hai ReplicaSet: cái mới 6bbbc96696 (busybox:1.37) đang giữ 3 Pod, cái cũ 7545d5669f (busybox:1.36) bị hạ về 0 — nhưng không bị xóa. Đây là mấu chốt của rollback: ReplicaSet cũ được giữ lại ở 0 bản sao như một "ảnh chụp" phiên bản trước, sẵn sàng được nâng lại. kubectl rollout history cho thấy các phiên bản đó:
kubectl rollout history deployment/rollout-demo
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Mỗi revision ứng với một ReplicaSet. Số ReplicaSet cũ được giữ do .spec.revisionHistoryLimit quy định, mặc định 10 — quá số đó thì ReplicaSet cũ nhất bị dọn.
Rollback một lệnh
Giả sử busybox:1.37 hỏng và cần lùi gấp. Vì ReplicaSet cũ còn nguyên ở 0, rollback chỉ là nâng nó lại — kubectl rollout undo làm đúng vậy:
kubectl rollout undo deployment/rollout-demo
kubectl rollout status deployment/rollout-demo
kubectl get deployment rollout-demo -o jsonpath='image={.spec.template.spec.containers[0].image}{"\n"}'
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'
deployment "rollout-demo" successfully rolled out
image=busybox:1.36
NAME DESIRED IMAGE
rollout-demo-6bbbc96696 0 busybox:1.37
rollout-demo-7545d5669f 3 busybox:1.36
Image của Deployment quay về busybox:1.36, và để ý: nó không tạo ReplicaSet thứ ba — nó tái dùng đúng cái RS cũ 7545d5669f, nâng từ 0 lên 3, hạ cái 1.37 về 0. Rollback chỉ là một rolling update theo chiều ngược, dùng lại ảnh chụp có sẵn. Vì thế nó nhanh và chắc: không build lại, không kéo image mới. (rollout undo --to-revision=N cho lùi về một revision cụ thể.)
Một cảnh báo thực tế hiện ra khi chạy lệnh này trên Deployment từng tạo bằng kubectl apply:
Warning: resource deployments/rollout-demo was previously managed with 'kubectl apply'.
Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation ...
Tức rollout undo đổi cluster nhưng không sửa file YAML/annotation last-applied của bạn, nên lần kubectl apply kế tiếp (từ file cũ chứa 1.37) sẽ vô tình "rollout tới" lại. Bài học GitOps: rollback nên đi kèm sửa nguồn cấu hình, đừng để cluster và Git lệch nhau.
Scale không phải rollout
Tài liệu nói scale không trigger rollout — kiểm chứng. Đổi số bản sao chứ không đụng template:
kubectl scale deployment/rollout-demo --replicas=4
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'
kubectl rollout history deployment/rollout-demo
NAME DESIRED IMAGE
rollout-demo-6bbbc96696 0 busybox:1.37
rollout-demo-7545d5669f 4 busybox:1.36
REVISION CHANGE-CAUSE
2 <none>
3 <none>
ReplicaSet đang dùng 7545d5669f chỉ đơn giản lên 4, không có ReplicaSet mới, không thêm revision (history vẫn 2 và 3). Đúng như tài liệu: scale là chỉnh .spec.replicas chứ không chỉnh .spec.template, nên Deployment xem đó là cùng một phiên bản, chỉ thay số lượng. Đây là lý do bạn scale thoải mái mà không "đốt" lịch sử rollout.
🧹 Dọn dẹp
kubectl delete deployment rollout-demo
Xóa Deployment kéo theo cả ReplicaSet (cả cái đang ở 0) và toàn bộ Pod qua chuỗi ownerReferences — đây là garbage collection của Bài 17 làm việc. Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 24-deployment.
Tổng kết
Deployment không quản Pod trực tiếp mà qua ReplicaSet: mỗi phiên bản pod template ứng một ReplicaSet (đánh dấu bằng pod-template-hash), ReplicaSet giữ đúng số Pod, Deployment điều phối nhiều ReplicaSet. Rollout chỉ kích hoạt khi .spec.template đổi (đổi image, label...) — khi đó Deployment tạo ReplicaSet mới và rolling update nâng nó lên/hạ cái cũ xuống theo maxSurge/maxUnavailable (mặc định 25%), giữ ReplicaSet cũ ở 0 làm ảnh chụp. Rollback (kubectl rollout undo) tái dùng ReplicaSet cũ đó, nâng lại — nhanh vì không build/pull mới (nhớ đồng bộ lại nguồn cấu hình kẻo apply sau lại lùi ngược). Scale chỉ đổi .spec.replicas, không đụng template nên không tạo revision. Số revision giữ lại do revisionHistoryLimit (mặc định 10).
Bài 25 sang StatefulSet — controller cho ứng dụng có trạng thái: pod có danh tính ổn định (tên cố định, thứ tự khởi động/tắt có quy luật) và mỗi pod giữ volume riêng, khác hẳn các Pod "đàn cá" vô danh thay thế lẫn nhau của Deployment.