Итак, как наверняка все знают, совсем недавно 1-2 октября в Москве в “Инфопространстве” прошёл DevOpsConfRussia2018. Для тех кто не вкурсе, DevOpsConf — профессиональная конференция по интеграции процессов разработки, тестирования и эксплуатации.
Наша компания также приняла участие в этой конференции. Мы являлись её партнерами, представляя компанию на нашем стенде, а также провели небольшой митап. К слову это было первое наше участие в подобном роде деятельности. Первая конференция, первый митап, первый опыт.
О чём мы рассказывали? Митап был на тему “Бэкапы в Kubernetes”.
Скорее всего услышав это название, многие скажут: “А зачем бэкапить в Kubernetes? Его не нужно бэкапить, он же Stateless”.

Введение...
Давайте начнём с небольшой предыстории. Почему вообще возникла необходимость осветить эту тему и для чего это нужно.
В 2016 г. мы познакомились с такой технологией как Kubernetes и начали активно её применять для наших проектов. Конечно, в основном это проекты с микросервисной архитектурой, а это в свою очередь влечёт за собой использование большого количества разнообразного ПО.
С первым же проектом, где мы использовали Kubernetes, у нас встал вопрос о том как осуществлять резервное копирование расположенных там Stateful сервисов, которые иногда по тем или иным причинам попадают в k8s.
Мы начали изучать и искать уже существующие практики для решения данной задачи. Общаться с нашими коллегами и товарищами: "А как этот процесс осуществляется и построен у них?"
Пообщавшись, мы поняли, что у всех это происходит разными методами, средствами и с большим количеством костылей. При этом мы не проследили какого-либо единого подхода даже в рамках одного проекта.
Почему это так важно? Так как наша компания обслуживает проекты, построенные на базе k8s, нам просто необходимо было выработать структурированную методику по решению данной задачи.
Представьте, Вы работаете с одним определенным проектом в Кубере. Он содержит какие-то Stateful сервисы и Вам нужно бэкапить их данные. В принципе здесь можно обойтись парой костылей и забыть об этом. Но что если у Вас уже два проекта на k8s? И второй проект использует в своей работе совершенно другие сервисы. А если проектов уже пять? Десять? Или более двадцати?
Конечно, ставить костыли дальше, уже сложно и неудобно. Нужен какой-то единый подход, который можно было бы использовать при работе с множеством проектов на Кубе и при этом, чтобы команда инженеров могла легко и буквально за считанные минуты вносить необходимые изменения в работу бэкапов этих проектов.
В рамках данной статьи, мы как раз и расскажем о том, каким инструментом и какую практику мы используем для решения этой задачи внутри нашей компании.
Чем мы это делаем?
Nxs-backup что это?
Для бэкапов нами используется наш собственный open source инструмент — nxs-backup. Не будем вдаваться в детали того, что он может. Более подробно с ним можно ознакомиться по следующей ссылке.
Теперь перейдём к самой реализации бэкапов в k8s. Как и что именно нами было сделано.
Что бэкапим?
Давайте рассмотрим пример бэкапа нашего собственного Redmine. В нём мы будем бэкапить базу MySQL и пользовательские файлы проекта.
Как мы это делаем?
1 CronJob == 1 Сервис
На обычных серверах и кластерах на железе, почти все средства резервного копирования в основном запускаются через обычный cron. В k8s для этих целей мы используем CronJob'ы, т.е создаем 1 CronJob на 1 сервис, который мы будем бэкапить. Все эти CronJob’ы размещаются в том же namespace, что и сам сервис.
Начнем с базы данных MySQL. Чтобы осуществлять бэкап MySQL, нам потребуется 4 элемента, как и почти для любого другого сервиса:
- ConfigMap (nxs-backup.conf)
- ConfigMap (mysql.conf для nxs-backup)
- Secret (тут хранятся доступы к сервису, в данном случае MySQL). Обычно, этот элемент уже определён для работы сервиса и его можно переиспользовать.
- CronJob (для каждого сервиса свой)
Пойдём по порядку.
nxs-backup.conf
apiVersion: v1 kind: ConfigMap metadata: name: nxs-backup-conf data: nxs-backup.conf: |- main: server_name: Nixys k8s cluster admin_mail: admins@nixys.ru client_mail: - '' mail_from: backup@nixys.ru level_message: error block_io_read: '' block_io_write: '' blkio_weight: '' general_path_to_all_tmp_dir: /var/nxs-backup cpu_shares: '' log_file: /dev/stdout jobs: !include [conf.d/*.conf]
Здесь мы задаем основные параметры, передаваемые нашему инструменту, которые нужны для его работы. Это название сервера, e-mail для нотификаций, ограничение по потреблению ресурсов и другие параметры.
Конфигурации могут задаваться в формате j2, что позволяет использовать переменные окружения.
mysql.conf
apiVersion: v1 kind: ConfigMap metadata: name: mysql-conf data: service.conf.j2: |- - job: mysql type: mysql tmp_dir: /var/nxs-backup/databases/mysql/dump_tmp sources: - connect: db_host: {{ db_host }} db_port: {{ db_port }} socket: '' db_user: {{ db_user }} db_password: {{ db_password }} target: - redmine_db gzip: yes is_slave: no extra_keys: '--opt --add-drop-database --routines --comments --create-options --quote-names --order-by-primary --hex-blob' storages: - storage: local enable: yes backup_dir: /var/nxs-backup/databases/mysql/dump store: days: 6 weeks: 4 month: 6
В этом файле описывается логика бэкапов для соответствующего сервиса, в нашем случае это MySQL.
Тут можно указать:
- Как называется Job (поле: job)
- Тип Job’а (поле: type)
- Временную директорию, необходимую для сбора бэкапов (поле: tmp_dir)
- Параметры подключения к MySQL (поле: connect)
- Базу данных, которую будем бэкапить (поле: target)
- Необходимость останавливать Slave перед сбором (поле: is_slave)
- Дополнительные ключи для mysqldump (поле: extra_keys)
- Storage хранения, т.е в каком хранилище будем хранить копию (поле: storage)
- Директорию, куда мы будем складировать наши копии (поле: backup_dir)
- Схему хранения (поле: store)
В нашем примере тип хранения установлен local, т.е мы собираем и храним резервные копии локально в определённой директории запускаемого pod’а.
Вот прям по аналогии с этим файлом конфигурации можно задать такие же конфигурационные файлы и для Redis, PostgreSQL или любого другого нужного сервиса, если его поддерживает наш инструмент. О том, что он поддерживает можно узнать по ссылке, приведённой ранее.
Secret MySQL
apiVersion: v1 kind: Secret metadata: name: app-config data: db_name: "" db_host: "" db_user: "" db_password: "" secret_token: "" smtp_address: "" smtp_domain: "" smtp_ssl: "" smtp_enable_starttls_auto: "" smtp_port: "" smtp_auth_type: "" smtp_login: "" smtp_password: ""
В секрете мы храним доступы для подключения к самому MySQL и почтовому серверу. Их можно хранить или в отдельном секрете, или воспользоваться существующим, конечно если он есть. Тут ничего интересного. В нашем секрете также хранится secret_token, необходимый для работы нашего Redmine.
CronJob MySQL
apiVersion: batch/v1beta1 kind: CronJob metadata: name: mysql spec: schedule: "00 00 * * *" jobTemplate: spec: template: spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - nxs-node5 containers: - name: mysql-backup image: nixyslab/nxs-backup:latest env: - name: DB_HOST valueFrom: secretKeyRef: name: app-config key: db_host - name: DB_PORT value: '3306' - name: DB_USER valueFrom: secretKeyRef: name: app-config key: db_user - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-config key: db_password - name: SMTP_MAILHUB_ADDR valueFrom: secretKeyRef: name: app-config key: smtp_address - name: SMTP_MAILHUB_PORT valueFrom: secretKeyRef: name: app-config key: smtp_port - name: SMTP_USE_TLS value: 'YES' - name: SMTP_AUTH_USER valueFrom: secretKeyRef: name: app-config key: smtp_login - name: SMTP_AUTH_PASS valueFrom: secretKeyRef: name: app-config key: smtp_password - name: SMTP_FROM_LINE_OVERRIDE value: 'NO' volumeMounts: - name: mysql-conf mountPath: /usr/share/nxs-backup/service.conf.j2 subPath: service.conf.j2 - name: nxs-backup-conf mountPath: /etc/nxs-backup/nxs-backup.conf subPath: nxs-backup.conf - name: backup-dir mountPath: /var/nxs-backup imagePullPolicy: Always volumes: - name: mysql-conf configMap: name: mysql-conf items: - key: service.conf.j2 path: service.conf.j2 - name: nxs-backup-conf configMap: name: nxs-backup-conf items: - key: nxs-backup.conf path: nxs-backup.conf - name: backup-dir hostPath: path: /var/backups/k8s type: Directory restartPolicy: OnFailure
Пожалуй, вот этот элемент самый интересный. Во-первых, для того, чтобы составить правильный CronJob — необходимо определить где будут храниться собранные бэкапы.
У нас для этого выделен отдельный сервер с необходимым количеством ресурсов. В примере под сбор резервных копий отведена отдельная нода кластера — nxs-node5. Ограничение запуска CronJob на нужных нам нодах мы задаём директивой nodeAffinity.
При запуске CronJob к нему через hostPath с хост-системы подключается соответствующий каталог, который как раз и используется для хранения резервных копий.
Далее, к конкретному CronJob подключаются ConfigMap’ы, содержащие конфигурацию для nxs-backup, а именно, файлы nxs-backup.conf и mysql.conf, о которых мы только что говорили выше.
Затем, задаются все нужные переменные окружения, которые определяются непосредственно в манифесте или подтягиваются из Secret’ов.
Итак, переменные передаются в контейнер и через docker-entrypoint.sh подменяются в ConfigMaps в нужных нам местах на нужные значения. Для MySQL это db_host, db_user, db_password. Порт в данном случае мы передаем просто как значение в манифесте CronJob’а, т.к он не несёт какой-либо ценной информации.
Ну, с MySQL вроде всё понятно. А теперь давайте посмотрим, что нужно для бэкапа файлов приложения Redmine.
desc_files.conf
apiVersion: v1 kind: ConfigMap metadata: name: desc-files-conf data: service.conf.j2: |- - job: desc-files type: desc_files tmp_dir: /var/nxs-backup/files/desc/dump_tmp sources: - target: - /var/www/files gzip: yes storages: - storage: local enable: yes backup_dir: /var/nxs-backup/files/desc/dump store: days: 6 weeks: 4 month: 6
Это конфигурационный файл, описывающий логику бэкапов для файлов. Здесь тоже нет ничего необычного, задаются все те же параметры, что и у MySQL, за исключением данных для авторизации, т.к их попросту нет. Хотя они могут и быть, если будут задействованы протоколы для передачи данных: ssh, ftp, webdav, s3 и другие. Такой вариант мы рассмотрим чуть позже.
CronJob desc_files
apiVersion: batch/v1beta1 kind: CronJob metadata: name: desc-files spec: schedule: "00 00 * * *" jobTemplate: spec: template: spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - nxs-node5 containers: - name: desc-files-backup image: nixyslab/nxs-backup:latest env: - name: SMTP_MAILHUB_ADDR valueFrom: secretKeyRef: name: app-config key: smtp_address - name: SMTP_MAILHUB_PORT valueFrom: secretKeyRef: name: app-config key: smtp_port - name: SMTP_USE_TLS value: 'YES' - name: SMTP_AUTH_USER valueFrom: secretKeyRef: name: app-config key: smtp_login - name: SMTP_AUTH_PASS valueFrom: secretKeyRef: name: app-config key: smtp_password - name: SMTP_FROM_LINE_OVERRIDE value: 'NO' volumeMounts: - name: desc-files-conf mountPath: /usr/share/nxs-backup/service.conf.j2 subPath: service.conf.j2 - name: nxs-backup-conf mountPath: /etc/nxs-backup/nxs-backup.conf subPath: nxs-backup.conf - name: target-dir mountPath: /var/www/files - name: backup-dir mountPath: /var/nxs-backup imagePullPolicy: Always volumes: - name: desc-files-conf configMap: name: desc-files-conf items: - key: service.conf.j2 path: service.conf.j2 - name: nxs-backup-conf configMap: name: nxs-backup-conf items: - key: nxs-backup.conf path: nxs-backup.conf - name: backup-dir hostPath: path: /var/backups/k8s type: Directory - name: target-dir persistentVolumeClaim: claimName: redmine-app-files restartPolicy: OnFailure
Тоже ничего нового, относительно MySQL. Но тут монтируется один дополнительный PV (target-dir), как раз который мы и будем бэкапить — /var/www/files. В остальном всё так же, храним копии локально на нужной нам ноде, за которой закреплён CronJob.
Итог
Для каждого сервиса, который мы хотим бэкапить, мы создаём отдельный CronJob со всеми необходимыми сопутствующими элементами: ConfigMaps и Secrets. По аналогии с рассмотренными примерами, мы можем бэкапить любой аналогичный сервис в кластере.
Я думаю, исходя из этих двух примеров у всех сложилось какое-то представление, как именно мы бэкапим Stateful сервисы в Кубе. Думаю, нет смысла разбирать подробно такие же примеры и для других сервисов, т.к в основном они все похожи друг на друга и имеют незначительные различия.
Собственно, этого мы и хотели добиться, а именно — какого-то унифицированного подхода при построении процесса резервного копирования. И чтобы этот подход можно было бы применять на большое число различных проектов на базе k8s.
Где храним?
Во всех рассмотренных выше примерах мы храним копии в локальной директории ноды на которой запущен контейнер. Но никто не мешает подключить Persistent Volume как уже рабочее внешнее хранилище и собирать копии туда. Или можно только синхронизировать их на удаленное хранилище по нужному протоколу, не сохраняя локально. То есть вариаций достаточно много. Сперва собрать локально, потом синхронизировать. Либо собирать и хранить только на удалённом хранилище, и.т.д. Настройка осуществляется достаточно гибко.
mysql.conf + s3
Ниже приведен пример файла конфигурации бэкапа MySQL, где копии хранятся локально на той ноде где выполняется CronJob, а также синхронизируются в s3.
apiVersion: v1 kind: ConfigMap metadata: name: mysql-conf data: service.conf.j2: |- - job: mysql type: mysql tmp_dir: /var/nxs-backup/databases/mysql/dump_tmp sources: - connect: db_host: {{ db_host }} db_port: {{ db_port }} socket: '' db_user: {{ db_user }} db_password: {{ db_password }} target: - redmine_db gzip: yes is_slave: no extra_keys: ' --opt --add-drop-database --routines --comments --create-options --quote-names --order-by-primary --hex-blob' storages: - storage: local enable: yes backup_dir: /var/nxs-backup/databases/mysql/dump store: days: 6 weeks: 4 month: 6 - storage: s3 enable: yes backup_dir: /nxs-backup/databases/mysql/dump bucket_name: {{ bucket_name }} access_key_id: {{ access_key_id }} secret_access_key: {{ secret_access_key }} s3fs_opts: {{ s3fs_opts }} store: days: 2 weeks: 1 month: 6
Т.е, если будет недостаточно хранить копии локально, можно синхронизировать их на любое удаленное хранилище по соответствующему протоколу. Число Storage для хранения может быть любым.
Но в данном случае всё-таки потребуется внести некоторые дополнительные изменения, а именно:
- Подключить соответствующий ConfigMap с содержимым, необходимым для авторизации у AWS S3, в формате j2
- Создать соответствующий Secret для хранения доступов авторизации
- Задать нужные переменные окружения, взятые из Secret’а выше
- Скорректировать docker-entrypoint.sh для замены в ConfigMap соответствующих переменных
- Пересобрать Docker образ, добавив в него утилиты для работы с AWS S3
Пока этот процесс далёк от совершенства, но мы над этим работаем. Поэтому, в ближайшее время мы добавим в nxs-backup возможность определять параметры в конфигурационном файле с помощью переменных окружения, что значительно упростит работу с entrypoint файлом и минимизирует временны́е затраты на добавление поддержки бэкапа новых сервисов.
Заключение
На этом, наверное, всё.
Использование подхода, про который только что было рассказано, в первую очередь позволяет структурировано и по шаблону организовать резервное копирование Stateful сервисов проекта в k8s. Т.е это уже готовое решение, а самое главное практика, которую можно применять в своих проектах, при этом не тратя время и силы на поиск и доработку уже имеющихся open source решений.
