Rollback и revision history: kubectl rollout
Rolling update — это половина истории Deployment. Вторая половина — rollback: возможность откатиться на любую предыдущую ревизию одной командой. K8s хранит историю не как git-лог, а как набор ReplicaSet-ов с replicas=0 в etcd. Каждый старый RS — это снимок template-а на момент той ревизии.
В этом уроке разбираем, как именно работают kubectl rollout history, undo, pause/resume и где они проигрывают git-у — а где наоборот спасают в production.
GitHub Actions: build + push Docker-образа
kubectl rollout: команды
# Прогресс текущего rollout
kubectl rollout status deploy/web
# Waiting for deployment "web" rollout to finish: 2 of 4 updated replicas are available...
# deployment "web" successfully rolled out
# История ревизий
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 1 <none>
# 2 Bump nginx 1.27 -> 1.28
# 3 CVE-2024-1234 hotfix
# Детали конкретной ревизии (показывает template)
kubectl rollout history deploy/web --revision=2
# Откат на предыдущую (revision - 1 от current)
kubectl rollout undo deploy/web
# Откат на конкретную
kubectl rollout undo deploy/web --to-revision=2
# Пауза текущего rollout (или предотвращение нового)
kubectl rollout pause deploy/web
# Возобновить
kubectl rollout resume deploy/web
# Перезапустить Pod-ы без изменения template (например, перечитать ConfigMap)
kubectl rollout restart deploy/web
Как K8s хранит ревизии: ReplicaSet snapshots
Каждый раз когда spec.template Deployment-а меняется, controller создаёт новый ReplicaSet с новым template и pod-template-hash. Старый RS не удаляется — он остаётся в etcd с replicas=0.
kubectl get rs -l app=web --show-labels
# NAME DESIRED CURRENT READY LABELS
# web-7d4b9f4f7 0 0 0 pod-template-hash=7d4b9f4f7
# web-8a1c3d2e5 0 0 0 pod-template-hash=8a1c3d2e5
# web-9c2e8a55b 4 4 4 pod-template-hash=9c2e8a55b (current)
Это и есть “история ревизий” — набор пустых RS с разными pod-template-hash. На каждом RS есть аннотация deployment.kubernetes.io/revision: "N", которая определяет, какой номер ревизии этот RS представляет.
# Посмотреть на annotations
kubectl get rs web-7d4b9f4f7 -o jsonpath='{.metadata.annotations}'
# {
# "deployment.kubernetes.io/desired-replicas": "4",
# "deployment.kubernetes.io/max-replicas": "5",
# "deployment.kubernetes.io/revision": "1",
# "kubernetes.io/change-cause": "Initial deploy"
# }
Pod-template-hash — это deterministic hash от spec.template. Если template вернуть точно к старому состоянию (вплоть до байта), пересоздаётся НЕ новый RS — а используется тот, который уже был. Поэтому история ревизий может быть прерывистой по нумерации.
kubectl rollout undo: как работает
kubectl rollout undo deploy/web не “удаляет” текущую ревизию и не “восстанавливает” старую. Он делает следующее:
- Берёт template из target RS (default — previous revision)
- Копирует template обратно в
spec.templateDeployment-а - DeploymentController замечает изменение template → создаёт новую ревизию с этим template
- Запускается обычный rolling update от текущей версии к “восстановленной”
То есть undo — это просто rolling update в обратную сторону. После undo номер ревизии увеличивается, а не уменьшается.
# До
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 1 Initial
# 2 Bump to v2
# 3 Bump to v3
# Откатываемся на rev 1
kubectl rollout undo deploy/web --to-revision=1
# После
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 2 Bump to v2
# 3 Bump to v3
# 4 Initial <- новая запись, template взят из rev 1
Если template для нового шага совпадает с одним из существующих RS (тот же pod-template-hash) — K8s “восстанавливает” этот RS вместо создания нового. В нашем примере rev 4 — это тот же RS, что был для rev 1, просто его scale up до 4.
Rollback скейлит существующий RS обратно вверх, а не создаёт Pod-ы с нуля. Это значит rollback очень быстрый — образ уже в кэше нод, controller просто меняет rs.spec.replicas с 0 на N.
revisionHistoryLimit: сколько хранить
spec:
revisionHistoryLimit: 5 # default 10
Сколько старых RS с replicas=0 хранить в etcd. После лимита самые старые RS удаляются — на эти ревизии rollback больше невозможен.
# revisionHistoryLimit=3
# История после 5 деплоев:
# rev 4 (был rev 1, удалён)
# rev 5 (был rev 2, удалён)
# rev 6 (current - 2)
# rev 7 (current - 1)
# rev 8 (current)
kubectl rollout undo deploy/web --to-revision=4
# error: unable to find specified revision 4 in history
Trade-off:
revisionHistoryLimit=10(default) — глубокая история, больше места в etcdrevisionHistoryLimit=3— production-safe, держим только текущую + пару назад для quick rollbackrevisionHistoryLimit=0— НИ ОДИН RS не сохраняется, rollback невозможен. Только для эфемерных Deployments или когда rollback реализован внешне (Helm, ArgoCD).
revisionHistoryLimit=0 — частая ошибка в Helm chart-ах. Helm сам управляет rollback через свою историю релизов, но если Deployment имеет limit=0, helm rollback не сможет вернуть старый Pod template — Helm применит старый YAML, но в кластере не будет старого RS для quick scale-up. Pod-ы пересоздаются с нуля, медленнее.
change-cause annotation
kubectl rollout history показывает CHANGE-CAUSE колонку — короткое описание, что именно изменилось в этой ревизии. По умолчанию пусто (<none>).
Раньше существовал флаг kubectl apply --record или kubectl set image --record, который автоматически сохранял команду в annotation. С v1.22 он deprecated. Теперь корректный способ — явно ставить annotation:
kubectl annotate deploy/web kubernetes.io/change-cause="Bump nginx 1.27 -> 1.28 for CVE-2024-1234" --overwrite
Или сразу в YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
annotations:
kubernetes.io/change-cause: "Bump nginx 1.27 -> 1.28 for CVE-2024-1234"
spec:
...
Annotation копируется в каждый новый RS как часть metadata, поэтому kubectl rollout history потом её читает.
kubernetes.io/change-cause имеет смысл проставлять до или одновременно с изменением template. Если поставить после kubectl set image, аннотация попадёт в Deployment, но RS уже создан без неё — rollout history для этой ревизии покажет <none>. На CKAD задание про change-cause — это типовое, поставить аннотацию правильно.
pause / resume: manual gating
kubectl rollout pause deploy/web
# Любые изменения spec.template НЕ триггерят rollout, пока paused
kubectl set image deploy/web nginx=nginx:1.28
# Image обновлён в Deployment.spec, но новый RS НЕ создаётся
kubectl rollout resume deploy/web
# Теперь controller замечает изменение и стартует rolling update
Use cases:
- Batch changes: pause → set image + set env + set resources → resume. Все изменения уйдут в один rollout (одну новую ревизию), вместо трёх.
- Manual canary: pause → создать canary Deployment с новой версией → проверить → если ok, resume основной Deployment с тем же image.
- CI/CD interruption: pause во время инцидента, чтобы блокировать неожиданные deploy-ы извне (хотя ArgoCD это игнорирует).
# Manual canary через pause
kubectl rollout pause deploy/web
kubectl set image deploy/web nginx=nginx:1.28
# В этот момент template Deployment-а изменён, но RS не создан
# Создаём временный canary Deployment с тем же image
kubectl create deploy web-canary --image=nginx:1.28 --replicas=1
# Тестируем canary через temp Service
# Если ok:
kubectl delete deploy web-canary
kubectl rollout resume deploy/web # запускается основной rollout
Pause НЕ останавливает уже запущенный rolling update — он предотвращает запуск нового. Если rollout уже идёт, pause поставит deployment в состояние, где новые шаги не выполняются, но текущий шаг доедет до конца. Для полной остановки — pause + scale текущего нового RS обратно к старому значению.
kubectl rollout status в CI/CD
kubectl set image deploy/web nginx=nginx:1.28
kubectl rollout status deploy/web --timeout=10m
# exit 0 — rollout successful
# exit 1 — timeout or rollout failed (например, ImagePullBackOff)
kubectl rollout status блокирующая команда — она ждёт пока:
- Все новые Pods достигнут Ready
- Старый RS scaled to 0
- Или
progressDeadlineSecondsистёк → Deployment.status.conditions.Progressing=False, Reason=ProgressDeadlineExceeded
В CI/CD это типовой паттерн:
#!/bin/bash
set -e
kubectl set image deploy/web nginx=nginx:1.28
if ! kubectl rollout status deploy/web --timeout=10m; then
echo "Rollout failed, rolling back"
kubectl rollout undo deploy/web
exit 1
fi
echo "Rollout successful"
Killer-моменты
undo— это rolling update в обратную сторону, не “восстановление”. Создаётся новая ревизия с template старой. Номер ревизии увеличивается после undo.pause+set image+resume= manual canary. Можно собрать множество изменений в одну ревизию или вручную проверить состояние перед rollout.revisionHistoryLimit=0ломает rollback. Используется только когда rollback управляется снаружи (Helm, ArgoCD). Default 10 — для dev. Production обычно 3-5.--recordудалён (deprecated v1.22, removed в последующих релизах). Теперьkubectl annotate deploy/X kubernetes.io/change-cause="..."— единственный правильный способ.rollout restartдобавляет аннотациюkubectl.kubernetes.io/restartedAt: <timestamp>в template → triggers rollout, без изменения image. Используется чтобы перечитать ConfigMap-ы.