Все должно быть под контролем. Резервное копирование для Kubernetes. Часть 2
В первой части мы рассмотрели подходы к созданию резервных копий контейнеров в кластере Kubernetes с использованием restic над каталогом данных и относительно новых возможностей CSI для создания и восстановления мгновенных снимков. Пришло время поговорить о возможностях автоматизации управления резервными копиями, о мониторинге процесса и иных важных DevOps-задачах.
Прежде всего для автоматизации нужно решить вопрос с доступом к данным и исключить необходимость установки дополнительных приложений (например. restic) на управляющий узел. В основном сценарии развертывания сервисы предполагается, что мы используем возможности Kubernetes по выделению емкости системы хранения через PersistentVoIumeClaim с режимом доступа ReadWriteOnce, который допускает монтирование раздела для записи только на один контейнер, что затрудняет восстановление данных из резервной копии. Также для создания резервной копии было бы желательно, чтобы доступ к данным был также у специального контейнера, содержащего все необходимые инструменты (например, Restic). Попробуем разобраться как это может быть реализовано и поговорим о концепции sidecar-контейнеров.
Прежде всего посмотрим на конфигурацию развертывания контейнера (может быть Deployment, DaemonSet или StatefulSet) и обнаружим, что в спецификации шаблона развертывания определение контейнеров перечисляется в списке containers. Такая схема определения позволяет запускать несколько контейнеров внутри одного Pod и все контейнеры будут разделять общий набор PersistentVolumeClaim и иметь доступ к опубликованным сетевым сервисам через localhost (все контейнеры в Pod разделяют общий ip-адрес). Таким образом можно добавить дополнительный контейнер с restic в развертывание и примонтировать к нему именованный раздел (в соответствии с названием в volumes или volumeClaimTemplates) и использовать его для получением доступа на чтение к данным из хранилища основного контейнера. Такой подход получил название "sideсаг-контейнер" и позволяет расширить функциональность основного контейнера без влияния на его доступность и конфигурацию. Так, мы можем добавить запуск образа контейнера, содержащего restic и подключить раздел с данными дополнительно к нему. Основная проблема состоит в том, что контейнер из образа restic/restic указывает точку входа в консольную утилиту restic и будет непрерывно перезапускаться. Чтобы этого избежать, можно поменять точку входа и отметить, что контейнер должен выполниться в режиме ожидания и мы в дальнейшем сможем подключиться к нему через kubectl ехес и выполнить команду restic backup когда наступит время для создания резервной копии. Также необходимо примонтировать раздел для хранения данных restic из системы хранения к соответствующему контейнеру, а также передать пароль для шифрования резервной копии через переменные окружения контейнера (может быть получен из Secret).
kubectl create secret -n postgres generic restic --from-literal=password=topsecret
postgres.yaml
apiVersion: v1
kind: Namespace
metadata:
name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: postgres
name: postgres
spec:
selector:
matchLabels:
app: postgres
serviceName: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13
ports:
- name: postgres
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_USER
value: postgres
- name: PGUSER
value: postgres
- name: POSTGRES_DB
value: postgres
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
- name: POSTGRES_PASSWORD
value: password
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
livenessProbe:
exec:
command:
- sh
- -c
- exec pg_isready --host $POD_IP
failureThreshold: 6
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
exec:
command:
- sh
- -c
- exec pg_isready --host $POD_IP
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
volumeMounts:
- mountPath: /var/lib/postgresql/data/pgdata
name: postgres
subPath: postgres-db
- name: restic
image: restic/restic
command: ["/bin/sh"]
args: ["-c", "while true; do sleep 360; done"]
env:
- name: RESTIC_PASSWORD
valueFrom:
secretKeyRef:
name: restic
key: password
volumeMounts:
- mountPath: /data
name: postgres
subPath: postgres-db
- mountPath: /backup
name: backup
volumeClaimTemplates:
- metadata:
name: postgres
spec:
storageClassName: "standard"
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi
- metadata:
name: backup
spec:
storageClassName: "standard"
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi
После применения конфигурации можно обнаружить, что теперь Pod запущен с двумя контейнерами:
$ kubectl apply -f postgres.yaml
$ kubectl get pod -n postgres
NAME READY STATUS RESTARTS AGE
postgres-0 2/2 Running 0 11s
И теперь мы можем выполнить инициализацию каталога для управления резервными копиями (однократно) и создать мгновенный снимок данных с использованием sidecar-контейнера restic:
kubectl exec -it -n postgres postgres-0 -c restic -- restic init --password-command "echo $RESTIC_PASSWORD" -r /backup
kubectl exec -it -n postgres postgres-0 -c restic -- restic backup --password-command "echo $RESTIC_PASSWORD" -r /backup /data
Для восстановления можно указать в target непосредственно каталог, куда примонтирован раздел оригинального контейнера (для sidecar ограничений на запись нет, поскольку все контейнеры разделяют единый PVC-раздел):
kubectl exec -it -n postgres postgres-0 -c restic -- restic restore --password-command "echo $RESTIC_PASSWORD" -r /backup latest --target /data
Для создания снимка (как и в первой части статьи) лучше, разумеется, использовать собственные инструменты исходного сервиса (например, pg_dump) и это также может быть предусмотрено в sidecar-контейнере. Образ для выгрузки состояния сервиса и восстановления может быть создан самостоятельно, например для PostgreSQL Dockerfile может выглядеть следующим образом:
FROM restic/restic
RUN apk update && \
apk add postgresql-client && \
echo '#!/bin/sh' >/opt/init.sh && \
echo 'restic init --password-command "echo $RESTIC_PASSWORD" -r /backup' >>/opt/init.sh && \
echo '#!/bin/sh' >/opt/backup.sh && \
echo 'mkdir -p /data' >>/opt/backup.sh && \
echo 'pg_dumpall -Upostgres -h localhost >/data/postgres.dump' >>/opt/backup.sh && \
echo 'restic backup --password-command "echo $RESTIC_PASSWORD" -r /backup /data' >>/opt/backup.sh && \
echo '#!/bin/sh' >/opt/restore.sh && \
echo 'restic restore --echo "echo $RESTIC_PASSWORD" -r /backup --target /' >>/opt/restore.sh && \
echo 'psql -Upostgres -h localhost </data/postgres.dump' >>/opt/restore.sh && \
chmod +x /opt/backup.sh && \
chmod +x /opt/restore.sh && \
chmod +x /opt/init.sh
ENTRYPOINT /bin/sh
Созданный контейнер также может быть добавлен как sidecar к основному процессу PostgreSQL и предусматривает инициализацию репозитория (kubectl exec -it -n postgres postgres-0 -c restic /opt/init.sh), создание резервной копии из дампа базы (kubectl exec -it -n postgres postgres-0 -c restic /opt/backup.sh) и восстановление из последнего доступного дампа из репозитория (kubectl exec -it -n postgres postgres-0 -c restic /opt/restore.sh). Аналогично могут быть созданы контейнеры для других сервисов, либо можно воспользоваться существующими на Docker Hub:
Основной недостаток рассмотренного выше решения - необходимость ручного изменения конфигурации развертывания сервисов, запуска сценариев создания резервной копии и восстановления, подключения дополнительных контейнеров с монтированием PersistentVolumeClaim к соответствующим каталогам, где ожидается получить данные в сценарии резервного копирования. Можно ли как-то автоматизировать этот процесс? Да, и в этом нам поможет Dynamic Admission Control (а именно использование Mutating Webhooks).
Регистрация ресурсов ValidatingWebhooks (admissionregistration.k8s.io/v1/ValidatingWebhookConfiguration)
и MutatingWebhooks (admissionregistration.k8s.io/v1/MutatingWebhookConfiguration)
позволяют добавить дополнительную обработку при создании/изменении/удалению ресурсов (например, Deployment) и добавляют возможности для дополнительных проверок конфигурации (при валидации) или изменения создаваемого ресурса "на лету" (при мутации). Конфигурация мутации определяет адрес API для вызова при возникновении подходящих условий и набор правил (rules), описывающих на какие группы и версии API применяется правило (apiGroups, apiVersions), какие операции отслеживаются (например, CREATE, UPDATE), какие типы ресурсов будут наблюдаться (resources). Часто установка вебхуков выполняется при установке операторов - контейнеров, самостоятельно управляющих ресурсами для развертывания управляемого сервиса и регистрирующих точки подключения к службам, а также дополнительные версии API и типы ресурсов (используется механизмы CRD - Custom Resource Definition). Кроме возможностей встраивания дополнительных действий в процесс развертывания ресурсов такой подход позволяет создавать предметно-специфические типы и метаданные для ресурсов (например, для описания расписания резервного копирования).
Рассмотрим несколько операторов, представляющих возможности для управления резервным копированием:
Velero
Продукт с открытым исходным кодом (ранее назывался Haptio Ark), позволяет выполнять резервное копирование ресурсов кластера Kubernetes и управлять резервными копиями PersistentVolume через утилиту командной строки velero. Для настройки процесса копирования (например, добавления pre- и post-сценариев для подготовки и очистки дампов) используются аннотации в метаданных развертывания. Поддерживаются также возможности CSI Snapshotting.
Для создания резервной копии используется restic и внешние хранилища, которые могут быть подключены через плагины. Создание резервной копии выполняется командой velero backup.
velero backup create postgres --ordered-resources 'statefulsets=postgres/postgres' --include-namespaces=postgres
Также можно запланировать резервное копирование по расписанию:
velero schedule create postgres --schedule="* * * * *"
Расписание задается в синтаксисе Cron (минуты часы день месяц день_недели).
K8Up
Оператор для управления ресурсами резервного копирования. Позволяет работать с PV-разделами с типом доступа ReadWriteMany.
Установка может быть выполнена через helm:
helm repo add appuio https://charts.appuio.ch
helm repo update
helm install k8up appuio/k8up --create-namespace --namespace k8up-operator
После установки регистрируется версия API k8up.io/v1 и дополнительные ресурсы:
Backup - задает конфигурацию репозитория для restic
PreBackupPod - описание sidecar-контейнера и команды для запуска для подготовки дампа системы (также может быть задано в аннотации k8up.io/backupcommand у любого развертывания).
Schedule - определить расписание резервного копирования (в синтаксисе Cron).
Archive - создание холодной копии restic-репозитория для долговременного хранения.
Stash
Оператор использует механизмы Mutating Webhooks для установки sidecar-контейнеров с поддержкой restic в существующие/новые/изменяемые развертывания. Stash позволяет настраивать резервное копирование для PVC-разделов с режимом доступа ReadWriteOnce, поскольку управляющий контейнер добавляется к существующим подам и разделяет с ними общий доступ к хранилищам и ip-адрес.
$ helm repo add appscode https://charts.appscode.com/stable/
$ helm repo update
$ helm install stash appscode/stash \
--version v2022.05.18 \
--namespace stash --create-namespace \
--set features.community=true \
--set-file global.license=/path
Файл opensource лицензии запрашивается на сайте проекта. После установки регистрируется новая версия API stash.appscode.com/v1alpha1 и stash.appscode.com/v1beta1 и дополнительные ресурсы:
Repository - описание расположения репозитория для Restic (может быть один на все развертывания или отдельный под каждое из них), поддерживается S3, Google Cloud Storage, Microsoft Azure Storage, а также REST для отправки в произвольный веб-ресурс.
Snapshot - обертка вокруг возможностей управления снимками Kubernetes.
BackupConfiguration - конфигурация резервного копирования (также может включать в себя hooks для подготовки данных и их очистки после завершения процесса).
BackupBlueprint - создание шаблона для описания резервного копирования, на него можно ссылаться в развертываниях через аннотации:
stash.appscode.com/backup-blueprint - название шаблона;
stash.appscode.com/schedule - расписание резервного копирования.
Function - шаг подготовки резервной копии, может включать в себя создание дампа базы данных или иные операции, необходимые для создания согласованной выгрузки. Функция определяется со ссылкой на образ sidecar-контейнера (для PostgreSQL можно использовать stashed/postgres-stash) и командную строку для запуска действия. Также функция может использоваться для отправки метрик по резервному копированию в Prometheus (образ appscode/stash:0.10.0, команда update-status). При описании аргументов команды можно использовать переменные окружения, предоставленные Stash (в том числе, информация о расположении ресурса).
Task - объединение последовательности функций (перечисляются как step) с параметрами. Task может быть указан для исполнения в описании конфигурации BackupConfiguration или BackupBlueprint.
RestoreSession - выполнение восстановления из указанного репозитория в каталог (обычно связывается с PVC по имени, в соответствии с названием в volumes/persistentVolumeClaims развертывании).
Пример конфигурации для настройки резервного копирования базы данных:
apiVersion: stash.appscode.com/v1beta1
kind: BackupConfiguration
metadata:
name: postgres
namespace: postgres
spec:
driver: Restic
repository:
name: postgres
schedule: "0 * * * *" #каждый час
target:
alias: app-data
ref:
apiVersion: apps/v1 #определение ресурса для резервного копирования
kind: StatefulSet
name: postgres
paths:
- /source/data #каталоги для копирования (обычно сюда монтируется PVC)
exclude:
- /source/data/tmp/* #исключения из копирования
volumeMounts:
- name: data
mountPath: /source/data
hooks:
preBackup:
exec:
command: #сценарий для запуска перед выполнением restic
- /bin/sh
- -c
- echo "Sample PreBackup hook demo"
containerName: my-app-container
postBackup: #сценарий для очистки после выполнения restic
exec:
command:
- /bin/sh
- -c
- echo "Sample PostBackup hook demo"
containerName: my-app-container
retentionPolicy: #политика удаления старых копий (здесь - оставить 5 последних)
name: 'keep-last-5'
keepLast: 5
prune: true
При выполнении резервной копии создается ресурс BackupSession, из которого можно получить информацию о состоянии. При успешном применении можно увидеть, что к развертыванию, которое указано в .spec.target.ref присоединяется sidecar-контейнер stashed/stash с запуском команды run-backup.
По умолчанию доступны 3 функции: update-status (отправка метрик в Prometheus), pvc-backup (сохранение данных из автономного Read-Write-Many PVC), pvc-restore (восстановление в автономный PVC). Для создания своей функции можно реализовать образ контейнера с точкой входа для создания дампа базы данных и отправки в restic-репозиторий, для настройки можно использовать переменные окружения:
REPOSITORY_PROVIDER - провайдер для репозитория (например, S3);
REPOSITORY_BUCKET - название S3 Bucket;
REPOSITORY_ENDPOINT - адрес для подключения к S3 (или другому хранилищу);
REPOSITORY_PREFIX - префикс пути в репозитории;
REPOSITORY_SECRET_NAME - название секрета, содержащего пароль к репозиторию;
REPOSITORY_SECRET_NAMESPACE - пространство имен, где хранится секрет репозитория;
RETENTION_KEEP_LAST - количество резервных копий для сохранения и ротации.
Пример определения функции можно посмотреть здесь. Далее полученная функция интегрируется в задачу:
apiVersion: stash.appscode.com/v1beta1
kind: Task
metadata:
name: postgres-backup
spec:
steps:
- name: postgres-backup
params:
- name: secretVolume
value: secret-volume
- name: update-status
params:
- name: outputDir
value: /tmp/output
- name: secretVolume
value: secret-volume
volumes:
- name: secret-volume
secret:
secretName: ${REPOSITORY_SECRET_NAME}
И далее задача может быть добавлена к конфигурации резервного копирования:
apiVersion: stash.appscode.com/v1beta1
kind: BackupConfiguration
metadata:
name: postgres
namespace: postgres
spec:
driver: Restic
repository:
name: postgres
task:
name: postgres-backup
...
Оператор Stash поддерживает присоединение sidecar-контейнеров к DaemonSet (добавляется к каждому запущенному Pod на всех допустимых узлах), StatefulSet (развертывание пересоздается с подключением дополнительного контейнера для каждой реплики сервиса), Deployment (дополнительный контейнер добавляется к существующему развертыванию без перезапуска).
Итак мы рассмотрели различные подходы к управлению резервным копированием, включая автоматическое управление через CRD-ресурсы в операторах Stash и k8up. Сейчас нет общепринятого решения для управления резервными копиями и появляется большое количество инструментов, основанных на CSI Snapshotting (например, Gemini или TrilioVault), но мы рассмотрели наиболее популярные и функциональные продукты, которые могут помочь вам настроить предсказуемое и надежное резервное копирование и избавить от переживаний о возможной потере данных.
Так;е хочу напомнить о том, что уже 2 июня мои коллеги из OTUS проведут бесплатный урок по теме: "Контроллеры репликации, сеты репликации и балансировка нагрузки". Регистрация на урок доступна по ссылке ниже.