Резервная копия, которая лежит на том же диске, что и рабочие файлы, спасает разве что от ошибочной команды rm. Отказ сервера или повреждённая файловая система требуют копии в другом месте, лучше у другого оператора. Объектное хранилище с S3-совместимым интерфейсом подходит почти идеально: место дешёвое, доступ по обычным ключам. Но хранилище чужое, и открытые файлы туда отправлять не хочется. Утилита restic снимает проблему шифрованием на стороне клиента: наружу уходят только зашифрованные блоки, оператор не видит ни содержимого файлов, ни их имён. Ниже разобран полный цикл: переменные окружения, создание репозитория, инкрементальные копии, политика удержания, освобождение места, проверка целостности и восстановление.

Устройство репозитория restic и инкрементальные копии без цепочек полных и разностных

Restic это одиночный бинарный файл на Go без зависимостей и без фонового сервиса: утилиту запускает планировщик, она делает работу и завершается. Основная сущность репозиторий, его создаёт команда init внутри корзины. Репозиторий хранит зашифрованные блоки данных, ключи шифрования и индекс, по которому блоки находятся.

Механика инкрементальности встроена в формат. Файл режется на блоки переменного размера, границы вычисляются по содержимому через скользящий хэш, поэтому дописывание в конец не трогает уже сохранённые блоки, а вставка в середину пересоздаёт только часть из них. Идентификатор блока это SHA-256 содержимого, совпавший блок повторно не загружается. Каждый запуск backup создаёт снапшот: полное дерево каталогов со ссылками на блоки. Для пользователя копия выглядит полной, а в хранилище прибавляются только изменившиеся части. Цепочек из полных и разностных звеньев нет, любой снапшот восстанавливается самостоятельно.

Шифрование тоже часть формата. Пароль растягивается функцией scrypt, из результата выводятся байты для алгоритма AES-256 и кода аутентификации Poly1305-AES. Каждый объект шифруется и подписывается отдельно, подделка или порча обнаруживается при чтении. Оператор хранилища видит только случайные имена объектов и шифротекст. Дедупликация работает и между серверами: одинаковые системные файлы в общем репозитории хранятся один раз.

Подготовка корзины и ключей доступа в S3-совместимом хранилище

Для репозитория нужна отдельная корзина и отдельная пара ключей с правами только на неё. Утёкший ключ тогда открывает одну корзину, а не все проекты. Настройка сводится к переменным окружения: AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY задают ключи, для временных добавляется AWS_SESSION_TOKEN, адрес репозитория хранится в RESTIC_REPOSITORY в формате s3:точка-доступа/имя-корзины. Некоторым провайдерам нужен регион через AWS_DEFAULT_REGION или ключ -o s3.region, режим адресации корзины меняется через -o s3.bucket-lookup.

export AWS_ACCESS_KEY_ID="ключ-доступа"
export AWS_SECRET_ACCESS_KEY="секретный-ключ"
export RESTIC_REPOSITORY="s3:storage-endpoint/backup-bucket"
export RESTIC_PASSWORD_FILE="/etc/restic/password"

В примере storage-endpoint это точка доступа, выданная провайдером, а backup-bucket имя корзины. Файл сохраняют как /etc/restic/env.sh с правами 600. Пароль репозитория держат отдельно в /etc/restic/password с правами 600 и указывают на него переменной RESTIC_PASSWORD_FILE: переменные окружения видны в списке процессов, а файл под root подсмотреть сложнее.

Создание репозитория командой init и правила обращения с паролем шифрования

Создание репозитория выглядит так:

restic init

Утилита запросит пароль дважды, создаст структуру и выведет идентификатор нового репозитория. Иногда команда создаст и саму корзину, но надёжнее подготовить её заранее.

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

Смена пароля не требует пересоздания репозитория: key add добавляет ключ с новым паролем, key list показывает ключи с короткими идентификаторами, key remove удаляет старый. Сначала добавляют новый ключ, проверяют его, затем убирают старый. В скриптах пароль передают и через RESTIC_PASSWORD_COMMAND, которая вызывает программу, печатающую секрет в стандартный поток вывода. Жизнеспособность репозитория проверяют командой snapshots, список копий пока пуст, и командой check.

Первая резервная копия, полезные ключи команды backup и запуск по расписанию

Первая копия читает и загружает всё, последующие копируют только изменения. Файл считается нетронутым при совпадении времени модификации, времени изменения метаданных, размера и номера inode, за точку отсчёта берётся предыдущий снапшот того же хоста и путей. На файловых системах поверх FUSE номера inode не стабильны, файлы перечитываются каждый раз, ключ --ignore-inode снимает проблему, --ignore-ctime разрешает расхождение времени метаданных.

Чаще всего нужны такие ключи: --one-file-system не даёт выйти за пределы файловой системы, при копировании корня не будут обработаны /proc и внешние диски; --exclude-caches пропускает каталоги с маркерным файлом CACHEDIR.TAG; --exclude-file принимает маски из файла; --files-from читает список каталогов; --dry-run показывает план без записи; --host отделяет копии одного сервера от другого; --tag добавляет метку. После завершения утилита печатает сводку: новые, изменённые и нетронутые файлы, добавленные блоки, идентификатор снапшота.

Пример файла исключений /etc/restic/excludes:

/home/*/.cache
/var/cache
/var/tmp
*.log
*.tmp

Скрипт /usr/local/bin/restic-backup.sh собирает всё вместе:

#!/bin/sh
set -eu
. /etc/restic/env.sh

restic backup /etc /var/www /home \
  --exclude-file /etc/restic/excludes \
  --exclude-caches \
  --one-file-system \
  --host web1 \
  --tag daily

Строка в /etc/crontab запускает его каждую ночь:

30 3 * * * root /usr/local/bin/restic-backup.sh >> /var/log/restic-backup.log 2>&1

На машине с systemd cron заменяет пара из юнита службы и таймера. Код завершения при сбое ненулевой, скрипт с set -eu передаёт его дальше, мониторингу достаточно кода и лога.

Политика удержания снапшотов командой forget и ключи keep по дням и месяцам

Без политики копии множатся: за год ежедневных запусков наберётся 365 снапшотов, и место уйдёт под ненужную историю. Чистить вручную невозможно, команда forget принимает политику, а не список. Смысл ключей единый: для каждого периода, в котором есть хотя бы один снапшот, хранится самый свежий снапшот этого периода. Для файлового сервера с ежедневными копиями рабочая схема выглядит так:

  1. последняя копия за каждый из последних семи дней хранится через --keep-daily 7;
  2. последняя копия за каждую из последних пяти недель хранится через --keep-weekly 5;
  3. последняя копия за каждый из последних двенадцати месяцев хранится через --keep-monthly 12;
  4. последние три годовые копии хранятся через --keep-yearly 3.

Команда с такой политикой:

restic forget \
  --host web1 \
  --keep-daily 7 \
  --keep-weekly 5 \
  --keep-monthly 12 \
  --keep-yearly 3 \
  --dry-run

Ключ --dry-run обязателен при первых прогонах: утилита печатает, какие снапшоты будут удалены, и ничего не меняет. Дальше forget работает по расписанию или сразу следом за backup. Есть и другие средства: --keep-last хранит последние N копий, --keep-within 30d удержит все копии моложе тридцати дней, --keep-tag держит помеченные копии в стороне от календарной чистки. Перед крупным обновлением свежий снапшот помечают командой restic tag --add before-upgrade, политика его не тронет.

Политика применяется к группам снапшотов, по умолчанию группы формируются по хосту и набору путей. Это защита: схема одного сервера не заденет копии другого в общем репозитории. Менять группировку ключом --group-by стоит осознанно. И главное: forget не освобождает место, удалённые записи оставляют блоки в хранилище, за очисткой нужна отдельная команда.

Освобождение места командой prune и проверка целостности командой check

Место возвращает prune: команда собирает список блоков, на которые ссылаются оставшиеся снапшоты, находит неиспользуемые, перепаковывает пакетные файлы со смесью нужных и ненужных блоков, удаляет лишнее.

restic prune --dry-run
restic prune --max-unused 5%

Ключ --max-unused задаёт допустимый объём неиспользуемых данных после очистки, в байтах или процентах: утилита не стремится к идеальной плотности, ведь перепаковка стоит трафика и времени, а --max-repack-size ограничивает объём перепаковки за один прогон. Prune ставит блокировку, копии в это время не запустятся, поэтому очистку назначают вне окна бэкапов, например раз в неделю. У команды forget есть ключ --prune, он запускает очистку сразу за удалением снапшотов.

После очистки запускают check. Без ключей проверяется структура: индексы, снапшоты, деревья каталогов, пакетные файлы, содержимое блоков не читается. Ключ --read-data скачивает и проверяет всё целиком, это дорого по трафику и времени, компромисс в --read-data-subset: значение 10% проверяет случайную десятую часть пакетов за прогон, полный обход растягивается частями 1/5, 2/5 и дальше. Ритм такой: структура после каждого prune, выборочная проверка еженедельно, полный прогон раз в квартал.

restic check
restic check --read-data-subset=10%

При проблемах с индексом помогает restic repair index, он пересобирает индекс из содержимого хранилища. После принудительного завершения процесса в репозитории может остаться блокировка, её снимает restic unlock, но запускать его стоит только когда других процессов утилиты точно нет. Репозиторий в S3 это просто набор объектов без скрытой серверной логики: корзину можно скопировать целиком к другому провайдеру, и копия останется рабочим репозиторием с тем же паролем.

Восстановление файлов через restore и монтирование снапшота в локальный каталог

Начинают со списка: restic snapshots --host web1 показывает копии с идентификаторами, временем и путями. Полная выгрузка последней копии на подготовленный сервер:

restic restore latest --host web1 --target /mnt/restore

Слово latest берёт самый свежий снапшот, фильтры --host и --path уточняют набор. Ключи --include и --exclude ограничивают выгрузку частью данных, при запуске от root восстанавливаются владельцы и права. Один файл можно не выгружать, а напечатать прямо в стандартный поток вывода:

restic dump latest /etc/nginx/nginx.conf > /tmp/nginx.conf

Перед dump полезен restic ls latest --host web1, он показывает состав снапшота и точные пути внутри. Для просмотра копий удобно монтирование: restic mount /mnt/restic поднимает через FUSE файловую систему, где снапшоты разложены по идентификаторам, хостам, тегам и времени плюс отдельный вход latest. Так вытащить несколько файлов или посмотреть содержимое на конкретную дату, для больших объёмов restore быстрее. Сеанс завершают umount или прерыванием по Ctrl-c, нужен модуль ядра FUSE и утилита fusermount. Разобраться, что изменилось между двумя копиями, помогает restic diff с парой идентификаторов, она выводит добавленные, изменённые и удалённые файлы.

Сценарий восстановления на новом сервере: установить утилиту, задать окружение с ключами и паролем, проверить доступ командой snapshots, выгрузить копию в пустой каталог и перенести файлы на место. Именно в пустой: restore не удаляет файлы, которых не было в снапшоте, старое содержимое останется рядом. Точное воспроизведение каталога даёт ключ --delete, но перед ним обязателен пробный прогон с --dry-run -vv, иначе легко стереть лишнее.

Вся схема сводится к четырём командам и одному расписанию. backup пишет только новые блоки, forget держит историю под контролем, prune возвращает место, check подтверждает порядок. Пароль лежит в двух независимых местах, ключи ограничены одной корзиной. Остаётся привычка, отличающая рабочую схему от иллюзии безопасности: раз в квартал выгрузить копию на тестовую машину и проверить, что файлы открываются. Копия, которую ни разу не восстанавливали, остаётся предположением.