nixhacks.net/content/do_boring_backups.rst

247 lines
11 KiB
ReStructuredText
Raw Permalink Normal View History

2022-10-21 22:29:23 +03:00
:title: Делаем скучные бэкапы
:date: 8 Oct 22
=====================
Делаем скучные бэкапы
=====================
Бэкапы, в самом деле, нескучные (:
Мотивацией для написания очередного велосипеда стало то, что даже для, казалось
бы, готовых инструментов приходится писать обвязки на шелле.
В какой-то момент мне надоело писать баш-портянки для бэкапа и я написал одну
большую, которая умеет сразу много всего. Знакомьтесь — **boring_backup**.
> **Пакеты для Debian/Ubuntu, Arch и сорцы**
`здесь <https://git.nxhs.cloud/ge/boring_backup/releases>`_.
Сначала о том, чем он *НЕ* является:
* Тупым линейным скриптом с вызовом tar и mysqldump.
* Полным аналогом `restic <https://restic.net/>`_ и похожих утилит.
* 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"
}
Это лишь небольшая часть того, что можно реализовать. Полная документация
есть `в виде мануала </cgi-bin/man.cgi?q=boring_backup&l=ru>`_.
Отмечу ещё несколько важных фич:
* Стандартную логику можно полностью переопределить, в том числе выкинуть
необходимость сохранять локальный бэкап. Достаточно описать функцию с
именем `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[@]}"
}