Автоматические бэкапы БД PostgreSQL по расписанию

В этой статье я поделюсь скриптом для создания бэкапов БД PostgreSQL за определенный период (например: 1, 2, 3 дня, 1 неделя, 1 месяц, 6 месяцев, каждый год).
Объясню как запустить скрипт с помощью расписания crontab, покажу как настроить синхронизацию папки с бэкапами с облаком Yandex Disk.

TL; DR

  1. Ссылка на репозиторий с python скриптом для создания бэкапов:
    https://github.com/oktend/server-backup-example

  2. Добавление расписания в crontab:

sudo apt-get update
sudo apt-get install cron
crontab -e
0 3 * * * cd /home/user/server-backup-example; /home/user/.local/bin/poetry run python src/main.py
  1. Синхронизация с Yandex Disk

echo "deb http://repo.yandex.ru/yandex-disk/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/yandex-disk.list > /dev/null && wget http://repo.yandex.ru/yandex-disk/YANDEX-DISK-KEY.GPG -O- | sudo apt-key add - && sudo apt-get update && sudo apt-get install -y yandex-disk
yandex-disk setup

Задача

Меня зовут Вадим Резвов, я начинающий системный администратор.

В мои обязанности входит: мониторинг состояния всех основных сервисов и систем проекта, создание бэкапов, поддержка ключевых компонентов сервера.

В одном из моих текущих проектов возникла задача — создавать регулярные бэкапы БД PostgreSQL за определенные отрезки времени (1, 2, 3 дня, 1 неделя, 1 месяц, 6 месяцев, каждый год — такое расписание я сформировал по запросу заказчика, чтобы была возможность откатиться на короткий, средний или длинный срок), также бэкап включает в себя папку с media и static контентом.
Регулярные бэкапы необходимы для того, чтобы была возможность восстановиться до последней работающей версии сервера.
В зависимости от того насколько давно возникла ошибка, можно откатиться на 1 день, месяц, год и т.д. — расписание можно поменять в server-backup-example/src/main.py, в 35–44 строке, все числа указаны в днях:

BACKUPS_TIMETABLE = [
    0,
    1, 
    2,
    3,
    7,
    30,
    180, 
    360, 
]

Бэкап попадающий в интервал между:
0 и 1, — бэкап за 1 день,
1 и 2, — за 2 день,
2 и 3, — за 3 день,
3 и 7, за 1 неделю,
7 и 30, за 1 месяц, и т.д.
В каждом интервале всегда находится только 1 бэкап попадающий в интервал, другой бэкап либо сдвигается в следующий интервал, либо удаляется.
Бэкапы в интервале после 360 — создаются за каждый год, и не удаляются.
Сообщения об ошибках и успешных бэкапах будут приходить в «Support» Telegram топик, — это сделано для того, чтобы постоянные сообщения о бэкапах не спамили в основной топик.

Решение задачи по отправке сообщений в определенный Telegram topic я описал в прошлой статье:

https://habr.com/ru/articles/770582/

Python скрипт для создания бэкапов за определенные отрезки времени

Репозиторий с кодом для создания бэкапов:
https://github.com/oktend/server-backup-example

Рассмотрим основные моменты в коде:

Функция create_dump_postgres делает dump БД PostgreSQL:

def  create_dump_postgres(now, dump_filename):     
    command = PG_DUMP_COMMAND_TEMPLATE.format(
        password=os.getenv("DATABASE_PASSWORD"),
        user=os.getenv("DATABASE_USER"),
        database=os.getenv("DATABASE_NAME"),
        port=os.getenv("DATABASE_PORT"),
        filename=dump_filename,
    )
    result = os.system(command)
    if result != 0:
        log.error(f"pg_dump failed with code {result}, command = {command}")
        sys.exit(10)
        

Функция create_backup_file архивирует dump БД (sql файл) и папку media в один файл формата tar.gz

def create_backup_file(now):
   log.info("create_backup_file started")
   archive_filename = BACKUPS_PATH / BACKUP_FILENAME_TEMPLATE.format(now=now)
   with tempfile.TemporaryDirectory() as tmpdirname:
       dump_filename = SQLDUMP_FILENAME_TEMPLATE.format(now=now)
       dump_filepath = Path(tmpdirname) / dump_filename
       create_dump_postgres(now, dump_filepath)
       tar_command = TAR_COMMAND_TEMPLATE.format(
           archive_filename=archive_filename,
           dump_dirname=tmpdirname,
           dump_filename=dump_filename,
           media_dirpath=os.getenv("MEDIA_DIRPATH"),
           media_dirname=os.getenv("MEDIA_DIRNAME"),
       )
      

Список BACKUPS_TIMETABLE далее используется в функции rotate_backups, чтобы удалить лишние бэкапы в определенных интервалах:

def rotate_backups(now):
    def add_backup(intervals, backup):
        for interval in intervals:
            if interval["start"] <= backup["timestamp"] and backup["timestamp"] < interval["end"]:
                interval["backups"].append(backup)
                return
        log.warning(f"found a file that does not belong to any interval: {backup}")


    def clear_extra_backups(intervals):
        for interval in intervals: 
            backups = sorted(interval["backups"], key=lambda a: a["timestamp"], reverse=True)
            for i in range(len(backups) - 1):
                filename = backups[i]["filename"] 
                log.info(f"deleting extra backup: {filename}")
                os.remove(filename)
                backups[i]["status"] = "deleted" 

    log.info("rotate_backups started") 
    intervals = []
    for i in range(len(BACKUPS_TIMETABLE) - 1):
        end = BACKUPS_TIMETABLE[i]
        start = BACKUPS_TIMETABLE[i + 1]
        intervals.append({
            "start": now - timedelta(days=start), 
            "end": now - timedelta(days=end),
            "backups": [],
            "days_end": BACKUPS_TIMETABLE[i],
            "days_start": BACKUPS_TIMETABLE[i + 1],
        })

По всему коду раскинуто логирование, например:

log.error(f"pg_dump failed with code {result}, command = {command}")
log.warning(f"found a file that does not belong to any interval: {backup}")
log.info("create_backup_file started")
log.debug(f"tar_command = {tar_command}")

В файле src/log.py в 33 строке, можно установить определенный уровень логирования, чтобы получать только важные сообщения (по уровню критичности логи имеют следущий порядок: DEBUG, INFO, WARNING, ERROR, CRITICAL), начиная с установленного уровня сообщения будут прилетать в Telegram топик:

telegram_handler.setLevel(logging.WARNING)

В .env файле прописываете правильные значения переменных:

BACKUPS_DIR="/home/user/backups"
DATABASE_USER=user
DATABASE_PASSWORD=123
DATABASE_NAME=database
DATABASE_PORT=5432
MEDIA_DIRPATH=/home/user/your_project_repo/static_content/
MEDIA_DIRNAME=media 
LOGFILE=logs/server_backup.log
TELEGRAM_NOTIFICATION_BOT_TOKEN="1234567890:asjWJHdejKJWKKklkli"
TELEGRAM_NOTIFICATION_CHAT_ID="-1234567890"
DUMP_MINIMAL_FILESIZE_MB="100"
TELEGRAM_MESSAGE_THREAD_ID="1234"

В данном примере полный путь до директории (/home/user/your_project_repo/static_content/media),
я разбил на две переменные MEDIA_DIRPATH=/home/user/your_project_repo/static_content/ и
MEDIA_DIRNAME=media, это нужно для корректной работы скрипта.
Если у вас нет media контента, в MEDIA_DIRPATH пропишите путь до любой фейковой директории, не включая саму директорию, а в MEDIA_DIRNAME укажите название самой директории.

В файле src/log.py функция init_logger берет из .env файла ранее указанные значения bot_token, chat_id, message_thread_id, на их основе сообщения отправляются в определенный Telegram топик:

def init_logger(filename, telegram_bot_token, telegram_chat_id, telegram_message_thread_id):

Как узнать значения bot_token, chat_id, message_thread_id, я рассказывал в прошлой статье:

https://habr.com/ru/articles/770582/

После окончания редактирования, в папке репозитория (server-backup-example) нужно выполнить следущие команды установки python, poetry, чтобы подготовить репозиторий к работе:

curl -sSL https://install.python-poetry.org | python3 -
poetry install
poetry shell

Crontab

Чтобы бэкапы автоматически создавались по расписанию, будем использовать crontab, который будет выполнять по указанному расписанию определенную команду (запуск скрипта по созданию бэкапа).
Для начала скачиваем crontab:

sudo apt-get update
sudo apt-get install cron

Далее нужно настроить расписание crontab, для этого прописываем команду:

crontab -e

и добавляем последнюю строку, запускающую backup скрипт, весь остальной не используемый код должен быть закоментирован:

0 3 * * * cd /home/user/server-backup-example; /home/user/.local/bin/poetry run python src/main.py

a1c524463dc855721ad360707d6c2c02.png

В записи »0 3 * * *» 0 — минуты, 3 — часы, * — дни, * — месяцы, * — день недели (1–7), например 1 — это понедельник.

В данной записи бэкап создается каждый день в 3 часа ночи по серверному времени.

Далее в ручную запускаем команду из crontab, чтобы проверить создается ли бэкап:

cd /home/user/server-backup-example; /home/user/.local/bin/poetry run python src/main.py

Синхронизация с облачным хранилищем Yandex Disk

Чтобы при возникновении каких-либо проблем с хранилищем на сервере мы не потеряли файлы бэкапов, имеет смысл привязать папку с бэкапами на сервере к папке с бэкапами в облачном хранилище, например Yandex Disk.
Для синхронизации понадобится аккаунт Yandex Disk, если его нет, нужно зарегестрировать новый:
https://passport.yandex.ru/registration? mode=register

Для начала, нужно скачать yandex disk cli:

echo "deb http://repo.yandex.ru/yandex-disk/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/yandex-disk.list > /dev/null && wget http://repo.yandex.ru/yandex-disk/YANDEX-DISK-KEY.GPG -O- | sudo apt-key add - && sudo apt-get update && sudo apt-get install -y yandex-disk

Далее запускаем настройку синхронизации:

yandex-disk setup

Когда запросят, выбираем путь к папке с бэкапами (та же папка, которую указали в .env файле: /home/user/backups), которая будет синхронизироваться с yandex диском.

Теперь бэкапы должны создаваться по расписанию и синхронизироваться с облаком Yandex Disk.

Заключение

Надеюсь моя статья будет полезна тем, кто хочет настроить создание бэкапов для своего сервера.

Буду рад любой критике, коментариям, отзывам.

© Habrahabr.ru