Сегодня обеспечение надежности и доступности данных в Kubernetes — ключевая задача для разработчиков и системных администраторов. Несмотря на высокую степень автоматизации и управления контейнерами, сбои и человеческие ошибки непредсказуемы. Поэтому резервное копирование становится неотъемлемой частью любого приложения.
Меня зовут Филипп, я системный администратор в отделе Data- и ML-продуктов Selectel. В этой статье постараюсь раскрыть, зачем настраивать резервное копирование в Kubernetes, и на простом примере покажу, как это сделать.
Резервное копирование в Kubernetes
Динамичная и распределенная архитектура Kubernetes обязывает использовать резервные копии. Необходимо учитывать состояние каждого контейнера, конфигурации, секрета и блока данных, которые находятся в постоянном хранилище.
Важно отметить, что в Kubernetes управление постоянным хранилищем осуществляется через основные компоненты.
- PVC (Persistent Volume Claim) — это запрос на использование хранилища со стороны приложения или пользователя.
- PV (Persistent Volume) — физическое или логическое хранилище в кластере, которое может быть связано с внешними хранилищами.
- Storage Class — структура, которая определяет, как PV создается.
- CSI (Container Storage Interface) — плагин, который позволяет взаимодействовать с различными внешними хранилищами.
- Внешнее хранилище — облачное или локальное хранилище, где фактически хранятся данные.
Также Kubernetes поддерживает парадигму Infrastructure as code, IaC. Она позволяет автоматизировать и стандартизировать развертывание и управление ресурсами с помощью GitOps. Однако даже в таком случае есть важная деталь, которую нельзя игнорировать: сохранность данных.
Пока ресурсы типа config-map, secret, deployment, service, ingress и другие могут быть воссозданы из кода по IaC, данные в PV восстановить в случае сбоя, удаления или взлома не получится.
Какие есть варианты
Можно придумать разные способы резервного копирования постоянных данных. Это могут быть, например, кастомные скрипты, которые запускаются по расписанию (cron) и создают снапшоты (снимки) самих PV.
При создании снимков PV могут возникнуть проблемы, если во время процесса в базу данных активно записываются данные. Например, снапшот может получиться неполным или поврежденным. Тогда при попытке восстановиться есть риск, что база данных запустится некорректно.
Однако есть сложные системы, которые обеспечивают непрерывное резервное копирование и автоматическое восстановление в случае сбоя. Среди них — Velero.io, Kasten K10, Cohesity, Portworx и другие.
Выбор стратегии резервного копирования будет зависеть от текущей реализации. Например, если ваше высоконагруженное приложение запущено в Managed Kubernetes, а стремительно растущие базы данных — на сетевых дисках, необходимо подумать о том, чтобы резервное копирование не влияло на работоспособность IT-систем.
Например, если основная база данных — PostgreSQL, необходимо подумать о бэкапировании с помощью streaming replication и настроить резервное копирование на secondary-реплике через pg_dump, pg_basebackup, Barman или WAL-E. А также — определиться с периодичностью бэкапирования, его шифрованием, способом хранения, мониторингом и проверкой восстановления из резервных копий.
Далее мы рассмотрим два инструмента K8UP и Kasten K10 от Veeam, которые предоставляют различный функционал для резервного копирования. Но прежде подготовим тестовый стенд в облаке Selectel.
Подготовка рабочего окружения
Подготовим тестовый стенд, в который установим простейшее приложение с постоянным хранилищем данных. За основу возьмем Managed Kubernetes, а в качестве хранилища резервных копий — объектное хранилище S3, к которому настроим доступ через сервисного пользователя и S3-ключ.
Подготовка кластера Managed Kubernetes
1. В панели управления нужно перейти в раздел Kubernetes:
2. Далее нужно создать и настроить кластер: ввести его имя, выбрать версию Kubernetes, регион, тип и подсеть для нод.
3. После настройки кластера необходимо добавить группы нод:
4. Далее дождитесь статуса ACTIVE и скачайте kubeconfig для дальнейшей работы с кластером:
5. Для работы с кластером можно использовать Lens или Kubectl. Для kubectl необходимо указать переменную KUBECONFIG:
#: export KUBECONFIG=<ПОЛНЫЙ ПУТЬ ДО KUBECONFIG ФАЙЛА>
6. В качестве проверки можно получить пространство имен кластера:
#: kubectl get namespace
Подготовка объектного хранилища
1. В панели управления нужно перейти в раздел Объектное хранилище → Контейнеры:
2. Далее нужно создать и настроить контейнер: ввести его название, выбрать Приватный тип, класс хранения Стандартное хранение и нажать кнопку Создать контейнер.
Создание сервисного пользователя и ключа S3
1. В панели управления нужно перейти в меню Управление пользователями:
2. Далее откройте раздел Сервисные пользователи:
3. Создайте пользователя и настройте данные для доступа к объектному хранилищу в вашем проекте облачной платформы:
4. Последним этапом нужно добавить S3-ключ для доступа к хранилищу:
Важно: S3 access и secret keys необходимо сохранить в надежном месте.
Установка тестового приложения в кластере
Теперь осталось установить в кластер простейшее тестовое приложение. Для этого подготовим манифест создания Storage Class, Deployment и Persistent Volume Claim и применим его:
# nginx-pvc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: default
annotations:
storageclass.kubernetes.io/is-default-class: 'true'
provisioner: cinder.csi.openstack.org
parameters:
availability: ru-9a
fsType: ext4
type: fast.ru-9a
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: nginx-volume
mountPath: /usr/share/nginx/html
volumes:
- name: nginx-volume
persistentVolumeClaim:
claimName: nginx-pvc
#: kubectl apply -f nginx-pvc.yaml
Готово — теперь можем начать работу с инструментами резервного копирования в Kubernetes.
Резервное копирование на базе K8up
K8up (/keɪtæpp/ или просто «кетчуп») — это оператор для автоматизации резервного копирования данных в Kubernetes, в основе которого лежит restic.
Возможности инструмента
- Автоматическое копирование данных из PVC и БД.
- Запуск резервного копирования по требованию и расписанию.
- Сохранение копий в разных местах, включая Amazon S3 и Minio.
- Восстановление данных через командную строку.
- Возможность создавать резервные копии, которые учитывают особенности приложений и включают результаты работы любого инструмента, способного выдавать данные в стандартный поток вывода.
Установка
Рассмотрим процесс установки и настройки K8up в подготовленном рабочем окружении. Для наглядности представим схему работы инструмента.
1. Устанавливаем Custom Resource Definition:
#: kubectl apply -f https://github.com/k8up-io/k8up/releases/download/k8up-4.4.1/k8up-crd.yaml
2. Для удобства устанавливаем K8up в отдельное пространство имен:
#: kubectl create namespace k8up
#: helm repo add k8up-io https://k8up-io.github.io/k8up
#: helm install -n k8up k8up k8up-io/k8up
Настройка хранилища для резервных копий
Перед созданием резервных копий необходимо указать хранилище, в котором они будут храниться. Подробнее о вариантах хранения можно можно почитать в документации restic.
Секреты для доступа по S3
Ранее мы подготовили объектное хранилище. Теперь создадим секреты, чтобы у K8up был к нему доступ. Укажем токен доступа к S3 и пароль для restic-репозитория:
# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: "s3-secrets"
namespace: k8up
stringData:
bucket: "<BUCKET NAME>"
password: ”<S3 SECRET KEY>”
username: ”<S3 ACCESS KEY>”
---
apiVersion: v1
kind: Secret
metadata:
name: "restic-repository-password"
namespace: k8up
stringData:
password: "secret_pass" # Это пароль для restic repository, создается при инициализации; его можно поменять на любой, желательно надежный и состоящий из более чем 16 символов
Применим описанный манифест в кластер:
#: kubectl apply -f secrets.yaml
Подготовительные работы выполнены, теперь можем приступить к созданию резервных копий.
Создание снимка из PVC
Теперь попробуем сделать бэкап данных приложения, которое мы задеплоили на этапе подготовки рабочего окружения. Для этого необходимо создать Custom Resource для K8up, тем самым объяснить, что и как мы хотим копировать.
Создадим CR-Backup, который сделает резервное копирование всех PVC в пространстве имен, где будет создан данный ресурс, а после — применим манифест:
# backup.yaml
apiVersion: k8up.io/v1
kind: Backup
metadata:
name: k8up-test-swift
namespace: k8up
spec:
failedJobsHistoryLimit: 4
successfulJobsHistoryLimit: 0
backend:
repoPasswordSecretRef:
name: "restic-repository-password"
key: "password"
s3:
accessKeyIDSecretRef:
key: username
name: s3-secrets
bucket: test
endpoint: s3.ru-1.storage.selcloud.ru
secretAccessKeySecretRef:
key: password
name: s3-secrets
#: kubectl apply -f backup.yaml
После создания ресурса K8up автоматически начнет поиск и создание снимков всех PVC с режимами доступа к хранилищу типа RMX (ReadMany, Execute) и RWO (ReadWriteOnce). Если все было выполнено правильно, мы должны увидеть успешно завершенный под с именем, в котором есть приставка backup-k8up.
Приставка означает, что под подмонтировал к себе нужный PVC (в нашем случае — nginx-pvc) и создал снимок файловой системы с помощью restic:
В S3-контейнере можно увидеть, что снимок файловой системы успешно создан с помощью restic. Также появилась папка, которую мы указали в манифесте:
То же самое можно проверить через CLI с помощью команд:
#: kubectl get -A backups.k8up.io
#: kubectl get -A snapshots.k8up.io
В K8up также можно настроить резервное копирование по расписанию и добавить проверку целостности с помощью custom-resource — Schedule.
Восстановление из снимка PVC
Локальное восстановление
Можно установить утилиту restic и подмонтировать снимок локально, указав чувствительные данные для доступа к объектному хранилищу. Но прежде необходимо узнать SNAPSHOT ID:
RESTIC_REPOSITORY="s3://s3.ru-1.storage.selcloud.ru/<S3-BUCKET NAME>/<S3 K8UP FOLDER PATH>" \
AWS_ACCESS_KEY_ID="" \
AWS_SECRET_ACCESS_KEY="" \
RESTIC_PASSWORD="" \
restic snapshots
Теперь мы можем скопировать к себе резервную копию командой:
RESTIC_REPOSITORY="s3://s3.ru-1.storage.selcloud.ru/<S3-BUCKET NAME>/<S3 K8UP FOLDER PATH>" \
AWS_ACCESS_KEY_ID="" \
AWS_SECRET_ACCESS_KEY="" \
RESTIC_PASSWORD="" \
restic restore <SNAPSHOT ID> --target ~/Desktop/mnt/
В папке ~/Desktop/mnt/ можно увидеть все файлы, находящиеся в снимке.
Восстановление в кластере Custom Resource
Для демонстрации восстановления внесем правки в index.html, добавив текст:
Теперь восстановим предыдущее состояние файла в кластере.
- Создадим customResource Restore и новый PVC, в который будем восстанавливать данные:
# restore.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: restore-test-mfw
namespace: k8up
annotations:
# set to "true" to include in future backups
k8up.io/backup: "false"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: k8up.io/v1
kind: Restore
metadata:
name: restore-test-mfw
namespace: k8up
spec:
podSecurityContext:
fsGroup: 65532
fsGroupChangePolicy: OnRootMismatch
restoreMethod:
folder:
claimName: restore-test-mfw
backend:
envFrom:
- secretRef:
name: "open-stack-secret"
repoPasswordSecretRef:
name: "restic-repository-password"
key: "password"
swift:
path: "/container-path"
container: "cmlp-testing-rpd-5921-ff-research-k8up"
2. В качестве восстановленной PVC можем указать новую, в которую восстановили бэкап:
kubectl patch -n k8up deployment nginx-deployment -p '{"spec":{"template":{"spec":{"volumes":[{"name":"nginx-volume","persistentVolumeClaim":{"claimName":"restore-test-mfw"}}]}}}}'
Далее можем открыть index.html и убедиться, что нам удалось откатить изменения.
Создание бэкапа баз данных
Проблема при создании бэкапов баз данных в том, что необходимо остановить запись в базу, чтобы на момент создания снимка не было никаких активных и открытых ячеек. Иначе в резервной копии будут незавершенные операции.
В результате мы пришли к выводу, что делать бэкап базы данных лучше встроенными утилитами — например, pg_dump, mongodump и другими. K8up позволяет нам использовать механизм запуска команд в поде, к которому будут добавлены аннотации.
Добавление аннотаций
Процесс создания аннотаций и их описание есть в документации K8up. Ниже — основные конфигурации для них.
annotations:
k8up.io/backupcommand: sh -c 'mongodump --username=$MONGODB_ROOT_USER --password=$MONGODB_ROOT_PASSWORD --archive'
k8up.io/file-extension: .archive
Конфигурация аннотации MongoDB.
annotations:
k8up.io/backupcommand: sh -c 'PGDATABASE="$POSTGRES_DB" PGUSER="$POSTGRES_USER" PGPASSWORD="$POSTGRES_PASSWORD" pg_dump --clean'
k8up.io/file-extension: .sql
Конфигурация аннотации PostgreSQL.
<SNIP>
template:
metadata:
labels:
app: my-db
annotations:
k8up.io/backupcommand: sh -c 'PGDATABASE="$POSTGRES_DB" PGUSER="$POSTGRES_USER" PGPASSWORD="$POSTGRES_PASSWORD" pg_dump --clean'
k8up.io/file-extension: .sql
k8up.io/backupcommand-container: postgres
spec:
containers:
- name: pgbouncer
- name: postgres
- name: prometheus-exporter
...
<SNIP>
Сборка аннотации на определенный контейнер.
Резервное копирование на базе Kasten K10 by Veeam
Kasten K10 создан специально для Kubernetes и представляет собой платформу управления данными Cloud Native для операций «второго дня».
Возможности инструмента
Инструмент предоставляет DevOps-командам простую, масштабируемую и безопасную систему для резервного копирования и восстановления приложений Kubernetes. Kasten K10 можно интегрировать с реляционными и NoSQL-базами данных и основными дистрибутивами Kubernetes.
Важно: бесплатная версия Kasten K10 ограничена пятью нодами.
Установка
PRE-FLIGHT проверки
Проверим наличие всех необходимых компонентов для Kasten K10:
#: curl https://docs.kasten.io/tools/k10_primer.sh | bash
В выводе команды мы увидим, чего не хватает для корректной работы K10. В нашем случае отсутствует CRD для создания снапшотов. Решим эту проблему.
1. Выведем имеющиеся для Volume CRD:
#: kubectl api-resources | grep volume
2. Теперь мы можем установить недостающие CRD и проверить, что они появились в списке api-resources:
#: kubectl apply -f https://raw.githubusercontent.com/selectel/mks-csi-snapshotter/master/deploy/setup-snapshot-controller.yaml
#: kubectl api-resources | grep volume
persistentvolumeclaims pvc v1 true PersistentVolumeClaim
persistentvolumes pv v1 false PersistentVolume
volumesnapshotclasses vsclass,vsclasses snapshot.storage.k8s.io/v1 false VolumeSnapshotClass
volumesnapshotcontents vsc,vscs snapshot.storage.k8s.io/v1 false VolumeSnapshotContent
volumesnapshots vs snapshot.storage.k8s.io/v1 true VolumeSnapshot
volumeattachments storage.k8s.io/v1 false VolumeAttachment
3. Повторно прогоняем проверки PRE-FLIGHT:
#: curl https://docs.kasten.io/tools/k10_primer.sh | bash
Ошибка по CRD-Base пропала, но появилась новая: CSI Provisioner doesn’t have VolumeSnapshotClass — Error. Все верно: CRD есть, а VolumeSnapshotClass отсутствует. Попробуем решить эту проблему.
1. Рабочий вариант — вручную подготовить манифест. Приведу пример для Cinder OpenStack. Подробнее о драйверах можно почитать на сайте.
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
driver: cinder.csi.openstack.org
metadata:
annotations:
k10.kasten.io/is-snapshot-class: "true"
name: csi-hostpath-snapclass-v1
deletionPolicy: Delete
2. Применим манифест и проверим наличие ресурса VolumeSnapshotClass:
#: kubectl apply -f VolumeSnapshotClass.yaml
volumesnapshotclass.snapshot.storage.k8s.io/cinder-csi-openstack-org created
#: kubectl get -f VolumeSnapshotClass.yaml
NAME DRIVER DELETIONPOLICY AGE
csi-hostpath-snapclass-v1 cinder.csi.openstack.org Delete 20s
3. Теперь можем снова запустить проверку PRE-FLIGHT:
#: curl https://docs.kasten.io/tools/k10_primer.sh | bash
Все пункты должны быть в статусе ОК.
4. Дополнительно необходимо добавить аннотацию k10.kasten.io/volume-snapshot-class в default storage-class:
#: kubectl annotate storageclass default \
k10.kasten.io/volume-snapshot-class=csi-hostpath-snapclass-v1
На этом предварительные настройки для работы Kasten K10 закончены. Теперь можем перейти к установке самого инструмента.
Установка Kasten K10
1. Добавляем репозиторий Kasten:
#: helm repo add kasten https://charts.kasten.io/
2. Создаем отдельное пространство имен:
#: kubectl create namespace kasten-io
3. Устанавливаем Helm-чарт с включением sidecar kanister, который необходим для создания снапшотов. С версии Kasten K10 6.5.0 он по умолчанию выключен.
#: helm install k10 kasten/k10 --namespace=kasten-io --set injectKanisterSidecar.enabled=true --set-string injectKanisterSidecar.namespaceSelector.matchLabels.k10/injectKanisterSidecar=true
4. Дожидаемся полной установки компонентов и пробрасываем порт на локальный хост:
#: kubectl get pods --namespace kasten-io --watch
#: kubectl --namespace kasten-io port-forward service/gateway 8080:8000
Супер — все загружено, теперь мы можем открыть веб-интерфейс инструмента по адресу http://127.0.0.1:8080/k10/#/dashboard.
Настройка хранилища для резервных копий
Для создания резервных копий из снимков, перемещения приложений и их данных между кластерами и разными облаками, а также — последующего импорта/экспорта этих резервных копий в другой кластер используются профили хранилища.
Инициализация профиля S3
Как и в случае с K8up, настроим объектное хранилище в качестве основного для всех резервных копий. Это можно сделать как через веб-интерфейс, так и с помощью YAML-манифестов.
1. Создадим и настроим S3-профиль хранения резервных копий в объектном хранилище Selectel. Для этого перейдем на страницу профилей (http://127.0.0.1:8080/k10/#/profiles/location). Нас встретит такая страница:
2. Чтобы создать профиль местоположения, нужно нажать New Profile на странице профилей и заполнить поля:
3. Далее необходимо сохранить профиль. Если все сделали правильно, в списке увидите профиль со статусом VALID:
Настройка резервного копирования
Приступим к настройке периодических резервных копий нашего приложения.
1. Укажем label для пространства имен, в которое установлено приложение. В нашем случае namespace=default:
#: kubectl label namespace default k10/injectKanisterSidecar=true
2. Создадим Policy. Это можно сделать из веб-панели Kasten K10, но мы воспользуемся манифестом:
#sample-backup-action.yam
apiVersion: config.kio.kasten.io/v1alpha1
kind: Policy
metadata:
name: test
namespace: kasten-io
spec:
frequency: "@hourly"
paused: false
actions:
- action: backup
backupParameters:
profile:
name: selectel-s3
namespace: kasten-io
- action: export
exportParameters:
frequency: "@hourly"
migrationToken:
name: test-migration-token
namespace: kasten-io
exportData:
enabled: true
retention: {}
retention:
hourly: 24
daily: 7
weekly: 4
monthly: 12
yearly: 7
selector:
matchExpressions:
- key: k10.kasten.io/appNamespace
operator: In
values:
- default
2. Применим манифест в кластере:
#: kubectl create -f sample-backup-action.yaml
3. Если все сделано правильно, должен создаться backupaction. Его можно вывести с помощью специальной команды:
#: kubectl get backupactions.actions.kio.kasten.io
NAME CREATED AT STATE PCT
scheduled-pwbvk 2023-11-02T09:08:00Z Complete 100
4. Во вкладке Action веб-интерфейса можно проверить статус резервного копирования:
Копируем файл в приложение
Для проверки восстановления из резервной копии скопируем файл, например, Snapshot-class.yaml и вручную запустим резервное копирование. После удалим файл и восстановимся из резервной копии.
1. Копируем файл Snapshot-class.yaml:
#: kubectl cp Snapshot-class.yaml default/nginx-deployment-5dcc6d978b-lc96j:/usr/share/nginx/html/
2. Вручную запускаем резервное копирование. Это можно сделать на странице Policies:
3. Дожидаемся завершения резервного копирования (статус можно отслеживать на странице Dashboard), а после — удаляем файл из примонтированной папки в контейнере:
#: kubectl --namespace default exec -it nginx-deployment-5dcc6d978b-ps6rs -- /bin/sh
Defaulted container "nginx-container" out of: nginx-container, kanister-sidecar
# rm /usr/share/nginx/html/Snapshot-class.yaml
Восстановление
Теперь можем проверить, восстанавливается ли система из резервной копии.
1. Перейдем во вкладку Application → Item-Menu → Restore:
2. Выберем нужную точку восстановления:
3. В появившемся окне Restore Point выберем приложение, поставим галочку рядом с Data-Only Restore и нажмем кнопку Restore:
4. Необходимо дождаться, пока задача на восстановление будет выполнена, и проверить, что ранее удаленный файл снова доступен в примонтированной папке:
Готово — резервное копирование на базе Kasten K10 работает!
Подводим итоги
Организация резервного копирования — сложный и комплексный процесс. Необходимо подумать о надежном хранении бэкапов, мониторинге их состояния, а также проверке на восстановление из них.
В своей ML-платформе мы используем инструмент K8up для резервного копирования важной информации — например, служебных данных ClearML. В качестве хранилища используем объектное хранилище. K8up создает файловые копии один раз в день, а также проверяет их на целостность.
Если вам интересно посмотреть, как это работает на практике, протестируйте нашу ML-платформу. Мы разворачиваем ее индивидуально для каждого клиента и можем включить в сборку такие open source-инструменты, как ClearML или Kubeflow. В общем, все для того, чтобы вы смогли организовать полный цикл обучения и тестирования ML-моделей.