[home] [projects] [knowledge base] [manpages] [code] [markdown] [my ip]

Делаем скучные бэкапы

Бэкапы, в самом деле, нескучные (:

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

В какой-то момент мне надоело писать баш-портянки для бэкапа и я написал одну большую, которая умеет сразу много всего. Знакомьтесь — 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 выглядит так:

sources=(/home/john)
targets=(/var/backups)

Сохраним это в файл под именем test.

Здесь очевидно дира /home/john будет скопирована в /var/backups, однако cперва она будет запакована в архив tar без сжатия. Запускаем бэкап:

boring_backup test

Итого получим файл /var/backups/test_john_2022.10.08-1230.tar. Если мы хотим сжать архив, то добавим компрессию:

sources=(/home/john)
targets=(/var/backups)
compression=xz

Здесь значение переменной compression будет интерпретироваться так же как tar интерпретирует расширение архива с опцией --auto-compress. То есть поддерживаются все утилиты, которыми tar умеет сжимать. Если переменная пуста или содержит что-то не то, то сжатие будет отключено.

Надо исключить лишние файлы из архива? Легко!

sources=(/home/john)
targets=(/var/backups)
compression=xz
tar_exclude=(.cache foo bar)

Для передачи опций для tar есть переменная tar_options в которой можно переопределить умолчание — -acf.

Теперь добавим базу данных MySQL в бэкап. Записывается URI в формате, который ещё называют DSN (data source name):

sources=(
    /home/john
    mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db
)
targets=(/var/backups)
compression=xz
tar_exclude=(.cache foo bar)

Обратите внимание, что пароль пользователя закодирован, чтобы не возникло коллизий из-за наличия в нём спецсимволов. Закодировать пароль крайне просто, вот пример на 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. Сваливать все файлы в одну директорию может быть неудобно, поэтому давайте сохранять файлы в папки по датам.

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, затем скрипт примет вид:

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. Следующие три записи полностью эквивалентны:

Пойдём дальше.

Мы можем обрабатывать ошибки. Допишем в скрипт функцию on_error, которая будет нам отправлять письмо с фрагментом лога и текстом ошибки.

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.

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. Попробуйте сами разобраться что тут происходит.

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[@]}"
}