OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Проверка бэкапов в CI: безопасное восстановление MySQL/PostgreSQL в изолированном стенде

Бэкап без тестового восстановления — лишь ощущение безопасности. Разбираем, как встроить автоматический restore в CI для MySQL и PostgreSQL: изолированный Docker‑sandbox, PITR и быстрые smoke‑проверки, хранение артефактов и требования к безопасности.
Проверка бэкапов в CI: безопасное восстановление MySQL/PostgreSQL в изолированном стенде

В продакшене нет ничего страшнее бэкапа, который «кажется рабочим». Пока вы не делали полноценный restore и не проверили результат, риск потерять данные и время остаётся высоким. В этой статье соберём воспроизводимый процесс backup restore в CI для MySQL и PostgreSQL: развернём изолированный sandbox на Docker, настроим восстановление как из логических дампов, так и с поддержкой PITR, и добавим автоматическую верификацию данных. Всё — без публикации портов наружу и с учётом безопасности.

Цели и подход

Нам нужны не просто файлы бэкапов, а уверенность, что:

  • восстановление запускается автоматически в CI по расписанию или по событию (появление нового бэкапа);
  • оно проходит в «песочнице» без доступа в интернет и внешних сервисов;
  • результат проверяется набором быстрых тестов: целостность схемы, выборочные агрегаты, версии миграций;
  • поддерживается сценарий точечного восстановления по времени (PITR), чтобы точно знать, что сможем вернуться на нужный момент;
  • отчёт о результатах доступен из логов CI, а артефакты (логи и контрольные отчёты) сохраняются для аудита.

Ключевая идея: в CI мы поднимаем ephemeral‑среду, восстанавливаем базу из актуальных бэкапов, проводим verification и уничтожаем окружение. Повторяемость важнее скорости, но скорость важна, чтобы тесты запускались регулярно.

Безопасность: минимальные привилегии и изоляция

Работа с реальными дампами подразумевает чувствительные данные. Минимальные правила для CI:

  • не публикуем порты баз наружу; используем внутреннюю сеть Docker и сервисы без host‑порта;
  • обнуляем egress (исключаем доступ в интернет) для контейнеров, где это возможно; если нужно — разрешаем доступ только к внутреннему реестру артефактов;
  • не используем продакшен‑пароли; у контейнеров свой логин/пароль, действительный только в CI;
  • скрываем секреты в переменных CI; включаем маскирование в логах;
  • очищаем тома после прогона, отключаем персистентность вне job;
  • для MySQL/PostgreSQL включаем ограничение памяти и CPU в контейнерах, чтобы не «долбануть» по агенту CI;
  • по возможности анонимизируем чувствительные колонки на этапе создания бэкапа или сразу после restore отдельным скриптом в sandbox.

Если в пайплайне требуется жёстче ограничить сетевой трафик контейнеров, пригодится детальный разбор iptables/nft для Docker — см. статью «Фаерволл для Docker и правила безопасности» по ссылке Docker firewall и безопасность.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Где брать бэкапы для CI

Потоки зависят от вашей стратегии:

  • логические дампы: mysqldump или mydumper для MySQL, pg_dump для PostgreSQL;
  • физические бэкапы: Percona XtraBackup для MySQL/MariaDB, pg_basebackup или специализированные инструменты для PostgreSQL;
  • PITR: binlog/GTID для MySQL, WAL для PostgreSQL.

В CI обычно стартуют с логических дампов — проще, быстрее поднять. Для валидации PITR лучше дополнить сценарием с binlog/WAL, чтобы проверить критический путь аварийного восстановления «до секунды». Для хранения бэкапов в объектном хранилище можно использовать проверенные инструменты — см. практику из материала «S3‑бэкапы: Restic/Borg» по ссылке S3‑backups Restic/Borg.

Схема источников бэкапов и изолированного CI‑sandbox

Docker sandbox: compose, сеть и ограничения

Создадим минимальный docker-compose.yml с двумя сервисами: MySQL и PostgreSQL. Никаких портов наружу, только внутренняя сеть. Ограничим ресурсы, отключим привилегии и смонтируем каталог с артефактами бэкапа только в режиме read‑only, а временные директории — как tmpfs при необходимости.

version: "3.9"

services:
  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=app
      - MYSQL_USER=app
      - MYSQL_PASSWORD=app_pass
      - MYSQL_ROOT_PASSWORD=root_pass
    networks:
      - internal
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "app", "-papp_pass"]
      interval: 5s
      timeout: 3s
      retries: 20
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2g
    security_opt:
      - no-new-privileges:true

  postgres:
    image: postgres:16
    environment:
      - POSTGRES_DB=app
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=app_pass
    networks:
      - internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 20
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2g
    security_opt:
      - no-new-privileges:true

networks:
  internal:
    internal: true

Дальше в CI job мы загрузим артефакты бэкапа в рабочий каталог, запустим compose, дождёмся healthcheck и выполним восстановление плюс проверочные запросы.

MySQL: восстановление из дампа и проверка

Базовый сценарий для логического дампа:

  1. Стартуем контейнер и ждём readiness.
  2. Стримим дамп в mysql без распаковки на диск.
  3. Запускаем smoke‑тесты (SELECT COUNT, проверка схемы, миграций).
# 1) Ожидание готовности
until docker compose exec -T mysql mysqladmin ping -h 127.0.0.1 -u app -papp_pass --silent; do sleep 2; done

# 2) Восстановление из gzip-дампа (пример: app.sql.gz)
gzip -cd artifacts/mysql/app.sql.gz | docker compose exec -T mysql mysql -u app -papp_pass app

# 3) Быстрая верификация
queries="
SELECT COUNT(*) AS users FROM users;
SELECT COUNT(*) AS orders FROM orders WHERE created_at > NOW() - INTERVAL 30 DAY;
SHOW TABLES;
"
printf "%s" "$queries" | docker compose exec -T mysql mysql -u app -papp_pass app

Если дамп большой, используйте mydumper/myloader и параллельную загрузку. Для контроля времени закладывайте таймаут на job и отдельный таймаут на restore.

MySQL PITR: применение binlog до нужного времени

PITR‑проверка нужна, чтобы убедиться, что binlog пригоден для восстановления «до секунды». В CI предположим, что у нас есть базовый дамп плюс набор binlog‑файлов за период. Восстановим дамп, затем «прокрутим» binlog до указанной метки времени:

# Восстановление базового дампа
gzip -cd artifacts/mysql/base.sql.gz | docker compose exec -T mysql mysql -u app -papp_pass app

# Применение binlog до времени RESTORE_TO (формат 'YYYY-MM-DD HH:MM:SS')
RESTORE_TO="2025-01-10 10:15:00"
for f in artifacts/mysql/binlog/mysql-bin.*; do
  docker compose exec -T mysql sh -lc "mysqlbinlog --stop-datetime='${RESTORE_TO}' /var/lib/mysql/$(basename $f) | mysql -u app -papp_pass app"
done

# Проверка контрольной точки (например, ожидаем, что заказ #123 существует)
printf "%s" "SELECT id,status FROM orders WHERE id=123;" | docker compose exec -T mysql mysql -u app -papp_pass app

Где брать binlog внутри контейнера? В реальной проверке скопируйте binlog‑файлы во временный каталог контейнера перед применением (или смонтируйте read‑only). Если объёмы велики — фильтруйте по дате и используйте --start-datetime и --stop-datetime, чтобы сократить объём. Важно: валидация PITR должна работать без доступа в интернет.

PostgreSQL: восстановление из дампа и проверка

Для pg_dump восстанавливаем custom или directory формат через pg_restore с параллельностью и проверяем результат:

# Ожидание готовности
until docker compose exec -T postgres pg_isready -U app -d app; do sleep 2; done

# Восстановление из custom-дампа
docker compose exec -T postgres sh -lc "pg_restore -U app -d app --clean --if-exists -j 4" < artifacts/postgres/app.dump

# Быстрая верификация
psql_q="
SELECT COUNT(*) AS users FROM public.users;
SELECT COUNT(*) AS orders_last30 FROM public.orders WHERE created_at > now() - interval '30 days';
SELECT extname FROM pg_extension ORDER BY 1;
"
printf "%s" "$psql_q" | docker compose exec -T postgres psql -U app -d app -v ON_ERROR_STOP=1

Для больших дампов задайте -j побольше и убедитесь, что контейнеру хватает CPU/IO. Верификацию делайте короткой (секунды), чтобы запуск был удобен ежедневно.

PostgreSQL PITR: base backup + WAL до цели

Для полноценного PITR в CI удобно иметь локальные артефакты: base backup каталога PGDATA и набор WAL‑сегментов за период. Восстановление делаем в отдельном контейнере или в том же, но с остановкой сервера и подменой PGDATA. Ниже — упрощённый сценарий на один контейнер для демонстрации:

# Остановить Postgres и очистить PGDATA
docker compose exec -T postgres pg_ctl -D "$PGDATA" -m immediate stop || true
docker compose exec -T postgres sh -lc "rm -rf $PGDATA/*"

# Развернуть base backup (например, tar с zstd)
cat artifacts/postgres/basebackup.tar.zst | docker compose exec -T postgres sh -lc "cd $PGDATA && zstd -d | tar -xf -"

# Подготовить recovery: задать restore_command и целевое время
RESTORE_TO="2025-01-10 10:15:00+00"
restore_cmd="cp /wal_archive/%f %p"

docker compose exec -T postgres sh -lc "echo \"restore_command = '$restore_cmd'\" >> $PGDATA/postgresql.conf"
docker compose exec -T postgres sh -lc "echo \"recovery_target_time = '$RESTORE_TO'\" >> $PGDATA/postgresql.conf"
docker compose exec -T postgres sh -lc "touch $PGDATA/recovery.signal"

# Разместить WAL-сегменты в /wal_archive (смонтируйте заранее артефакты)
# Запустить сервер и дождаться завершения recovery
docker compose exec -T postgres pg_ctl -D "$PGDATA" -w start

# Проверка контрольной выборки данных
printf "%s" "SELECT now(), pg_is_in_recovery();" | docker compose exec -T postgres psql -U app -d app -v ON_ERROR_STOP=1

На практике вместо «cp из локальной папки» в restore_command может использоваться ваш инструмент доставки WAL (локальный каталог артефактов CI, а не удалённое хранилище). Важно, чтобы всё работало офлайн, внутри sandbox. После старта сервер автоматически остановит recovery на целевом времени и перейдёт в обычный режим. Проверяйте, что pg_is_in_recovery() вернул false, и выполняйте свои smoke‑запросы.

Временная шкала PITR: base backup и журналы WAL/binlog до целевой точки

Верификация: что именно проверять

Помимо «база стартанула», нужно быстро подтвердить консистентность. Минимальный набор:

  • контрольные агрегаты: число строк по ключевым таблицам, суммы по инвариантам;
  • схема: наличие обязательных таблиц/индексов/расширений, версия миграций;
  • бизнес‑индикаторы: несколько точечных кейсов (заказы, балансы, статусы);
  • метка времени последней транзакции (сходится ли с ожиданиями относительно PITR).

Примеры запросов:

# MySQL: список таблиц без данных (подозрительно при restore)
printf "%s" "SELECT table_name FROM information_schema.tables WHERE table_schema='app' AND table_rows=0;" | docker compose exec -T mysql mysql -u app -papp_pass app

# PostgreSQL: поиск таблиц без индекса по первичному ключу
printf "%s" "SELECT relname FROM pg_class c JOIN pg_index i ON i.indrelid=c.oid WHERE i.indisprimary IS TRUE;" | docker compose exec -T postgres psql -U app -d app -v ON_ERROR_STOP=1

Всё это должно выполняться быстро (секунды), иначе CI станет узким местом. Для подробной проверки заведите недельные/ночные задания.

Пример пайплайна CI

Ниже пример job в стиле GitHub Actions (аналогично можно сделать в GitLab CI, Jenkins и т. п.). Он скачивает артефакты, поднимает compose, восстанавливает MySQL и PostgreSQL и запускает верификацию. Обратите внимание на таймауты, очистку и маскирование секретов.

name: backup-restore-verification
on:
  schedule:
    - cron: "0 3 * * *"
  workflow_dispatch:

jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Download artifacts
        run: |
          mkdir -p artifacts/mysql artifacts/postgres
          # Здесь разместите загрузку ваших артефактов бэкапов в ./artifacts

      - name: Start sandbox
        run: |
          docker compose up -d
          docker compose ps

      - name: Wait for DBs
        run: |
          until docker compose exec -T mysql mysqladmin ping -h 127.0.0.1 -u app -papp_pass --silent; do sleep 2; done
          until docker compose exec -T postgres pg_isready -U app -d app; do sleep 2; done

      - name: MySQL restore
        run: |
          gzip -cd artifacts/mysql/app.sql.gz | docker compose exec -T mysql mysql -u app -papp_pass app

      - name: PostgreSQL restore
        run: |
          docker compose exec -T postgres sh -lc "pg_restore -U app -d app --clean --if-exists -j 4" < artifacts/postgres/app.dump

      - name: Smoke checks
        run: |
          printf "%s" "SELECT COUNT(*) FROM users;" | docker compose exec -T mysql mysql -u app -papp_pass app
          printf "%s" "SELECT COUNT(*) FROM public.users;" | docker compose exec -T postgres psql -U app -d app -v ON_ERROR_STOP=1

      - name: Cleanup
        if: always()
        run: |
          docker compose down -v

Для PITR можно сделать отдельную job с параметрами времени восстановления. Храните метку RESTORE_TO в переменных пайплайна или вычисляйте (например, «минус 15 минут от последнего WAL/binlog»), чтобы сценарий оставался детерминированным.

Производительность и стабильность

Несколько практических советов, чтобы CI не «плавал» по времени и не падал от нехватки ресурсов:

  • используйте сжатие zstd и потоковую распаковку — меньше IO на диск;
  • параллельные restore‑инструменты (pg_restore -j, myloader) ускоряют в 2–5 раз;
  • ограничьте размер буферов и памяти контейнеров; ставьте с запасом, но без риска OOM агента;
  • для больших стендов выносите CI‑агента на отдельный рабочий узел с быстрым SSD; удобнее всего держать его на VDS с предсказуемыми ресурсами;
  • разделяйте быстрые ежедневные проверки и глубокие недельные прогоны с полной валидацией;
  • фиксируйте версии образов баз, чтобы исключить дрейф окружения между прогоном и аварией.

Частые ошибки и как их избежать

  • «Бэкап есть, а recovery не воспроизводится»: отсутствуют binlog/WAL или метаданные. Проверьте полноту набора: base + журнал + каталог таймлайнов (PostgreSQL).
  • «В CI всё ок, в бою — нет»: образы/версии отличаются. Зафиксируйте теги и параметры конфигурации, добавьте проверку совпадений.
  • «Песочница утекает наружу»: случайно опубликованные порты, общий network с runner. Проверяйте, что compose‑сеть внутренняя и нет published ports.
  • «Долго восстанавливается — не запускаем ежедневно»: разделите пайплайн и тестируйте маленькими «срезами» данных либо используйте инкрементальные бэкапы.
  • «Секреты попадают в логи»: используйте переменные‑маски CI, избегайте вывода паролей в командной строке, где это возможно.

Набор минимальной «боевой готовности»

Поставьте себе цель на ближайшую неделю:

  1. Соберите ежедневный логический дамп MySQL и PostgreSQL с сжатием.
  2. Настройте CI job с Docker sandbox, restore и двумя‑тремя проверками.
  3. Сделайте отдельную job для PITR на «минус 15 минут» от отметки бэкапа.
  4. Сохраняйте логи и итоговый отчёт в артефактах задачи.
  5. Добавьте алерт, если restore занял больше N минут или проверки упали.

Итоги

Проверка бэкапов в CI — простой способ превратить «надежду» в предсказуемую процедуру. Изолированный sandbox на Docker, короткие smoke‑проверки и периодическая PITR‑валидация дают уверенность, что в критический момент вы нажмёте «по инструкции» и восстановите сервисы за предсказуемое время. Начните с логического backup restore, затем добавьте binlog/WAL и доведите сценарий до полного off‑line восстановления. Регулярность и автоматизация — ваши лучшие друзья в борьбе с сюрпризами.

Поделиться статьей

Вам будет интересно

PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2 OpenAI Статья написана AI (GPT 5)

PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2

Разбираем, как на одном VDS безопасно собрать PHP-бэкенд, Node.js-воркеры и WebSocket-сервисы, очереди на RabbitMQ или Redis, наст ...
Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик OpenAI Статья написана AI (GPT 5)

Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик

Разбираем практическую схему: свой docker registry proxy cache на отдельном VDS, который прозрачно проксирует Docker Hub и другие ...
Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader OpenAI Статья написана AI (GPT 5)

Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader

В реальных проектах Composer крутится в каждом CI job и на каждом деплое, а vendor раздувается до сотен мегабайт. На PHP VDS с SSD ...