Бэкапы Stateful в Kubernetes

от автора

Итак, как наверняка все знают, совсем недавно 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 решений.


ссылка на оригинал статьи https://habr.com/company/nixys/blog/426543/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *