Skip to Content
How-toОткат деплоя

Как откатить деплой сервиса

Что делать, когда новый релиз в prod оказался хуже предыдущего: пошли 5xx, p99 деградировал, forwarder встал, consumer-lag растёт. Эта страница — про откат деплоя (Docker-образа), не про откат миграции БД. Миграция откатывается отдельно (чаще — forward-fix, см. rollback-migration), и работает она корректно только если была оформлена по expand-contract (см. ../conventions/db-pgx#expand-contract).

Общая процедура деплоя — deploy-service.

Содержание

Решение: откат или форвард-fix

Первое решение, которое принимает on-call, — откатывать или чинить вперёд. Критерии:

СимптомОткат или forward-fix
Fast-burn SLO alert (14.4× за 5m/1h) сработал после cutoverОткат немедленно, разбор потом
Растёт доля 5xx > 1% на нескольких endpoint’ахОткат
Один endpoint вернулся 500-ми из-за очевидного null-pointer / panicОткат (меньше риска, чем hotfix за 5 минут)
p99 вырос на 50%+ без видимых 5xxЕсли SLO горит — откат; если нет — разобраться, потом форвард-fix
Outbox встал, forwarder не публикуетОткат
Consumer-группа ушла в rebalance-loopОткат
Логически корректный релиз, но одна опечатка в endpoint’е (404 вместо 400)Forward-fix в рабочее время
Миграция прошла, но код-релиз сломалсяОткат кода (миграция остаётся, expand-совместима)

Правило: если не уверен — откатывай. Откат успешного релиза стоит 5 минут — потери от долгого инцидента измеряются SLO- бюджетом и нервами пользователей.

Forward-fix допустим только когда:

  1. Root cause понятен за 2–3 минуты анализа.
  2. Фикс — одно изменение, которое можно merge’ить, CI прогоняется за <15 минут, staging успевает принять.
  3. Откат не быстрее (например, новая миграция contract-типа уже применена и prod-код под неё не работает в старом виде).

Во всех остальных случаях — откат, форвард-fix следующим релизом.

Immediate mitigation

До самого отката — 1–2 минуты, в которые стоит снизить blast-radius:

  1. Объявить инцидент в on-call канале: «SEV-2: <svc> деплой vX.Y.Z — fast-burn SLO, откатываем». Одна строка, не эссе.

  2. Остановить дальнейшие деплои. Если CI/CD раскатывает по тегам — заморозить ветку / tag’и:

    # пример, конкретика — в infra-репо git tag -d vX.Y.Z # локально, для наглядности # плюс: lock в CI — pause deploys until further notice
  3. Если деплой ещё идёт (rolling в процессе) — можно прервать:

    kubectl rollout pause deployment/<svc> -n backend

    Это заморозит переключение pod’ов, часть реплик останется на старой версии — уже хоть что-то.

  4. Если есть canary — снять 100% трафика с canary:

    # зависит от mesh / Cilium Gateway API конфига, обычно патч weight = 0
  5. Снять зависимые джобы, если они могут умножить ущерб (CronJob’ы, которые читают новую колонку из БД и падают — kubectl patch cronjob <x> -p '{"spec":{"suspend":true}}').

После этого — откат.

Откат образа через Helm / kustomize

GitOps-first: правильный rollback — это commit в infra-репо, возвращающий image.tag на предыдущее значение. Не helm rollback в runtime — он рассинхронизируется с git, GitOps-sync (ArgoCD / Flux) при следующей проверке восстановит новую версию и откат откатится.

Шаблон через commit в infra-репо

  1. Определить предыдущий рабочий тег:

    # последний тег до инцидента git -C infra log -- charts/<svc>/values.prod.yaml --oneline | head -5
  2. Revert-коммит:

    git -C infra revert <sha-of-bad-deploy> --no-edit git -C infra push

    GitOps-sync подхватит через минуту-две, Kubernetes запустит rolling update на старую версию.

  3. Проверка, что pod’ы фактически откатились:

    kubectl get pods -l app=<svc> -n backend \ -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}' # должны показывать старый тег, например ghcr.io/.../<svc>:v1.4.1

Быстрый rollback без GitOps (аварийный)

Если GitOps-sync медленный (>5 мин) или недоступен, допустим прямой helm rollback c последующим commit’ом в infra-репо сразу после стабилизации. Без commit’а нельзя оставлять — следующий sync затрёт rollback.

helm history <svc> -n backend # найти revision до bad-deploy helm rollback <svc> <revision> -n backend --wait --timeout 5m

Сразу после — revert-коммит в infra-репо, как в основном пути.

Подтверждение успешного отката

  • http_requests_total{code=~"5.."} возвращается к pre-deploy уровню за 1–5 минут.
  • SLO burn-rate fast-alert перестаёт срабатывать.
  • Латенси p95/p99 возвращается к baseline.
  • /readyz на всех pod’ах старой версии — 200.

Если не возвращается — root cause не в образе, откат образа ничего не решил. Типичные причины:

  • Миграция contract-типа успела применить constraint, старый код в него не пишет. См. следующий раздел.
  • Упал shared upstream (БД primary, Kafka broker) — симптом совпал с cutover’ом, но не причинно. См. ../troubleshooting/failure-modes-matrix.

Откат миграции — отдельная тема

Важная часть: откат деплоя не откатывает миграцию. Если релиз содержал expand-миграцию, она осталась применённой — и это правильно, старый код с expand-изменением работает.

Если релиз сломал expand-contract-правило (миграция применила contract одновременно с новым кодом — например, SET NOT NULL на колонку, которую старый код не заполняет), тогда откат кода недостаточен: старый код нарушит NOT NULL constraint.

В таком случае:

  1. Сначала применить forward-fix-миграцию, снимающую contract-ограничение (ALTER COLUMN ... DROP NOT NULL).
  2. Затем откатить образ.

Откат down-миграции в prod по умолчанию не используется — см. rollback-migration. Штатный путь — новая forward-fix-миграция, не migrate down.

Синхронизация с feature flags

Если новая логика была под feature flag (FEATURE_X=false в pre-deploy) — сначала выключи флаг, потом разбирайся:

  1. Правка ConfigMap / env в infra-репо: FEATURE_X: "false".
  2. kubectl rollout restart deployment/<svc> -n backend (или hot-reload, если поддерживается).
  3. Наблюдай 5 минут — если симптом ушёл, root cause в коде под флагом. Образ не откатываешь; флаг остаётся выключенным до фикса.
  4. Если флаг выключен, а симптомы остаются — проблема в чём-то ещё, переходи к откату образа.

Это быстрее, чем rollback всего релиза, и сохраняет полезные изменения, которые успели поехать в этом же образе.

Наблюдение после отката

После отката — тот же цикл наблюдения, что при деплое:

  • SLO burn-rate perестал ли срабатывать? Должен.
  • Error-rate, p95/p99, outbox/consumer lag вернулись к baseline?
  • Нет ли новых алертов, связанных с откатом (старый образ, например, не умеет читать свежую схему из БД)?

30 минут on-call на связи, даже если первые 5 минут всё выглядит зелёным.

Если откат не помог — см. §Откат образа через Helm / kustomize, подтверждение и возможные причины. Эскалация: lead-инженер backend

  • owner downstream, если симптомы ушли в соседний сервис.

Post-mortem

Любой откат = incident review в ближайший рабочий день. Шаблон живёт в infra-репо (docs/post-mortems/) либо в handbook отдельной страницей (если её ещё нет — заведи отдельным PR). Обязательный минимум:

  • Timeline. Merge → deploy → первый симптом → решение откатить → полный rollback → end-of-incident. Время в UTC.
  • Impact. Сколько пользователей, сколько запросов упало, сколько процентов SLO-budget’а сгорело.
  • Root cause. Что именно было не так в коде / конфиге / миграции.
  • What worked. Хорошая диагностика, быстрый откат, честный alert.
  • What didn’t. Где alert запоздал, где staging не поймал, где CI пропустил.
  • Action items. С owner’ом и сроком. «Добавить unit-тест на X» / «проверить expand-contract на PR-ревью» / «уменьшить chi- timeout с 60s до 30s».

Action items не хоронятся в документе. Каждый — issue в репо соответствующего сервиса, со ссылкой на post-mortem. Следующий post-mortem начинается с «что закрыли с прошлого раза».

Post-mortem — blameless. Не «кто выкатил» — а «какой процесс / инструмент / правило должен был это поймать, но не поймал».

Что не делать

  • Не откатываться «ещё раз назад», минуя rollback. Если образ v1.4.2 плохой, а v1.4.1 рабочий — откатываемся ровно на v1.4.1. Не прыгаем на v1.3.0 — разрыв в версиях делает forward-fix невозможным.
  • Не делать helm rollback без последующего commit’а в infra- репо. GitOps-sync через 5 минут вернёт bad-deploy.
  • Не пушить hotfix-tag прямо в prod без staging-прохода «по горячему». Staging дешевле, чем второй откат.
  • Не удалять bad-deploy tag из git. Он нужен для post-mortem: найти PR, commits, ревьюеров. Остаётся в репо навсегда.
  • Не пропускать post-mortem. «Откатились, всё работает, забыли» = тот же баг приедет через квартал.
  • Не откатывать деплой, если root cause — shared upstream. Пример: Kafka-brokers проседают, сервис пишет 5xx. Откат не поможет — старая версия упадёт так же. Иди в ../troubleshooting/failure-modes-matrix.
  • Не «тихо» откатывать. Объявление в on-call канале — часть процедуры. Тихий откат мешает параллельным инцидентам и лишает команду контекста.

Связанные разделы

Last updated on