Skip to Content
How-toОткатить миграцию

Как откатить миграцию в prod

Runbook для ситуации: миграция применилась в prod (через PreSync-Job Atlas), после деплоя сервис падает / корраптит данные / ведёт себя неверно — нужно вернуть схему в рабочее состояние. Это последнее средство, применяется только после того, как другие варианты (rollback кода, быстрый fix-forward миграции) не сработали.

Как писать миграции, про Atlas-модель (versioned SQL, atlas.sum, atlas_schema_revisions, PreSync-Job, роль <svc>_migrator) и expand-contract — в add-migration. Про типовые сбои самой миграции — ../troubleshooting/migration-fails.

Содержание

Главное правило: в prod нет down

Atlas — versioned, forward-only. В каталоге migrations/ нет down-файлов вообще: миграция — это одна NNNN_name.sql вперёд. Откат в prod — это новая миграция, которая возвращает схему в нужное состояние, а не «отмотать назад».

Почему не делаем reverse-DDL автоматом:

  • Down удалил бы колонку/таблицу с данными — необратимая потеря.
  • Down не знает про зависимые объекты, добавленные позже (FK, view, функции, триггеры) — DROP ... CASCADE порвёт связи.
  • atlas_schema_revisions — это журнал применённого. Ручная отмотка записи без сверки реальной схемы рассинхронизирует журнал и БД, и следующий atlas migrate apply пойдёт не в ту ветку DDL.

Единственное исключение — dev / staging, где данные можно потерять. Там при отладке самой миграции до merge можно сгенерировать обратный план:

# ТОЛЬКО dev/staging. Планирует обратные DDL по dev-базе и откатывает # последнюю применённую версию. В prod не запускаем. atlas migrate down --env k8s --dev-url "docker://postgres/16/dev"

В prod команда atlas migrate down не используется — только fix-forward.

Когда откатывать

  • Миграция добавила NOT NULL / CHECK, который ломается на существующих строках → сервис не стартует / падают записи после деплоя.
  • Миграция удалила колонку, которую всё ещё читает старая версия сервиса (rolling deploy не завершён — нарушен expand-contract).
  • Миграция ввела новый FK, и записи теперь нарушают его на production-данных.
  • Backfill перезаписал данные, которые нельзя было трогать (fired trigger, неверное условие WHERE).

Когда НЕ откатывать

  • Сервис падает не из-за схемы, а из-за кода — откатывай код, не миграцию (kubectl rollout undo).
  • Миграция добавила nullable-колонку → чуть больше размер таблицы, но ничего не ломается. Оставь, удалишь отдельной cleanup-миграцией позже.
  • Fix-forward уже готов и быстрее, чем откат. См. §Стратегия A.
  • Backfill ещё не закончился — не прерывай его посередине, это оставит схему в смешанном состоянии. Дождись, потом решай.

1. Остановить дальнейший ущерб

Перед любым SQL-вмешательством останови поток записи, который делает ситуацию хуже:

  • Rollback кода — если новая версия сервиса пишет «битые» данные через свежую схему, откати деплой до предыдущего образа (kubectl rollout undo deployment/<service> -n core). Это делается до работы со схемой.
  • Останови проблемный worker — если backfill-Job / CronJob продолжает портить данные, заскейль в 0 (kubectl scale cronjob/<name> --replicas=0 -n core или suspend).
  • Read-only mode, если сервис поддерживает (MAINTENANCE_MODE=true) — запретить записи, пока разбираешься.

Rollback кода и rollback схемы — разные операции. Код откатывается быстрее. Схему трогаем только если её эффект действительно нужно обратить.

2. Зафиксировать состояние

Миграции применяет роль <svc>_migrator через сессионный PgBouncer-алиас <db>_migrate (creds — секрет <svc>-migrator). Под ней и подключайся:

# журнал ревизий Atlas — источник правды о применённом atlas migrate status --env k8s
-- журнал ревизий напрямую (последние записи) SELECT version, description, type, applied, total, error IS NOT NULL AS failed FROM atlas_schema_revisions ORDER BY version DESC LIMIT 10; -- структура проблемной таблицы \d <schema>.<table> -- счётчики до вмешательства SELECT count(*) FROM <schema>.<table>; SELECT count(*) FROM <schema>.<table> WHERE <new_column> IS NULL; -- длинные транзакции, держащие lock SELECT pid, state, xact_start, query FROM pg_stat_activity WHERE state = 'idle in transaction' AND xact_start < NOW() - INTERVAL '30 seconds';

Запиши результаты в incident log (см. §6) — без этого через день не вспомнишь, что было «до».

Если есть свежий логический бэкап (pgBackRest / pg_dump) новее проблемной миграции — проверь, что он реально свежий. Восстановление целиком из бэкапа убивает всю активность с момента бэкапа; только при необратимой corruption’е.

3. Выбрать стратегию отката

СитуацияСтратегия
Миграция добавила constraint/index, который мешает — данные целыA: fix-forward миграция, отменяющая constraint
Миграция удалила колонку, которую читает старый кодB: rollback кода + compatibility-миграция, возвращающая колонку
Миграция упала посередине → ревизия failed в журналеC, см. ../troubleshooting/migration-fails
Backfill перезаписал данные неверноA + восстановление из audit-trail / бэкапа

Стратегия A: fix-forward миграция

Самый частый и безопасный путь. Пишем новую миграцию с номером N+1, которая отменяет то, что сделала проблемная миграция N.

Пример: 042_add_review_status_not_null.sql добавила NOT NULL, но на существующих строках status IS NULL → сервис падает при update.

atlas migrate new review_status_drop_not_null --env ci
-- migrations/043_review_status_drop_not_null.sql ALTER TABLE reviews ALTER COLUMN status DROP NOT NULL;
atlas migrate hash --env ci # обновить atlas.sum atlas migrate lint --env ci --latest 1

Порядок действий:

  1. Создай файл 043_*.sql через atlas migrate new, обнови atlas.sum через atlas migrate hash.
  2. Локально прогони atlas migrate apply на dev-копии — убедись, что фикс работает.
  3. Открой PR с описанием инцидента в description (короткое «что случилось, почему откатываем, как проверили»). atlas migrate lint в CI обязан быть зелёным.
  4. Merge → build миграционного образа → Argo подтянет тег → PreSync-Job применит 043 перед раскаткой Deployment.
  5. После восстановления схемы разбирайся, почему исходная миграция сломала prod (нет backfill, не тестировали на prod-подобных данных), фиксируй в пост-мортеме.

Этот путь не трогает atlas_schema_revisions руками, не использует reverse-DDL, forward-only. Именно его применяй в 90% случаев.

Стратегия B: revert кода + compatibility-миграция

Когда миграция DROP COLUMN удалила поле, а старая версия сервиса (или rolling deploy в процессе) всё ещё читает его → 500 на column "X" does not exist. Правильный expand-contract это исключил бы (см. add-migration), но если уже случилось:

  1. Откати код до версии, которая не читает поле (kubectl rollout undo). Это немедленно останавливает 500.

  2. Убедись, что новая схема совместима с откаченным кодом.

  3. Напиши compatibility-миграцию, которая возвращает колонку nullable:

    -- migrations/044_restore_review_old_field.sql ALTER TABLE reviews ADD COLUMN old_field TEXT; -- данные восстановить из бэкапа отдельным backfill-Job (см. §5)
  4. Восстанови данные в колонке, если нужны — см. §5.

После DROP COLUMN данных в колонке больше нет — «просто вернуть старый код» недостаточно, его надо сопроводить миграцией, возвращающей колонку.

Стратегия C: упавшая (failed) ревизия

Используется, когда PreSync-Job упал посередине миграции: часть DDL применилась, в atlas_schema_revisions ревизия помечена как failed (error IS NOT NULL, applied < total). Полный flow — в ../troubleshooting/migration-fails. Кратко:

  1. atlas migrate status --env k8s — увидишь версию в состоянии failed и на каком statement она упала.

  2. Предпочтительно — fix-forward. Если оставшиеся DDL безопасно до-применить, исправь причину и дай PreSync-Job повторить: Atlas продолжит с упавшего statement. (Миграции пишем так, чтобы шаги были идемпотентны — IF NOT EXISTS / IF EXISTS.)

  3. Если до-применить нельзя — отмени вручную уже применённые DDL под advisory-lock, затем приведи журнал в соответствие реальной схеме:

    # пометить версию как НЕ применённую (после ручной отмены её DDL) atlas migrate set <prev-version> --env k8s

    atlas migrate set переписывает журнал ревизий под фактическое состояние. Делать только после того, как схема физически приведена к <prev-version>, иначе журнал и БД разойдутся.

Ручное вмешательство в журнал — только аварийный механизм. В штатных случаях — Стратегия A.

4. Проверить atlas migrate status

После любого отката:

atlas migrate status --env k8s # ожидаем: "Migration Status: OK", No pending migrations, # Current — последняя версия из migrations/ репо, без failed-ревизий.

Ожидания:

  • Нет ревизий в состоянии failed.
  • Current-версия = последняя миграция в migrations/.
  • Нет pending-миграций (всё применено).

Проверь, что сервис стартует:

kubectl rollout restart deployment/<service> -n core kubectl logs -f deployment/<service> -n core # ожидаем старт без ERROR, readiness 200

Сам сервис миграции не прогоняет — это делает PreSync-Job. Стартап сервиса только проверяет, что схема на месте.

5. Восстановить данные, если backfill испортил

Если миграция/Job включали UPDATE, который перезаписал данные неверно — восстановление зависит от того, что есть:

a) Audit-trail таблица

Если в сервисе есть history-таблица (reviews_history с триггером) — источник истинных данных до миграции:

UPDATE reviews r SET status = h.status FROM reviews_history h WHERE h.review_id = r.id AND h.changed_at = ( SELECT MAX(changed_at) FROM reviews_history WHERE review_id = r.id AND changed_at < '2026-04-19 12:00:00'::timestamptz -- до миграции );

b) Логический бэкап

pg_restore на отдельную БД, INSERT INTO ... SELECT FROM с fetch’ем нужных строк. Никогда не накатывай бэкап поверх живой prod-БД — только через staging-копию.

c) Outbox/events

Если изменения публиковались через outbox, прошлые события (kazmaps.review.review) всё ещё в Kafka (retention 7 дней). Можно re-play’нуть последнее событие до миграции и получить payload старого состояния.

Что доступноИспользуй
Audit-traila — самый точный и быстрый
Только бэкапb — медленнее, но всегда доступно
Audit нет, но есть Kafka-событияc — точечно по ID
НичегоДанные утрачены. Инцидент = DLP. Фиксируй в incident log.

6. Incident log

После отката заведи запись (в сервис-репо docs/incidents/ или в системе инцидентов команды):

# 2026-04-19 review-service: откат миграции 042 ## Что случилось Миграция 042 добавила `NOT NULL` на `reviews.status` без backfill NULL-строк. После деплоя сервис падал в 100% PUT /v1/reviews. ## Как починили Стратегия A: миграция 043 сняла `NOT NULL`. Build образа → Argo → PreSync-Job применил 043 → rollout сервиса. ## Почему не поймали раньше `atlas migrate lint` не ловит data-зависимые падения: на staging нет исторических reviews с `status IS NULL`. ## Что сделаем - Тест миграций на snapshot prod-схемы + sample данных. - Чеклист PR: backfill для `SET NOT NULL` на существующих данных.

Без этой записи следующая аналогичная миграция повторит ту же ошибку.

Частые ошибки при откате

  • atlas migrate down в prod. Планирует reverse-DDL, дропает объекты с данными. В prod только fix-forward.
  • Редактирование уже применённой миграции. Файл 042_*.sql теперь делает другое, но atlas.sum и журнал хранят старый хеш → на новом окружении (fresh DB, после restore) получишь другое состояние, а atlas migrate validate упадёт на checksum mismatch. Пиши новую миграцию N+1.
  • Ручной правёж atlas_schema_revisions без сверки схемы. БД и журнал расходятся, следующий apply идёт не в ту ветку DDL. Используй atlas migrate set и только после физического приведения схемы.
  • Забыл advisory lock в ручных DDL при Стратегии C. Параллельный PreSync-Job/pod пытается то же — оба падают.
  • Не откатил код до миграции. Схему вернул, а код всё ещё использует новые поля → 500.
  • Восстановление из бэкапа без остановки writes. Бэкап накатывается поверх live-данных → потеряны записи с момента бэкапа.

Чеклист

  • Определил, нужен ли реально откат схемы, или достаточно rollback кода.
  • Остановил поток «битых» записей (rollback кода / read-only / worker scale=0).
  • Зафиксировал atlas migrate status, журнал ревизий, структуру таблицы и счётчики строк.
  • Выбрал стратегию (A / B / C) и обосновал выбор.
  • Fix-forward миграция создана через atlas migrate new + atlas migrate hash, atlas migrate lint зелёный.
  • Прогнал миграцию локально на snapshot-копии prod-данных.
  • PR создан с описанием инцидента, approve от lead.
  • После деплоя: atlas migrate status чистый (нет failed/pending), сервис стартует, readiness 200.
  • Данные восстановлены (если нужно) из audit-trail / бэкапа / events.
  • Incident log написан.
  • Post-mortem запланирован: почему не поймали на pre-prod.

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

Last updated on