Deployment: основной workload для stateless apps
Deployment — то, чем вы будете запускать ~80% workload-ов в production. И ~80% задач на CKAD-экзамене. Это высокоуровневый объект, который сидит над ReplicaSet и Pod-ами и решает за вас три вещи, которые иначе пришлось бы делать руками: постепенное обновление (rolling update), хранение истории (revisions), откат на любую предыдущую версию (rollback).
Понять Deployment — значит понять, как одна декларативная декларация превращается в сложную хореографию из множества RS и Pod-ов, координируемых через reconcile loops.
Зачем нужен Docker Compose: от одиночных контейнеров к сервисам
Базовый Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 64Mi
Ключевые поля:
spec.replicas— желаемое число Pod-овspec.selector— label selector (immutable после создания)spec.template— Pod template (можно менять — это триггерит rollout)spec.strategy— стратегия обновления:RollingUpdate(default) илиRecreatespec.revisionHistoryLimit— сколько старых ReplicaSet хранить для rollback (default 10)spec.progressDeadlineSeconds— за сколько секунд rollout должен завершиться, иначе помечается failed (default 600)
Иерархия: Deployment → ReplicaSet → Pod
Ключевая модель: Deployment не создаёт Pod-ы напрямую. Он создаёт ReplicaSet, а тот уже создаёт Pod-ы. Каждая ревизия Deployment-а — это новый ReplicaSet.
Подсмотрим в кластер после обновления:
kubectl get deploy,rs,pods -l app=web
# NAME READY UP-TO-DATE AVAILABLE
# deployment.apps/web 3/3 3 3
#
# NAME DESIRED CURRENT READY
# replicaset.apps/web-7d4b9f4f7 0 0 0 # ← старый, scaled to 0
# replicaset.apps/web-9c2e8a55b 3 3 3 # ← новый, active
#
# NAME READY STATUS
# pod/web-9c2e8a55b-abc12 1/1 Running
# pod/web-9c2e8a55b-def34 1/1 Running
# pod/web-9c2e8a55b-xyz56 1/1 Running
Старый RS не удаляется — он остаётся “пустым” (replicas=0) и хранится в etcd для возможного rollback. Сколько таких пустых RS хранить — определяется revisionHistoryLimit.
DeploymentController автоматически добавляет в Pod template уникальный label pod-template-hash — это deterministic хеш от template. Selector каждого RS включает этот hash, поэтому каждый RS управляет только Pods своей ревизии и они не путаются.
RollingUpdate: maxSurge и maxUnavailable
Стратегия по умолчанию — RollingUpdate. Контроллер постепенно скейлит новый RS вверх и старый RS вниз, поддерживая инвариант доступности.
Управляется двумя параметрами:
- maxSurge — максимальное число Pod-ов СВЕРХ
spec.replicas, которые могут существовать одновременно. Default25%. Можно абсолютное число. - maxUnavailable — максимальное число Pod-ов, которые могут быть НЕДОСТУПНЫ в любой момент. Default
25%. Можно абсолютное число.
Например, replicas=10, maxSurge=2, maxUnavailable=2:
- Минимум
10 - 2 = 8Pod-ов доступно всегда - Максимум
10 + 2 = 12Pod-ов существует одновременно - Controller балансирует: создаёт новые до лимита 12, ждёт ready, удаляет старые до лимита 8 доступных
Когда maxSurge=0, maxUnavailable=0
Нельзя оба сразу — controller заблокирован: новый Pod создать нельзя (surge=0), старый удалить нельзя (unavailable=0). API server отклонит такую конфигурацию.
maxSurge=100%, maxUnavailable=0 — blue/green-стиль
Сначала создаются ВСЕ новые Pods (replicas стало 2×), ждут ready, потом все старые удаляются. Без downtime, но удваивается потребление ресурсов на время rollout.
maxSurge=0, maxUnavailable=100% — fast roll, с downtime
Все старые сразу killed, потом создаются новые. Похоже на Recreate, но в рамках RollingUpdate стратегии.
В CKAD-задачах часто требуется “обновить deployment без downtime”. Это значит — оставить дефолтные RollingUpdate с maxSurge > 0 и maxUnavailable < replicas, плюс readinessProbe на контейнерах (без неё Pod считается ready как только запустился).
Recreate: для apps которые не выдерживают двух версий
spec:
strategy:
type: Recreate
При Recreate controller сначала убивает ВСЕ старые Pod-ы (новый RS scaled to 0), и только потом создаёт ВСЕ новые. Это даёт downtime, но гарантирует, что в любой момент времени работает только одна версия.
Use cases:
- Apps с миграциями БД, которые ломают совместимость старых клиентов
- Apps, использующие эксклюзивный ресурс (например, leader election с фиксированным именем)
- Apps, где две версии одновременно ломают консистентность (некоторые stateful workloads)
Rollout: триггеры обновления
Rolling update триггерится автоматически когда меняется ЛЮБОЕ поле в spec.template:
# image change (типичный)
kubectl set image deploy/web nginx=nginx:1.28
# environment variable
kubectl set env deploy/web LOG_LEVEL=debug
# resource limits
kubectl set resources deploy/web --limits=cpu=500m,memory=512Mi
# patch произвольного поля template
kubectl patch deploy/web -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:1.29"}]}}}}'
# rollout restart (форсирует пересоздание Pod-ов без изменения template)
kubectl rollout restart deploy/web
Изменение spec.replicas НЕ триггерит rollout — это просто scaling в рамках текущего RS. Rollout — только изменения в template.
kubectl rollout restart хитрый — он добавляет аннотацию kubectl.kubernetes.io/restartedAt: <timestamp> в template, что технически меняет template и триггерит создание нового RS. Используется когда нужно перезапустить Pods (например, чтобы перечитать ConfigMap-ы), но image остался тот же.
kubectl rollout: status, history, undo
# Следить за прогрессом rollout
kubectl rollout status deploy/web
# Waiting for deployment "web" rollout to finish: 2 out of 4 new replicas have been updated...
# deployment "web" successfully rolled out
# История ревизий
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 1 <none>
# 2 kubectl set image deploy/web nginx=nginx:1.28
# 3 kubectl set image deploy/web nginx=nginx:1.29
# Детали конкретной ревизии
kubectl rollout history deploy/web --revision=2
# Откат к предыдущей
kubectl rollout undo deploy/web
# Откат к конкретной ревизии
kubectl rollout undo deploy/web --to-revision=2
# Пауза (новые изменения не триггерят rollout)
kubectl rollout pause deploy/web
# Возобновить
kubectl rollout resume deploy/web
change-cause annotation
Раньше был флаг --record, который сохранял команду kubectl в аннотацию. С v1.22 он deprecated. Теперь правильный способ — добавлять аннотацию явно:
metadata:
annotations:
kubernetes.io/change-cause: "Bump to nginx 1.28 — CVE-2024-1234 fix"
Или через kubectl annotate:
kubectl annotate deploy/web kubernetes.io/change-cause="Bump to nginx 1.28" --overwrite
Эта аннотация показывается в kubectl rollout history как CHANGE-CAUSE и помогает понять, зачем была сделана конкретная ревизия.
revisionHistoryLimit
Каждая ревизия = старый RS остаётся в кластере с replicas=0. Это нужно для rollback. Но если ревизий накопится много, etcd распухнет.
spec.revisionHistoryLimit (default 10) ограничивает сколько старых RS хранить:
spec:
revisionHistoryLimit: 5
Когда ревизий больше лимита, controller удаляет самые старые. Нельзя откатиться на ревизию, RS которой уже удалён — поэтому нужно балансировать: больше history = больше места в etcd, но больше глубина rollback.
В production обычно ставят 3-5. Дефолт 10 — для удобства разработки.
revisionHistoryLimit=0 означает что НИ ОДИН старый RS не хранится — rollback невозможен. Использовать только для эфемерных Deployments, например в Helm-релизах с собственным rollback-механизмом.
Полный rollout: трассируем шаг за шагом
CKAD-сценарий: обновить image, проверить статус, откатиться при проблемах.
# Текущая ревизия
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 1 <none>
# Обновить image
kubectl set image deploy/web nginx=nginx:1.28
kubectl annotate deploy/web kubernetes.io/change-cause="Bump nginx 1.27 → 1.28"
# Следить за прогрессом
kubectl rollout status deploy/web
# ...
# deployment "web" successfully rolled out
# История теперь
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 1 <none>
# 2 Bump nginx 1.27 → 1.28
# Что-то пошло не так — откатываем
kubectl rollout undo deploy/web
# deployment.apps/web rolled back
# Проверяем
kubectl rollout history deploy/web
# REVISION CHANGE-CAUSE
# 2 Bump nginx 1.27 → 1.28
# 3 <none> ← rollback создаёт новую ревизию с template из rev 1
Rollback — это не “удалить новую ревизию”, а “создать новую ревизию с template из старой”. Поэтому после undo номер ревизии увеличивается. Это сохраняет линейность истории и позволяет потом откатиться обратно.
progressDeadlineSeconds: что значит “failed rollout”
spec:
progressDeadlineSeconds: 600
Если за это время rollout не сделал прогресс (новые Pods не стали ready), Deployment помечается со статусом Progressing=False, reason=ProgressDeadlineExceeded.
Это не останавливает rollout автоматически — controller продолжает пытаться. Но kubectl rollout status выйдет с non-zero exit code, что важно для CI/CD пайплайнов.
kubectl rollout status deploy/web --timeout=300s
# error: deployment "web" exceeded its progress deadline
echo $? # 1
kubectl: основные команды
# Создать декларативно
kubectl apply -f deploy.yaml
# Создать imperative
kubectl create deployment web --image=nginx:1.27 --replicas=3
# Сгенерировать YAML без создания
kubectl create deployment web --image=nginx:1.27 --replicas=3 --dry-run=client -o yaml
# Изменить image
kubectl set image deploy/web nginx=nginx:1.28
# Scale
kubectl scale deploy/web --replicas=5
# Описать
kubectl describe deploy/web
# Удалить (cascade — RS и Pods тоже удалятся)
kubectl delete deploy/web
kubectl describe deploy/web показывает:
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
OldReplicaSets: web-7d4b9f4f7 (0/0 replicas created)
NewReplicaSet: web-9c2e8a55b (3/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 5m deployment-controller Scaled up replica set web-9c2e8a55b to 1
Normal ScalingReplicaSet 5m deployment-controller Scaled down replica set web-7d4b9f4f7 to 2
...