Learning Platform
Глоссарий Troubleshooting
Урок 17.02 · 22 мин
Продвинутый
Rollbackrollout historyrevisionHistoryLimitchange-causerollout pauserollout status

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 представляет.

Структура revision history
DeploymentСам Deployment держит ТЕКУЩИЙ template в spec. История ревизий не лежит в Deployment-объекте — она распределена между ReplicaSet-ами с annotation deployment.kubernetes.io/revision.
owns multiple RS
rev 1RS web-7d4b9f4f7, replicas=0. Аннотация deployment.kubernetes.io/revision=1. Template здесь — это первоначальная версия. Это и есть 'снимок' для возможного rollback.
rev 2RS web-8a1c3d2e5, replicas=0. revision=2. Template отличается от rev 1 — другой image или env. Pod-ов нет, но template сохранён в spec.template самого RS.
rev 3RS web-9c2e8a55b, replicas=4 (=desired Deployment). revision=3. Это текущая активная ревизия. Pod-ы созданы по этому template.
# Посмотреть на 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"
# }
NOTE

Pod-template-hash — это deterministic hash от spec.template. Если template вернуть точно к старому состоянию (вплоть до байта), пересоздаётся НЕ новый RS — а используется тот, который уже был. Поэтому история ревизий может быть прерывистой по нумерации.


kubectl rollout undo: как работает

kubectl rollout undo deploy/web не “удаляет” текущую ревизию и не “восстанавливает” старую. Он делает следующее:

  1. Берёт template из target RS (default — previous revision)
  2. Копирует template обратно в spec.template Deployment-а
  3. DeploymentController замечает изменение template → создаёт новую ревизию с этим template
  4. Запускается обычный 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.

TIP

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) — глубокая история, больше места в etcd
  • revisionHistoryLimit=3 — production-safe, держим только текущую + пару назад для quick rollback
  • revisionHistoryLimit=0 — НИ ОДИН RS не сохраняется, rollback невозможен. Только для эфемерных Deployments или когда rollback реализован внешне (Helm, ArgoCD).
WARNING

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 потом её читает.

NOTE

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
WARNING

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-ы.

Проверка знанийKnowledge check
После `kubectl rollout undo deploy/web --to-revision=1` ты делаешь `kubectl rollout history`. Какой номер ревизии будет назначен 'восстановленному' состоянию?
ОтветAnswer
Назначается СЛЕДУЮЩИЙ свободный номер, а не возврат к 1. Если до undo была история rev 1, 2, 3 — после undo появится rev 4 с template из rev 1 (rev 1 при этом может исчезнуть из списка, если этот RS физически переиспользован — controller scaled его обратно вверх). rollback это не 'удалить current и восстановить старое', это 'применить старый template как новую ревизию'. Это сохраняет линейность истории — можно потом сделать undo обратно на rev 3 и вернуться к версии, которую только что откатили.
Проверка знанийKnowledge check
Почему `revisionHistoryLimit=0` ломает Helm rollback, хотя сам Helm хранит свою историю релизов?
ОтветAnswer
Helm хранит ChartYAML и values предыдущих релизов в Secret-ах, и при `helm rollback` ре-применяет старый YAML. Если бы старый ReplicaSet существовал в кластере с replicas=0 — controller просто скейлит его обратно вверх (быстро, образы уже в кэше нод). Но с revisionHistoryLimit=0 при каждом обновлении старый RS УДАЛЯЕТСЯ. При helm rollback Deployment получает старый template, но в кластере нет соответствующего RS — controller создаёт новый RS с этим template, Pod-ы тоже создаются заново (Container Image Pull, init и т.д.). Откат становится медленным и стрессовым. Production: revisionHistoryLimit=3-5.
Проверка знанийKnowledge check
kubectl rollout pause deploy/web → kubectl set image deploy/web nginx=nginx:1.28 → kubectl set env deploy/web LOG_LEVEL=debug → kubectl rollout resume deploy/web. Сколько новых RS создастся и сколько ревизий появится в history?
ОтветAnswer
Один новый RS и одна новая ревизия. В paused-состоянии controller не реагирует на изменения template — он копит их в spec.template Deployment-а. После resume controller видит изменённый template (image+env вместе), генерирует ОДИН новый pod-template-hash и создаёт ОДИН новый RS. Без pause это были бы 2 ревизии (RS на каждое изменение). Это типовой production-приём для batch-изменений: pause → накопить все изменения → resume. Полезно когда update состоит из нескольких kubectl-команд, чтобы не было промежуточных rollout-ов.
Проверка знанийKnowledge check
kubectl apply --record был типовым способом проставить change-cause. С v1.22 он deprecated. Какой правильный способ сейчас и почему просто `kubectl annotate` после `kubectl set image` иногда не работает?
ОтветAnswer
Правильный способ: `kubectl annotate deploy/web kubernetes.io/change-cause='...' --overwrite`. Проблема: annotation проставляется на Deployment, и потом КОПИРУЕТСЯ в новый RS как часть его metadata при создании. Если поставить annotation ПОСЛЕ kubectl set image, controller уже создал новый RS без неё — rollout history покажет <none> для новой ревизии. Аннотацию надо ставить ДО или одновременно с изменением template — через kubectl apply (template и annotation в одном YAML), или через kubectl annotate с --overwrite, выполненную перед kubectl set image. На CKAD: лучше готовить YAML с annotation в metadata, apply, тогда история всегда чистая.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. kubectl rollout undo deploy/web --to-revision=1 — что технически делает эта команда?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5