247 lines
11 KiB
ReStructuredText
247 lines
11 KiB
ReStructuredText
: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[@]}"
|
||
}
|