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