❯ Мои проблемы

В предыдущих статьях я уже рассказывал о своей домашней инфраструктуре ServeHub-2. Это проект, который позволяет развернуть собственное окружение из двух серверов — удалённого VPS и домашнего сервера, объединённых в единую защищённую сеть.

У каждого сервера своя роль. Удалённый VPS (Edge) отвечает за сетевую инфраструктуру, DNS-фильтрацию, мониторинг и оповещения. Домашний сервер (Workload, на котором сосредоточена основная нагрузка) используется для размещения приложений, баз данных и хранения личных файлов.

У меня работает множество сервисов, например Nextcloud, Vaultwarden и другие (подробнее о них можно почитать в самом проекте). Сервисы развёрнуты с помощью Ansible и Docker Compose. Как уже можно догадаться, всё это необходимо резервировать, иначе рассчитывать на сохранность своих данных не приходится.

Также необходимо учитывать, что серверов два. При потере домашнего сервера потребуется восстановить пользовательские приложения и личные данные. Если проблема возникнет с VPS, нужно будет вернуть сетевые компоненты, мониторинг и остальные служебные сервисы.

До последней версии ServeHub-2 резервные копии хранились только на домашнем сервере — на внешнем HDD. Это было лучше, чем ничего, но такой схемы всё же было недостаточно.

Поэтому мне потребовалась система с двумя уровнями хранения:

  • Локальная резервная копия на каждом сервере для восстановления данных.

  • Удалённая копия в S3-совместимом объектном хранилище на случай потери самого сервера или локального накопителя.

❯ Решение?

Для реализации я решил использовать несколько инструментов, каждый из которых отвечает за свою часть процесса.

Основой стал BorgBackup — инструмент для создания зашифрованных резервных копий с дедупликацией. Для настройки конфигурации репозиториев, политик хранения и выполнения операций используется Borgmatic.

За передачу данных в объектное хранилище отвечает rclone. Он синхронизирует локальные Borg-репозитории с S3 и позволяет проверить наличие загруженных объектов и совпадение их размеров.

Дополнительно используются systemd timers для автоматизации запусков и Prometheus для контроля состояния всей цепочки резервного копирования.

Вместо того чтобы собирать данные с обоих серверов в одном месте и уже оттуда отправлять их в облако, каждый узел самостоятельно создаёт собственный локальный Borg-репозиторий и синхронизирует его с отдельным префиксом в S3 — edge или workload.

На домашнем сервере локальный репозиторий дополнительно размещается на отдельном HDD. При развёртывании проекта Ansible монтирует этот накопитель по UUID, если он указан в конфигурации. Если отдельный диск не настроен, используется стандартный каталог на системном накопителе.

Таким образом, у каждого сервера есть локальная копия, а за пределами инфраструктуры хранится её удалённое зеркало. Это позволяет не зависеть исключительно от одного места хранения: при временной недоступности S3 остаётся локальный репозиторий, а при потере сервера или его локального накопителя можно использовать удалённую копию.

❯ Что именно я сохраняю

Перед настройкой резервного копирования пришлось определить, какие данные действительно необходимы для восстановления инфраструктуры.

В проекте постоянные данные сервисов на обоих серверах хранятся преимущественно в каталогах apps-data/. Там находятся настройки приложений, базы данных, состояние сервисов и другая информация, которая должна сохраняться между пересозданиями Docker-контейнеров. Конфигурационные файлы отдельно вынесены в configs/, а параметры окружения хранятся в .env.

Для упрощения резервного копирования и переносимости я использую bind mounts (каталоги хостовой системы), а не именованные Docker volumes. Благодаря этому постоянные данные контейнеров находятся в предсказуемых каталогах файловой системы и могут резервироваться как обычные директории.

Отдельно от каталога проекта находится PersonalData/. Там хранятся пользовательские файлы, музыка, аудиокниги и другие данные. Несмотря на то что эта директория не входит непосредственно в состав ServeHub-2, её содержимое тоже необходимо сохранять.

Таким образом, на обоих серверах резервируются:

  • apps-data/ — состояние приложений и инфраструктурных компонентов.

  • configs/ — конфигурации сервисов.

  • .env — переменные окружения и параметры Compose.

Дополнительно на Workload сохраняются:

  • certbot/ — сертификаты и данные Certbot.

  • PersonalData/ — пользовательские файлы, музыка, аудиокниги и другие личные данные.

Для этого я вынес конфигурацию Borgmatic в один Jinja2-шаблон ansible/templates/borgmatic_config.yaml.j2, который Ansible использует при развёртывании:

source_directories:
  - "/home/{{ deploy_user }}/ServeHub-2/apps-data/"
  - "/home/{{ deploy_user }}/ServeHub-2/configs/"
  - "/home/{{ deploy_user }}/ServeHub-2/.env"
{% if backup_role == 'workload' %}
  - "/home/{{ deploy_user }}/ServeHub-2/certbot/"
  - "/home/{{ deploy_user }}/PersonalData/"
{% endif %}

repositories:
  - path: {{ backup_repository_path | to_json }}
    label: {{ (backup_role ~ '-local') | to_json }}

keep_daily: 7
keep_weekly: 4
keep_monthly: 6
compression: lz4

encryption_passphrase: {{ borgmatic_encryption_passphrase | to_json }}

{% if backup_role == 'workload' %}
loki:
  url: http://10.8.0.1:3100/loki/api/v1/push
  labels:
    app: borgmatic
    host: __hostname
    config: __config
  send_logs: true
{% endif %}

Для сжатия используется LZ4. Я не ставил перед собой задачу максимально уменьшить размер архивов любой ценой: скорость создания и восстановления резервных копий тоже имеет значение.

Также используется шифрование репозиториев. При первоначальной инициализации Ansible запускает Borgmatic с параметром --encryption repokey-blake2. Парольная фраза задаётся отдельно от исходного кода проекта. Её необходимо сохранить независимо от серверов и объектного хранилища, иначе восстановить зашифрованные архивы не получится.

Оба сервера используют одну парольную фразу, но создают независимые Borg-репозитории со своими идентификаторами.

❯ Локальное хранение

Для локальных резервных копий я решил использовать разные расположения репозиториев.

На Edge репозиторий находится в: /var/lib/servehub-backup/edge-repo

На Workload есть возможность использовать отдельный накопитель: /mnt/backup_storage/server_backup.borg

Если UUID дополнительного диска не указан, используется обычный каталог: /var/lib/servehub-backup/workload-repo

При использовании отдельного диска недостаточно просто указать путь к нему. Если накопитель по какой-то причине не смонтируется, Linux всё равно позволит записывать данные в обычную директорию /mnt/backup_storage, расположенную на корневом разделе.

В результате резервная копия может успешно создаться, хотя фактически окажется не на предназначенном для неё накопителе.

Поэтому в Ansible предусмотрены проверка UUID диска и его автоматическое монтирование.

Фрагмент из ansible/tasks/setup_local.yml:

- name: "[Backup] Монтирование отдельного диска для локального репозитория"
  when:
    - backup_disk_uuid | default('') | string | trim | length > 0
    - disk_check.rc | default(1) == 0
  block:
    - name: "[Backup] Монтирование диска и запись в fstab"
      ansible.posix.mount:
        path: /mnt/backup_storage
        src: "UUID={{ backup_disk_uuid }}"
        fstype: auto
        opts: defaults,nofail
        state: mounted

    - name: "[Backup] Ограничение доступа к каталогу резервного диска"
      ansible.builtin.file:
        path: /mnt/backup_storage
        state: directory
        owner: root
        group: root
        mode: "0700"

Кроме того, при использовании отдельного диска systemd получает условие ConditionPathIsMountPoint. Это не позволяет запустить резервное копирование, если нужный раздел не смонтирован.

❯ Подключение S3

Для удалённого хранения я выбрал Timeweb Cloud S3 с холодным классом хранения.

Кстати, за обновлениях S3 и других сервисов удобно следить в Telegram-канале Timeweb Cloud. Подписывайтесь, чтобы ничего не пропустить.

Он рассчитан на редко используемые данные, включая резервные копии и архивы. В моём случае это подходит. При выборе такого класса необходимо учитывать, что исходящий трафик обходится дороже, чем в стандартном хранилище. Это особенно важно при восстановлении больших объёмов данных.

Проект не привязан к конкретному провайдеру. В конфигурации rclone используется универсальный S3-режим Other, который предназначен для совместимых с S3 хранилищ.

[offsite-s3]
type = s3 # Тип remote — S3-совместимое хранилище
provider = Other # Произвольный S3-провайдер (не AWS/MinIO и т.п.)                  
no_check_bucket = true
env_auth = false
access_key_id = {{ offsite_s3_access_key_id }}
secret_access_key = {{ offsite_s3_secret_access_key }}

{% if offsite_s3_region | default('', true) | string | trim %}
region = {{ offsite_s3_region | string | trim }}

{% endif %}
endpoint = {{ offsite_s3_endpoint | string | trim }}

Теоретически это позволяет использовать и других провайдеров, однако отдельно я тестировал только Timeweb Cloud. Поэтому гарантировать совместимость со всеми реализациями S3 не буду.

В самом проекте параметры подключения задаются через ansible/vars/secrets.yml. Для подстановки переменных у меня есть GUI установщик проекта, но это отдельная тема.

borgmatic_encryption_passphrase: ""
backup_disk_uuid: ""
offsite_s3_endpoint: ""
offsite_s3_region: ""
offsite_s3_bucket: ""
offsite_s3_access_key_id: ""
offsite_s3_secret_access_key: ""
offsite_s3_workload_prefix: "workload" # Бакет для обоих серверов один, но префиксы разные
offsite_s3_edge_prefix: "edge"

❯ Как проходит резервное копирование

Для настройки резервного копирования обоих серверов Ansible использует общую задачу setup_backup.yml, передавая ей разные параметры: путь к локальному Borg-репозиторию, Compose-файл, список сервисов, которые нужно оставить работающими, и время запуска.

Некоторые сервисы, особенно базы данных, продолжают изменять файлы во время работы. Поэтому простое копирование их каталогов не всегда гарантирует согласованность резервной копии.

Вместо создания отдельных механизмов для каждого приложения я решил временно останавливать большинство Compose-сервисов.

Скрипт scripts/borgmatic_with_metrics.sh определяет запущенные Compose-сервисы и исключает из списка остановки те, которые передал Ansible:

read -r -a KEEP_SERVICES <<< "$KEEP_SERVICES_RAW"

for service in "${RUNNING_SERVICES[@]}"; do
    keep=0 

    for keep_service in "${KEEP_SERVICES[@]}"; do
        if [[ "$service" == "$keep_service" ]]; then
            keep=1
            break
        fi
    done

    if (( keep == 0 )); then
        STOP_SERVICES+=("$service")
    fi
done

На Edge продолжают работать Node Exporter и cAdvisor. На Workload к ним добавляется клиент AmneziaWG, необходимый для связи между серверами.

После остановки выбранных сервисов запускается Borgmatic, а затем скрипт пытается вернуть ранее работавшие Compose-сервисы. Для этого предусмотрено до трёх попыток с проверкой готовности сервисов. При неудаче обработчик EXIT также может повторить процедуру восстановления.

У такого подхода есть недостаток: часть инфраструктуры временно становится недоступной. На Edge это затрагивает даже сетевые компоненты и мониторинг. Так как проект рассчитан на личное пользование, я решил что на такой компромисс можно пойти, ради сохранения простоты и стабильности работы.

❯ Синхронизация с S3

После выполнения Borgmatic скрипт пытается вернуть ранее работавшие Compose-сервисы. Если создание локальной резервной копии завершилось успешно, он переходит к синхронизации с S3 через rclone. Даже если вернуть сервисы не удалось, выгрузка всё ещё может состояться, но весь цикл резервного копирования будет отмечен как неуспешный.

if /usr/bin/rclone --config "$RCLONE_CONFIG" sync \
    --fast-list --transfers 4 --checkers 8 --stats 30s --log-level INFO \
    "$BORG_REPOSITORY" "$RCLONE_REMOTE_PATH"; then

    if /usr/bin/rclone --config "$RCLONE_CONFIG" check \
        --one-way --size-only \
        "$BORG_REPOSITORY" "$RCLONE_REMOTE_PATH"; then
        OFFSITE_STATUS=1
    else
        echo "[ERROR] rclone size verification of the off-site copy failed." >&2
    fi
else
    echo "[ERROR] rclone synchronization to the off-site S3 repository failed." >&2
fi

При синхронизации используется rclone sync. Он копирует изменения из локального репозитория в S3 и удаляет там файлы, которых уже нет в источнике. Например, когда Borg удаляет старые архивы, они удаляются и из S3.

Поэтому S3 хранит актуальную копию локального репозитория, а не отдельный набор резервных копий со своей историей.

Перед синхронизацией скрипт проверяет, что локальный и удалённый репозитории действительно принадлежат друг другу. Для этого он сравнивает их идентификаторы. Если они не совпадают или скрипт не может прочитать конфигурацию удалённого репозитория, синхронизация отменяется.

После загрузки rclone check --one-way --size-only проверяет, что все файлы присутствуют в S3 и имеют такой же размер, как локальные. Содержимое файлов эта проверка полностью не проверяет, поэтому она не заменяет тестовое восстановление.

❯ Автоматизация и мониторинг

Для ежедневного запуска используются systemd timers.

На каждом сервере создаётся собственный borgmatic.service типа oneshot и соответствующий таймер. Для Edge задано время запуска 03:00, для Workload — 04:00.

Дополнительно используется случайная задержка до 15 минут и параметр Persistent=true, позволяющий выполнить пропущенный запуск после восстановления работы системы.

Фрагмент таймера из ansible/tasks/setup_backup.yml:

[Timer]
OnCalendar=*-*-* {{ backup_schedule_time }}
RandomizedDelaySec=15m
Persistent=true

Но автоматический запуск — только половина задачи. Нужно ещё понимать, действительно ли процесс завершился успешно. Для этого я интегрировал резервное копирование в уже существующую систему мониторинга на Prometheus и Grafana.

Скрипт создаёт Prometheus-совместимый файл метрик, который считывается Node Exporter через Textfile Collector.

Я разделил результат на несколько независимых показателей:

Метрика

Назначение

servehub_borgmatic_backup_last_run_success

Результат создания локального архива

servehub_offsite_backup_last_run_success

Результат синхронизации и проверки S3

servehub_backup_stack_restore_success

Удалось ли вернуть Compose-сервисы

servehub_backup_pipeline_last_run_success

Результат всей цепочки

Если последний успешный запуск локального резервирования, выгрузки или полного цикла старше 30 часов, срабатывает соответствующий критический alert. Отдельно отслеживается невозможность восстановления ранее работавших сервисов.

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

❯ Восстановление данных из S3

Теперь рассмотрим обратную операцию — получение резервной копии из S3.

На машине также необходимо заранее подготовить конфигурацию rclone /etc/servehub/rclone.conf с параметрами подключения к S3. Она не входит в резервную копию, поэтому её нужно хранить отдельно.

Скачиваем нужный репозиторий. Например, для Workload:

sudo mkdir -p /srv/restore/workload-repo

sudo rclone --config /etc/servehub/rclone.conf copy \
  offsite-s3:<bucket>/workload \
  /srv/restore/workload-repo \
  --progress

Для Edge используется тот же подход, только вместо workload указывается edge.

После скачивания необходимо ввести парольную фразу Borg и можно будет посмотреть доступные архивы. Она используется для доступа к зашифрованному репозиторию:

sudo -i

read -rsp 'Borg passphrase: ' BORG_PASSPHRASE; export BORG_PASSPHRASE; echo

borg list /srv/restore/workload-repo

После выбора нужного архива создаём отдельный каталог и извлекаем его содержимое:

mkdir -p /srv/restore/extracted && cd /srv/restore/extracted

borg extract /srv/restore/workload-repo::ARCHIVE_NAME

После завершения работы парольную фразу следует удалить из окружения:

unset BORG_PASSPHRASE
exit

Таким образом, из S3 можно получить локальный Borg-репозиторий и извлечь из него нужные данные. Дальнейший перенос файлов на восстановленный сервер выполняется отдельно — с проверкой владельцев и прав доступа.

❯ Какие есть ограничения

  • Во время создания локального архива часть Compose-сервисов временно недоступна. На Edge это затрагивает в том числе сетевые компоненты и мониторинг.

  • S3-копия является зеркалом локального репозитория, а не независимым неизменяемым архивом.

  • Проверка rclone check --one-way --size-only не заменяет полноценную проверку содержимого.

  • Конфигурацию подключения к S3, Ansible-секреты и парольную фразу необходимо хранить отдельно.

  • Фактическое восстановление приложений следует периодически проверять на отдельном окружении.

  • Холодный класс хранения удобен для долгосрочного размещения резервных копий, но при восстановлении нужно учитывать исходящий трафик.

❯ Заключение

В итоге для ServeHub-2 получилось объединить несколько инструментов в одну систему резервного копирования, которая учитывает особенности двух разных серверов.

Borg отвечает за создание зашифрованных локальных архивов, Borgmatic — за управление ими, rclone — за синхронизацию с S3, а Prometheus позволяет контролировать состояние всей цепочки.

Исходный код проекта доступен в GitHub.

Если вам интересна тема сетевой архитектуры, администрирования Linux и DevOps, делюсь своими практическими заметками и опытом в Telegram-канале My Tech Notes.

Может быть интересно:
Перейти ↩

Перейти ↩