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

Комментарии (0)