Данило (Dayfing)
Назад до публікацій
2 541 слів14 хв

Бекапи 3-2-1 з restic: налаштування, зовнішня копія, навчання з відновлення

Бекап, з якого можна відновитися, запускається за розкладом без вашої участі, зберігає одну копію поза майданчиком, де зламаний сервер не зможе її видалити, знімає бази даних в узгодженому стані й нещодавно пройшов перевірку відновленням із заміром часу. У restic це означає зашифрований репозиторій у S3-сумісному сховищі під захистом object lock або append-only сервера, дампи баз перед кожним запуском, таймер systemd зі сповіщеннями про збої та регулярні навчання з відновлення. У цьому посібнику таку схему зібрано для Linux-сервера, а ті самі елементи підходять і для машини розробника.

Визначте RPO і RTO до вибору інструментів

Recovery point objective (RPO) показує, скільки даних ви готові втратити, в одиницях часу. Recovery time objective (RTO) показує, скільки сервіс може простоювати під час відновлення. Власник сервісу відповідає на обидва запитання простими словами: «ми можемо втратити не більше одного дня замовлень» і «магазин має запрацювати протягом чотирьох годин».

Найгірша втрата даних під час бекапу за розкладом приблизно дорівнює інтервалу між запусками плюс часу, за який запуск доходить до зовнішнього сховища. Нічне завдання, яке стартує о 02:00 і завершує вивантаження о 02:40, дає найгірший RPO близько 24 годин 40 хвилин. Якщо для бази даних це забагато, робіть її бекап частіше або додайте архівування WAL.

RTO здебільшого визначається часом передавання й перебудови. Час передавання в секундах дорівнює обсягу даних у мегабітах, поділеному на швидкість каналу в Мбіт/с. Відновлення 200 ГБ становить 1 600 000 мегабіт, тож на 100 Мбіт/с воно триватиме 16 000 секунд, близько 4,4 години, ще до відновлення бази. Якщо це вже більше за RTO, один віддалений бакет мети не забезпечить, і потрібна локальна копія або резервна система.

Правило 3-2-1 і його розширення 3-2-1-1-0

Правило 3-2-1 означає три копії даних на двох різних типах сховищ, з яких одна копія розташована поза майданчиком. На VPS першою копією є робочий диск, другою слугує локальний дамп або репозиторій на окремому сховищі, а третя лежить у бакеті в іншого провайдера чи в іншому акаунті. Другий розділ на тому самому диску помирає разом із першим, тому тримайте локальну копію на іншому пристрої (обрати його допоможе посібник із вибору NVMe SSD) або на іншому хості.

Виробники систем резервного копіювання популяризували розширення 3-2-1-1-0. Додаткова одиниця означає копію, яка перебуває офлайн або є незмінною, щоб зловмисник із root на сервері та його хмарними обліковими даними однаково не зміг її видалити. Нуль означає відсутність помилок у перевірках цілісності й тестах відновлення. Більшість схем пропускає саме останню цифру, хоча лише вона показує, чи справжні решта чотири.

Що включати в бекап і що виключати

Зберігайте те, що не можна швидко відтворити з публічного джерела: /etc, код застосунку й завантаження в /srv або /var/www, домашні каталоги з ключами SSH і GPG, crontab, TLS-матеріали, дампи баз даних і список установлених пакетів з apt-mark showmanual. На машині розробника цінні незапушена робота, stash, dotfiles, ключі та документи.

Виключайте великі або відтворювані дані: кеші, node_modules, віртуальні середовища, результати збирання, образи контейнерів, ваги моделей і swap. Виключайте живі каталоги баз даних, наприклад /var/lib/postgresql, бо копія, знята з працюючого сервера, не є придатним бекапом. У файлах винятків restic шаблон, що починається з /, збігається з абсолютним шляхом, а просте ім’я на кшталт node_modules збігається на будь-якій глибині:

/var/lib/postgresql
/var/lib/docker
/var/cache
/var/tmp
/swapfile
/home/*/.cache
node_modules
.venv
__pycache__
*.tmp

--exclude-caches додатково пропускає каталоги зі стандартним файлом CACHEDIR.TAG, а --one-file-system не дає restic переходити в інші змонтовані файлові системи.

Основи restic: репозиторій, шифрування й пароль

restic зберігає дедупліковані снапшоти в репозиторії на локальному диску, через SFTP, на власному REST-сервері або в об’єктному сховищі та вивантажує лише нові фрагменти. Шифрування не можна вимкнути: за документацією дизайну restic усі дані шифруються AES-256 у режимі лічильника й автентифікуються Poly1305-AES, а ключ захищено паролем, посиленим через scrypt.

Тримайте налаштування у файлі середовища, доступному лише root, щоб інтерактивні команди й завдання за розкладом використовували однакові значення. У документації з репозиторіїв наведено формати URL, наприклад s3:https://server:port/bucket для S3-сумісних сервісів.

# /etc/restic/restic.env, mode 600
RESTIC_REPOSITORY=s3:https://s3.example.com/web1-backups
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=replace-me
AWS_SECRET_ACCESS_KEY=replace-me
HEARTBEAT_URL=https://monitor.example.net/ping/web1-backup
ALERT_WEBHOOK_URL=https://chat.example.net/hooks/backups
install -d -m 700 /etc/restic
(umask 077; openssl rand -base64 32 > /etc/restic/password)
set -a; . /etc/restic/restic.env; set +a
restic init
restic key add    # другий пароль для офлайн-копії на випадок відновлення
restic backup --one-file-system --exclude-caches \
  --exclude-file=/etc/restic/excludes.txt /etc /root /home /srv /var/www
restic snapshots

Документація каже прямо: втрата пароля означає безповоротну втрату даних. Зберігайте пароль, URL репозиторію й облікові дані бакета в менеджері паролів і в запечатаній офлайн-копії, але ніколи лише на сервері, який захищаєте. Якщо ви користуєтеся менеджером секретів, RESTIC_PASSWORD_COMMAND може виводити пароль замість читання файлу.

Зберігання через forget і prune, цілісність через check

restic forget застосовує політику зберігання до кожної групи снапшотів (типово групування йде за хостом і шляхами), а prune видаляє дані, на які більше не посилається жоден снапшот. Прапорець --prune виконує обидва кроки:

restic forget --prune --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
restic check --read-data-subset=10%

Така політика зберігає найновіший снапшот за кожен з останніх 14 днів, у які він був, далі по одному на тиждень за 8 тижнів, на місяць за 12 місяців і на рік за 3 роки. Зміни політики перевіряйте, додавши --dry-run. У документації з forget зазначено, що під час prune репозиторій заблоковано, тож рознесіть його й бекапи в часі та дайте команді бекапу --retry-lock.

restic check перевіряє структуру й метадані. --read-data завантажує кожен pack, що коштує часу й вихідного трафіку, а --read-data-subset читає частину: 10% вибирає випадкову підмножину під час кожного запуску, а 3/12 читає фіксовану дванадцяту частину, як описано в посібнику з обслуговування репозиторію. Успішна перевірка доводить узгодженість, але не те, що ви зможете відновити сервіс.

Зовнішній репозиторій, який не зітре шифрувальник

Зловмисник із root може прочитати /etc/restic/restic.env. Якщо ці облікові дані дозволяють видаляти об’єкти, зовнішню копію теж можна видалити. Обмежте облікові дані одним бакетом, тримайте адміністративні ключі поза сервером і застосовуйте принцип мінімальних привілеїв із посібника із захисту Linux VPS. Потім оберіть один із двох захистів.

REST-сервер у режимі append-only

rest-server від restic має режим --append-only, який приймає нові бекапи, але забороняє видаляти чи змінювати наявні. Документація restic радить запускати forget і prune для таких репозиторіїв з окремої, добре захищеної машини та використовувати --keep-within замість правил на кшталт --keep-weekly, бо зламаний клієнт може додати фальшиві снапшоти з підібраними мітками часу, які займуть місця справжніх.

Object lock у S3-сумісному сховищі

S3 Object Lock зберігає версії об’єктів за моделлю write-once-read-many. Він потребує версіонування, а в режимі compliance ніхто, зокрема root-користувач акаунта, не може видалити заблоковану версію до дати завершення зберігання. Починаючи з версії 0.13 restic надсилає контрольні суми завантаження, яких вимагають такі бакети, тому він працює з типовою політикою зберігання бакета (для інших провайдерів додайте --endpoint-url):

aws s3api create-bucket --bucket web1-backups --object-lock-enabled-for-bucket
aws s3api put-object-lock-configuration --bucket web1-backups \
  --object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'

Коли restic видаляє файл у такому бакеті, S3 додає маркер видалення, а заблокована версія залишається. Після атаки скопіюйте бакет у тому стані, в якому він був, наприклад командою rclone copy --s3-version-at "2026-09-20" offsite:web1-backups /srv/recovered-repo, і відновлюйтеся з цієї копії. Дані після prune і далі займають місце до кінця строку зберігання, тому видаляйте неактуальні версії правилом життєвого циклу не раніше цього строку. Перевірте весь цикл у свого провайдера, бо реалізації S3-сумісних сервісів відрізняються.

Узгоджені бекапи PostgreSQL

Документація PostgreSQL про бекапи на рівні файлової системи каже прямо: для придатної копії каталогу даних сервер має бути зупинений, і заборони підключень недостатньо, бо утиліти на кшталт tar не роблять атомарного знімка, а сервер буферизує дані в пам’яті. Атомарний знімок тому з усіма файлами даних і WAL працює, оскільки PostgreSQL відтворює WAL, як після збою.

Для більшості серверів логічні дампи лишаються найпростішим коректним варіантом. pg_dump робить узгоджене вивантаження, поки база використовується, і не блокує інших користувачів. Формат directory дає змогу паралельно створювати дампи й відновлювати їх. Ролі до дампу не входять, тому глобальні об’єкти вивантажуються окремо. Цей скрипт запускається перед кожним бекапом і зберігає останні дампи на локальному диску:

#!/usr/bin/env bash
# /usr/local/sbin/pg-dump-for-backup
set -euo pipefail
out=/var/backups/postgresql
install -d -m 700 -o postgres -g postgres "$out"
runuser -u postgres -- pg_dumpall --globals-only --file="$out/globals.sql"
runuser -u postgres -- psql -Atc \
  "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate" |
while read -r db; do
  rm -rf "$out/$db.new"
  runuser -u postgres -- pg_dump --format=directory --jobs=2 --file="$out/$db.new" "$db"
  rm -rf "$out/$db"
  mv "$out/$db.new" "$out/$db"
done

Використовуйте pg_basebackup, коли логічне відновлення великого кластера не вкладається в RTO або потрібне point-in-time recovery. Він копіює весь кластер у порожній каталог протоколом реплікації, потребує ролі з атрибутом REPLICATION і запису в pg_hba.conf, а також передає WAL, потрібний для запуску копії. pg_verifybackup перевіряє результат за маніфестом:

sudo -u postgres pg_basebackup --pgdata=/var/backups/pg-base --wal-method=stream --checkpoint=fast
sudo -u postgres pg_verifybackup /var/backups/pg-base

pg_restore перебудовує індекси за їхніми визначеннями. Якщо в базі є великі векторні індекси, наприклад HNSW із посібника з гібридного пошуку на pgvector, ця перебудова може забрати більшу частину часу відновлення.

Розклад через сервіс і таймер systemd

Сервіс типу oneshot запускає спершу дамп, потім бекап. Якщо дамп не вдався, бекап не стартує, і юніт переходить у стан failed. Збережіть юніти в /etc/systemd/system/:

# restic-backup.service
[Unit]
Description=restic backup to the off-site repository
Wants=network-online.target
After=network-online.target postgresql.service
OnFailure=backup-alert@%n.service

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
CacheDirectory=restic
Nice=10
IOSchedulingClass=idle
ExecStartPre=/usr/local/sbin/pg-dump-for-backup
ExecStart=/usr/local/bin/restic backup --retry-lock 30m \
    --one-file-system --exclude-caches \
    --exclude-file=/etc/restic/excludes.txt --tag scheduled \
    /etc /root /home /srv /var/www /var/backups/postgresql
ExecStartPost=-/usr/bin/curl -fsS --max-time 10 --retry 3 ${HEARTBEAT_URL}
# restic-backup.timer
[Unit]
Description=Nightly restic backup

[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

Увімкніть таймер командою systemctl enable --now restic-backup.timer і перевірте його через systemctl list-timers. Згідно з systemd.timer, Persistent=true надолужує запуск, пропущений, поки машина була вимкнена, а RandomizedDelaySec= розносить у часі хости, що пишуть в один бакет. Сервіси oneshot типово не мають тайм-ауту запуску. На Linux-ноутбуці та сама пара працює як користувацькі юніти для домашнього каталогу.

Щотижневий restic-maintenance.service з тим самим заголовком виконує очищення й перевірки:

ExecStart=/usr/local/bin/restic forget --prune --retry-lock 2h \
    --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
ExecStart=/usr/local/bin/restic check --read-data-subset=10%%

Пишіть %% для буквального знака відсотка, бо в юнітах % починає специфікатор.

Моніторинг і сповіщення про збої

Використовуйте два незалежні сигнали, інакше тихий збій виявиться під час інциденту. Перший сигнал дає OnFailure=. Коди виходу restic конкретні: 1 означає помилку команди, 3 означає, що частину вихідних файлів не вдалося прочитати, 10 повідомляє про відсутність репозиторію, 11 про невдале блокування, а 12 про неправильний пароль. systemd вважає збоєм будь-який ненульовий код, і для 3 це правильно: снапшот є, але в ньому прогалини.

# [email protected]
[Unit]
Description=Alert for failed %i

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
ExecStart=/usr/bin/curl -fsS --max-time 10 --retry 3 \
    --data-urlencode "text=%H: %i failed, see journalctl -u %i" \
    ${ALERT_WEBHOOK_URL}

Один шаблон обслуговує всі завдання, а systemd передає йому змінні на кшталт MONITOR_EXIT_STATUS. Перевірте ланцюжок командою systemctl start [email protected].

Другий сигнал працює як dead man's switch: ExecStartPost= надсилає запит на heartbeat URL лише після успіху, а система моніторингу здіймає тривогу, якщо запит не надійшов, скажімо, за 26 годин. Так виявляються вимкнений таймер або вимкнений хост. Для погляду ззовні щодня запускайте restic snapshots --latest 1 --json --no-lock з іншої машини й сповіщайте, якщо найновіший снапшот старший за RPO.

Проведіть навчання з відновлення із заміром часу

Навчання перетворюють припущення на цифри. Проводьте їх після початкового налаштування, після великих змін і не рідше ніж раз на квартал на чистій віртуальній машині, а не на робочому хості, використовуючи лише те, що матиме інженер у реальному інциденті: запис у менеджері паролів і runbook.

  1. Запустіть секундомір у момент початку умовного інциденту.
  2. Отримайте облікові дані, установіть restic і виведіть список снапшотів. Вік найновішого снапшота і є вашим фактичним RPO.
  3. Відновіть файли й дампи, потім базу даних, замірюючи кожен крок.
  4. Запустіть staging-копію застосунку й пройдіть один реальний сценарій.
  5. Запишіть тривалість етапів і проблеми, потім оновіть runbook.
set -a; . ./restic.env; set +a
restic snapshots --latest 3
time restic restore latest --target /restore --verify
sudo -u postgres psql -f /restore/var/backups/postgresql/globals.sql
sudo -u postgres createdb app
time sudo -u postgres pg_restore --jobs=4 --dbname=app /restore/var/backups/postgresql/app
sudo -u postgres psql -d app -c "SELECT max(created_at) FROM orders;"

--verify повторно читає відновлені файли й порівнює їх зі снапшотом. Останній запит показує, скільки даних відновлення фактично втратило. Помилки про те, що ролі вже існують, очікувані. Відпрацюйте також відновлення одного файлу з --include /etc/nginx/nginx.conf, найчастіший реальний запит.

Порівняйте загальний час з RTO. Типові несподіванки: відсутні облікові дані, пароль, який існував лише на загиблому сервері, повільна перебудова індексів і забуті файли, як-от TLS-ключі або .env поза шляхами, що зберігаються.

restic, BorgBackup чи знімки файлової системи

Багато схем поєднують ці підходи: знімок ZFS, Btrfs або LVM для швидкого локального відкату плюс файлові бекапи поза майданчиком. BorgBackup 1.4 є поточною стабільною гілкою.

Властивість restic BorgBackup 1.4 Знімки файлової системи
Де зберігати Локально, SFTP, REST-сервер, S3-сумісні та інші хмарні сховища Локально або через SSH, краще з Borg на сервері Той самий пул; для зовнішньої копії потрібна реплікація, наприклад zfs send
Шифрування Завжди ввімкнене Обирається під час створення репозиторію, необов’язкове Залежить від сховища
Зламаний клієнт rest-server --append-only, object lock бакета borg serve --append-only Захищені лише за реплікації в окремо адміністровану систему
Узгодженість БД Потрібні дампи Потрібні дампи Crash-consistent, якщо дані й WAL в одному атомарному знімку
Відновлення Від одного файлу до всього дерева, монтування через FUSE Від одного файлу до всього дерева, монтування через FUSE Відкат усього тому або копіювання зі змонтованого знімка
Клієнти Linux, macOS, Windows, BSD Linux, macOS, BSD; Windows лише через експериментальні WSL або Cygwin Залежить від файлової системи

Знімок у тому самому пулі захищає від невдалого деплою, але не від відмови пулу чи зламу хоста, тож сам по собі він не є копією в сенсі 3-2-1.

Чек-лист

  • RPO і RTO записані й погоджені з власником сервісу.
  • Є три копії, одна поза майданчиком і одна незмінна або append-only.
  • Пароль, URL репозиторію й облікові дані лежать у менеджері паролів і офлайн.
  • Живі каталоги баз виключено, а дампи включено.
  • PostgreSQL покрито pg_dump із глобальними об’єктами або перевіреним pg_basebackup.
  • Таймер використовує Persistent=true, а обробник OnFailure= хоча б раз спрацював на тесті.
  • Heartbeat здіймає тривогу за відсутності успішного запуску, а зовнішня перевірка за застарілих снапшотів.
  • forget --prune і check --read-data-subset виконуються щотижня.
  • Облікові дані сервера не можуть видалити заблоковані версії.
  • Навчання з відновлення із заміром часу відбулися в цьому кварталі, і runbook відображає їхні висновки.

Інші публікації