backup В любой инфраструктуре наступает момент, когда процесс разработки и деплоя налажен, врата в наш сервис открыты для пользователей и мы начинаем их обслуживать и накапливать ценные данные. К этому моменту также должен быть готов план бекапа и восстановления всех необходимых данных. Но когда мы говорим о контейнеризации наших приложений, мы зачастую думаем о таких веселых вещах, как:

  • как разбить инфраструктуру на микросервисы
  • как оптимизировать сборку контейнеров
  • как тестировать код
  • как выкатывать/обновлять сервисы

в общем, типичные шаги CI/CD-пайплайна. В этой картине мира после деплоя сервиса на прод как будто бы ничего больше и не происходит, а эксплуатации построенной инфраструктуры, как отдельной дисциплины как будто бы и нет.

Конечно же это не так. Иначе этот подход можно было бы переименовать в “А дальше хоть трава не расти”. Как минимум, для любого прод-окружения должны быть налажены процедуры мониторинга и бекапов, и в этой статье я остановлюсь на бекапах.

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

  • определение приложений и данных, подлежащих резервному копированию
  • определение периодичности снятия резервных копий
  • расчет необходимого места под бекапы
  • описание процедур восстановления
  • и многие другие…

Я не ставлю целью этой статьи создать подобную методичку, которая позволит организовать резервное копирование контейнеризированной инфраструктуры, которой сможет воспользоваться человек, ни разу в жизни не сделавший бекапов. Я буду отталкиваться от того, что читатель в целом умеет работать с бекапами и имеет опыт бекапов легаси-инфраструктуры. А остановлюсь именно на особенностях, возникающих именно при работе с контейнерами.

В чем разница?

Главная особенность контейнеров в наличии четкого разделения изменяемой и неизменяемой частей: кода и данных. Образ и созданный из него контейнер по умолчанию неизменяемы и мы должны явно нарушать это правило точечно прокалывая дырочки в изоляции файловой системы с помощью механизма вольюмов.

С одной стороны это хорошая подсказка о том, что именно нужно бекапить, с другой - возникает вопрос “а как именно?”. Наивная идея “просто бекапить вольюмы” (что-бы она не значила) довольно быстро разбивается о суровую реальность.

Универсального решения в данном вопросе нет, многие приложения требуют специфичного подхода. К счастью, абсолютное большинство попадают в три типичных категории:

  • не требуют бекапа
  • требуют файловый бекап
  • требуют специализированный бекап

пройдемся по ним, с разбором примеров.

Не требуют бекапов

Мечта любого админа: нет бекапов - нет проблем. К сожалению таких приложений не так уж много. Например это может быть контейнер redis или memcached, который используется в качестве кеша для уменьшения нагрузки на СУБД и не содержит данных, которые жалко потерять. Плюс, образы этих контейнеров находятся на ДокерХабе и о их сохранности беспокоится команда мейнтейнеров проекта.

Среди наших проектов тоже могут быть контейнеры, которым не нужны бекапы. Докерфайл простешего примера может выглядеть следующим образом:

FROM nginx
RUN echo "Hello, world!" > /usr/share/nginx/html/index.html

Это полностью статичный сайт, он не способен накапливать состояние, поэтому удалив его и пересоздав в другом месте мы ничего не потеряем. Т.к. все необходимые для работы данные будут “запечены” в образ, то все, что нам необходимо - это удостовериться в том, что этот образ всегда доступен.

– Понял, значит мы не бекапим контейнер, а бекапим образ? – Не совсем, нет смысла бекапить отдельные образы, надо просто обеспечить доступность реестра, в котором находятся наши образы – Значит мы бекапим наш реестр? – Уже ближе, но не совсем, сделаем еще один шаг назад, ближе к первоисточнику. Образы в реестре появляются не из космоса, а попадают туда по строго определенным рельсам CI-пайплайна. Мы всегда сможем их пересоздать, пока жива наша CI-система. – Бекапим Jenkins/GitLab/etc? – Уже совсем рядом, если более формально, то: для того, чтобы восстановить все образы в нашем реестре, нам нужен работающий репозиторий с исходным кодом и CI-сервер с определениями пайплайнов.

И этот воображаемый диалог плавно подводит нас ко второй категории контейнеров.

Требуют файловый бекап

Допустим мы не пользуемся облачными CI-решениями вроде связок GitHub+CircleCI, а хостим собственные решения, например Gitea+Jenkins или GitLab. В таком случае забота о сохранности нашей интеллектуальной собственности ложится на наши плечи.

Возьмем для примера Jenkins, он работает без СУБД и все свое состояние хранит в /var/jenkins_home. Этот путь заветится в вольюмах контейнера, а точнее будет единственным вольюмом. Для переноса на другой сервер достаточно скопировать содержимое этого каталога и “подсунуть” его в качестве вольюма нового контейнера.

Допустим наш Jenkins запущен как standalone-контейнер и его вольюм с данным смонитрован в /volumes/jenkins на сервере. Мы хотим сделать бекапы и положить их в S3-совместимое хранилище. Скрипт бекапа будет выглядеть примерно так:

#!/bin/bash
access_key=FOO
secret_key=BAR
server=ams3.digitaloceanspaces.com
bucket=backups
backup_file=jenkins_$(date +%Y%m%d_%H%M%S).tar.gz

# Create backup archive
tar -czvf /tmp/$backup_file -C /volumes/jenkins/backup .

# Send backup archive to backup server
docker run -i --rm \
-e AWS_ACCESS_KEY_ID=$access_key \
-e AWS_SECRET_ACCESS_KEY=$secret_key \
-v /tmp/$backup_file:/data/$backup_file \
zarbis/s3-backup \
s3cmd put \
--host $server \
--host-bucket "%(bucket)s.$server" \
/data/$backup_file s3://$bucket/jenkins/

# Remove backup archive
rm /tmp/$backup_file

Не вдаваясь в подробности мы:

  • ахривируем папку с определениями пайплайнов и прочими настройками
  • запускаем контейнер-заливалку в S3 со следующими параметрами:
    • передаем ключи от нашей S3-учетки
    • монтируем архив с бекапом внутрь контейнера как /data/$backup_file
    • запускаем s3cmd put /data/$backup_file
  • подчищаем за собой

Восстановление из бекапов будет выглядеть соответственно так:

#!/bin/bash

backup_file="$1"

# Cleanup volume dir
rm -rf /volumes/jenkins/*

# Stop Jenkins container
echo Stopping Jenkins container
docker stop jenkins

# Unpack backup
tar -xvf "$backup_file" -C /volumes/jenkins/

# Start Jenkins container
echo Starting Jenkins container
docker start jenkins

Тут нужно еще меньше комментариев, единственное - не восстанавливать бекап наживую.

Требуют специализированный бекап

Не все приложения можно бекапить просто копированием файлов, например в случае СУБД это приведет к неконсистентному состоянию из-за механизмов кеширования и прочих факторов. Для проведения бекапов СУБД как правило используются утилиты, поставляемые в комплекте с самой СУБД. Эти утилиты, в отличие от архиватора, знают с чем имеют дело, могут поговорить с СУДБ и произвести необходимые сервисные операции, вроде сброса кеша на диск, остановки записи и т.д.

Но несмотря на эти особенности, в целом, процедура будет выглядеть также, разве что перед созданием архива с бекапом нужно получить собственно файловый бекап средставами приложения:

docker exec -i mongodb mongodump
tar -czvf /tmp/$backup_file /volumes/mongodb/data/dump/

Заключение и рекомендации

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

  • создать консистентный бекап в файловом виде
  • этот бекап должен быть “экспортирован” за пределы фс контейнера в хостовый вольюм
  • этот бекап должен быть переложен в теплое сухое место
  • почистить за собой

Все в контейнеры

Главная же рекомендация - делать как можно больше шагов с помощью контейнеров. Например, я использовал контейнер с s3cmd вместо установки утилиты на сервер. Мотивация здесь очень простая: это задел на динамическое будущее. Пусть, возможно, сейчас ваша инфраструктура статична:

  • сервера вы создаете и настраиваете руками
  • используете хост-маунты в качестве вольюмов
  • не используете контейнерный оркестратор

… но с каждым шагом в сторону этого светлого динамического будущего использовать хостовые инструменты будет все сложнее и нецелесообранее.

Если мы будем создавать сервера для нашей инфраструктуры динамически - то мы конечно озаботимся автоматической установкой джентельменского набора софта, но это создает кучу проблем:

  • если мы добавляем в нашу экосистему новое приложение, то мы должны добавить его бекапилку в джентельменский набор
  • а что делать со старыми серверами? доустанавливать вручную? удалять и пересоздавать с нуля?
  • а что делать, когда приложение переезжает с одного сервера на другой?

Столько вопросов, а docker run -it --rm my-backup-tool работает всегда.

Рано или поздно мы откажемся от хостовых маунтов в качестве вольюмов (причины за пределами этой статьи). В таком случае у нас не будет простого и надежного способа достучаться с хоста до нашего файлового бекапа. Нам придется перенести процедуру бекапа целиком в контейнерные файловые системы, используя общие вольюмы для контейнера приложения и бекап-контейнера.

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

С учетом этих замечаний бекап-скрипты должны быть переписаны таким образом, чтобы исключить из них все хостовые шаги и заменить их на контейнеризированные варианты.

Бекапы - часть приложения

Микросервисная ахритектура, которая отлично ложится на контейнеры, подталкивает нас к созданию отдельных баз для каждого микросервиса, что может перевернуть картину нашей инфраструктуры из “у нас есть один ГЛАВНЫЙ ОБЩИЙ кластер мускуля” к “у нас десяток мускулей по всему кластеру”. В таком случае уследить за этим динамичным хозяйством пост-фактум, задним числом, становится нереально. Адекватный выход - включать в состав микросервиса не только приложение и его субд, но и сервисный бекап-контейнер. Тогда функционал бекапа будет следовать за приложением автоматически, в какой бы динамичной и оркестрируемой среде они не находились.