Кейс 4: Построение отказоустойчивой системы резервного копирования

Роль: DevOps Engineer
Длительность: ~34 часа (от разработки скриптов до комплексного тестирования и документации)
Стек: Restic, Ansible, Bash, Python, GitLab CI, S3, Systemd (user-level).

Проблема и Контекст (Baseline)

Резервное копирование критических данных (PostgreSQL, Redis) выполнялось нестабильно или вручную. Первая попытка (v1) — кастомный скрипт синхронизации с S3 с проактивной диагностикой — дала видимость, но упёрлась в системные пределы: собственный код синхронизации нужно поддерживать, а шифрования и дедупликации в нём не было. Ключевые боли:

  • Отсутствие шифрования и дедупликации при хранении в облаке.
  • Риск “тихой” потери данных: скрипты не возвращали корректные коды ошибок, уведомления не приходили.
  • Экспоненциальный рост снапшотов и отсутствие автоматической ротации.

Инженерные решения (Action)

1. Выбор инструмента: Restic вместо расширения v1

Расширение кастомного скрипта отвергнуто: шифрование, дедупликацию и ротацию пришлось бы писать и поддерживать самостоятельно. Выбран Restic — эти свойства встроены в инструмент (AES-256 на стороне клиента, дедупликация, retention-политики), а моя задача — корректно их сконфигурировать и обернуть в надежную автоматизацию.

2. Enterprise-grade скриптинг с честной обработкой ошибок

  • Разработан backup.sh с retry-логикой (3 попытки для pg_dump) и корректными exit codes: 1 при полном провале, 0 с предупреждением при частичном успехе.
  • Созданы интерактивные скрипты восстановления (restore-postgres.sh, restore-redis.sh) с обязательным подтверждением yes/no и поддержкой безопасного режима --dry-run.
  • Выделен надежный Python-скрипт (send-email.py) для отправки уведомлений с корректной обработкой UTF-8 и MIME-заголовков (вместо ненадежных curl-hacks).

3. Идемпотентная автоматизация через Ansible

  • Создана роль restic_backup в строгом корпоративном стиле: использование FQCN (ansible.builtin.*), разделение задач по файлам, no_log: true для чувствительных данных.

3.1. User-level systemd timer: обходной манёвр Ansible

Baseline: Бэкапы должны запускаться по расписанию без прав root — через user-level systemd timer пользователя deployer. Но стандартный модуль ansible.builtin.systemd с become_user + scope:user не работает в этой схеме.

Аудит и инсайт: Модуль systemd обращается к D-Bus через DBUS_SESSION_BUS_ADDRESS. При sudo-become эта переменная недоступна — systemd user bus слушает на /run/user/<UID>/bus, который монтируется только при PAM-логине. Единственный надёжный способ — shell с явной передачей переменных окружения.

Решение — активация таймера через shell (тот же обходной манёвр, что в кейсе 3 для rootless Podman):

- name: Enable and start restic-backup.timer via shell (user-level)
  ansible.builtin.shell: |
    systemctl --user daemon-reload
    systemctl --user enable .timer
    systemctl --user start .timer
  become: yes
  become_user: ""
  environment:
    XDG_RUNTIME_DIR: "/run/user/"
    DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user//bus"
  changed_when: true

Этот паттерн (shell + явная передача XDG_RUNTIME_DIR и DBUS_SESSION_BUS_ADDRESS) впервые был отработан при активации rootless Podman socket в кейсе 3 и здесь применён как уже проверенное решение — без повторного траблшутинга.

4. Умная ротация и предотвращение “раздувания” хранилища

  • Проблема S3: Restic считал каждую staging-директорию с уникальным timestamp новым путем, создавая дубликаты.
  • Решение: Внедрены флаги --tag daily_backup при создании и --group-by host,tags при ротации (restic forget).
  • Проблема локальной очистки: Команда find -mtime +1 означала “строго больше 48 часов”. Формула скорректирована на find_days = LOCAL_RETENTION_DAYS - 1 (использование -mtime +0 для корректной очистки старше 24 часов).

5. Бесшовная интеграция в CI/CD

  • Роль добавлена в существующий шаблон с input-параметром restic_backup_enabled.
  • Настроен строгий маппинг переменных: для бэкапов используются изолированные S3_BUCKET_BACKUPS_*, что гарантирует защиту от случайной перезаписи основного хранилища.

Результаты и Метрики

Метрика Результат
Надежность 100% автоматизация через user-level systemd timer. Честные exit codes и алертинг.
Безопасность Restic выбран за встроенные AES-256 на стороне клиента и дедупликацию; секреты скрыты через no_log.
Эффективность хранения Исключен экспоненциальный рост снапшотов благодаря группировке по тегам и хостам.
Восстанавливаемость Restore формализован интерактивными скриптами с --dry-run и Runbook с пошаговым восстановлением и Troubleshooting.

Архитектура процесса резервного копирования

graph TD
    subgraph Target [Target Server - User: deployer]
        DB[(PostgreSQL / Redis)] -->|pg_dump / BGSAVE| Staging[Staging Directory]
        Staging -->|restic backup --tag| Restic[Restic Client]
    end
    
    subgraph Automation [Automation and Orchestration]
        Systemd[Systemd User Timer] -->|Trigger daily| BackupScript[backup.sh]
        BackupScript -->|on fail| Email[send-email.py]
        Ansible[Ansible Role] -.->|Manages| Systemd
        Ansible -.->|Manages| Restic
    end
    
    subgraph Storage [Cloud Storage - S3]
        Restic -->|AES-256 Encrypted + Dedup| S3[(S3 Bucket)]
        S3 -->|restic forget --group-by| Cleanup[Smart Retention Policy]
    end
    
    subgraph CI [CI/CD Integration]
        GitLab[GitLab CI] -->|restic_backup_enabled=true| Ansible
        GitLab -->|Masked Variables| S3Creds[S3_BUCKET_BACKUPS_*]
    end

Content licensed under CC BY-SA 4.0 · Built with Jekyll · Source on GitHub