Бэкапы 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 решений.

    Nixys
    30,00
    Компания
    Поделиться публикацией

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое