:title: Делаем скучные бэкапы :date: 8 Oct 22 ===================== Делаем скучные бэкапы ===================== Бэкапы, в самом деле, нескучные (: Мотивацией для написания очередного велосипеда стало то, что даже для, казалось бы, готовых инструментов приходится писать обвязки на шелле. В какой-то момент мне надоело писать баш-портянки для бэкапа и я написал одну большую, которая умеет сразу много всего. Знакомьтесь — **boring_backup**. > **Пакеты для Debian/Ubuntu, Arch и сорцы** `здесь `_. Сначала о том, чем он *НЕ* является: * Тупым линейным скриптом с вызовом tar и mysqldump. * Полным аналогом `restic `_ и похожих утилит. * POSIX-совместимым скриптом. Хотелось бы, конечно, но это достаточно больно. Может быть когда-нибудь я перепишу код и оно заработает на dash/ash и прочих шеллах, но это будет уже другой скрипт. **boring_backup** — это библиотека функций Bash и интерфейс командной строки для скриптов резервного копирования. Вот что в ней есть: * Обработка ошибок (wow!) * Логирование в файл или syslog (WOW!) * Парсер URI практически целиком совместимый с RFC 3986! * Готовые функции для запаковки файлов (tar) и снятия SQL-дампов (MySQL/MariaDB и PostgreSQL) Логика работы утилиты крайне проста. Есть два массива данных — список URI исходных ресурсов, которые надо бэкапить и список URI хранилищ, куда предстоит бэкапиться. Поддерживается определённый набор схем URI, каждую схему обрабатывает свой обработчик. Cамый простой скрипт резервного копирования, который можно написать с помощью **boring_backup** выглядит так: .. code-block:: shell sources=(/home/john) targets=(/var/backups) Сохраним это в файл под именем `test`. Здесь очевидно дира /home/john будет скопирована в /var/backups, однако cперва она будет запакована в архив tar без сжатия. Запускаем бэкап: .. code-block:: shell boring_backup test Итого получим файл /var/backups/test_john_2022.10.08-1230.tar. Если мы хотим сжать архив, то добавим компрессию: .. code-block:: shell sources=(/home/john) targets=(/var/backups) compression=xz Здесь значение переменной ``compression`` будет интерпретироваться так же как tar интерпретирует расширение архива с опцией ``--auto-compress``. То есть поддерживаются все утилиты, которыми tar умеет сжимать. Если переменная пуста или содержит что-то не то, то сжатие будет отключено. Надо исключить лишние файлы из архива? Легко! .. code-block:: shell sources=(/home/john) targets=(/var/backups) compression=xz tar_exclude=(.cache foo bar) Для передачи опций для tar есть переменная ``tar_options`` в которой можно переопределить умолчание — ``-acf``. Теперь добавим базу данных MySQL в бэкап. Записывается URI в формате, который ещё называют DSN (data source name): .. code-block:: shell sources=( /home/john mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db ) targets=(/var/backups) compression=xz tar_exclude=(.cache foo bar) Обратите внимание, что пароль пользователя закодирован, чтобы не возникло коллизий из-за наличия в нём спецсимволов. Закодировать пароль крайне просто, вот пример на Perl, который можно выполнить прямо в командной строке: .. code-block:: perl echo ',#s{RGqH' | perl -MURI::Escape -wlne 'print uri_escape $_' Теперь в /var/backups окажется два файла — test_john_2022.10.08-1230.tar.xz и test_test_db_2022.10.08-1230.sql.xz. Сваливать все файлы в одну директорию может быть неудобно, поэтому давайте сохранять файлы в папки по датам. .. code-block:: shell sources=( /home/john mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db ) today=$(date +%Y-%m-%d) # в результате даст дату yyyy-mm-dd make_target_dir=yes targets=(/var/backups/$today) compression=xz tar_exclude=(.cache foo bar) Теперь пути до файлов будут выглядеть так: /var/backups/2022-10-08/test_john_2022.10.08-1230.tar.xz. Теперь поробуем передать эти файлы ещё и на удалённое хранилище. Допустим на S3-совместимое объектное хранилище. Сперва придётся установить и настроить утилиту s3cmd, затем скрипт примет вид: .. code-block:: shell sources=( /home/john mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db ) today=$(date +%Y-%m-%d) make_target_dir=yes targets=( /var/backups/$today s3://my_bucket/$today ) compression=xz tar_exclude=(.cache foo bar) **boring_backup** сначала сделает бэкап и сохранит его в /var/backups и только затем передаст файлы в S3-хранилище (или любое другое). Можно указывать любое количество URI в targets и также в sources. Особенностью targets является то, что требуется хотя бы один URI со схемой `file`, куда будут сохраняться локальные бэкапы. Просто путь без указания схемы тоже считается за file. Следующие три записи полностью эквивалентны: * /var/backups * file:/var/backups * file:///var/backups Пойдём дальше. Мы можем обрабатывать ошибки. Допишем в скрипт функцию `on_error`, которая будет нам отправлять письмо с фрагментом лога и текстом ошибки. .. code-block:: shell sources=( /home/john mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db ) today=$(date +%Y-%m-%d) make_target_dir=yes targets=( /var/backups/$today s3://my_bucket/$today ) compression=xz tar_exclude=(.cache foo bar) email=me@example.com on_error() { local log_fragment err_message err_message="$*" log_fragment="$(grep -n 'Backup started' "$log_file" | tail -1 | cut -d ':' -f 1 | xargs -I {} tail -n +{} "$log_file")" printf \ 'Текст ошибки:\n%s\n\nЛог последнего бэкапа:\n%s' \ "$err_message" "$log_fragment" | mail -s "$HOSTNAME: Ошибка при выполнении бэкапа" "$email" } Также мы можем после копирование в удалённое хранилище удалять локальный бэкап. Список всех созданных за текущее выполнение скрипта бэкапов хранится в массиве ``backups``. .. code-block:: shell finalise() { log -p "\tClean up local backups" log -p "\tFiles to delete: ${backups[@]}" rm -rv -- "${backups[@]}" log -p "\tLocal backups deleted" } Это лишь небольшая часть того, что можно реализовать. Полная документация есть `в виде мануала `_. Отмечу ещё несколько важных фич: * Стандартную логику можно полностью переопределить, в том числе выкинуть необходимость сохранять локальный бэкап. Достаточно описать функцию с именем `backup`. * Можно выполнять действия перед бэкапом в функции `prepare` и после бэкапа — `finalise`. Есть и вещи, которые вам не понравятся, но решение которых вероятно появится в слудующих версиях **boring_backup**: * Концепция репозитория бэкапов не поддерживается. Файлы просто загружается туда куда вы укажете. * Из коробки можно делать только полные бэкапы. * Нет ротации бэкапов. Её нужно реализовывать самостоятельно. * Из коробки нет шифрования. GPG и кастомная функция `backup` вам в помощь. В заключение дам ещё один пример скрипта для бэкапа приложения Gitea. Попробуйте сами разобраться что тут происходит. .. code-block:: shell sources=(.) # just pass validation targets=(.) today="$(date +%d_%b_%Y)" s3cmd_config=~/.s3cfg prepare() { systemctl stop gitea.service sleep 5 } backup() { log -p "Dumping Gitea" su -c "/usr/local/bin/gitea dump -c /etc/gitea/app.ini \ -f /home/git/.cache/gitea_dump.zip" - git 2>> "$log_file" backups+=(/home/git/.cache/gitea_dump.zip) tgt_s3cmd s3://mybucket/backups/gitea-$today } finalise() { systemctl start gitea.service rm -rv -- "${backups[@]}" }