- PVSM.RU - https://www.pvsm.ru -
Примечание: эта статья не претендует на статус лучшей практики. В ней описан опыт конкретной реализации инфраструктурной задачи в условиях использования Kubernetes и Helm, который может быть полезен при решении родственных проблем.
Использование review-окружений в CI/CD может быть весьма полезным, причём как для разработчиков, так и для системных инженеров. Давайте для начала синхронизируем общие представления о них:
На пересечении второго и третьего пунктов зачастую и возникают сложности: поскольку инфраструктура бывает очень разной, её компоненты могут деплоиться долгое время. В это затрачиваемое время, например, входит восстановление базы данных из уже подготовленного бэкапа*. Статья — о том, каким увлекательным путем мы однажды отправились для решения такой проблемы.
* Кстати, конкретно о больших дампах БД в этом контексте мы уже писали в материале про ускорение bootstrap’а БД [1].)
В одном из проектов нам поставили задачу «создать единую точку входа для разработчиков и QA-инженеров». За этой формулировкой скрывалось технически следующее:
С точки зрения реализации основная проблема сводится к обеспечению идемпотентности при создании и удалении review-окружений. Чтобы добиться этого, мы изменили механизм создания review-окружений, предварительно перенеся сервисы PostgreSQL, MongoDB и RabbitMQ в статическое окружение. Под статическим понимается такое «постоянное» окружение, которое не будет создаваться по запросу пользователя (как это происходит в случае review-окружений).
Важно! Сам подход со статическим окружением далек от идеального — о его конкретных недостатках см. в завершении статьи. Однако мы в подробностях делимся этим опытом, поскольку он может быть в той или иной степени применим в иных задачах, а заодно послужить аргументом при обсуждении вопросов проектирования инфраструктуры.
Итак, последовательность действий в реализации:
В нашем случае инфраструктура функционирует в рамках Kubernetes (с использованием Helm). Поэтому для реализации вышеописанных задач отлично подошли Helm-хуки [2]. Они могут выполняться как перед созданием всех остальных компонентов в Helm-релизе, так и/или после их удаления. Поэтому:
pre-install
, чтобы запускать его перед созданием всех ресурсов в релизе;post-delete
.Перейдем к деталям реализации.
В изначальном варианте в этом проекте использовался только один Job, состоящий из трех контейнеров. Конечно, это не совсем удобно, поскольку в итоге получается большой манифест, который банально сложно прочитать. Поэтому мы разделили его на три небольших Job’а.
Ниже представлен листинг для PostgreSQL, а два остальных (MongoDB и RabbitMQ) идентичны ему по структуре манифеста:
{{- if .Values.global.review }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: db-create-postgres-database
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-weight": "5"
spec:
template:
metadata:
name: init-db-postgres
spec:
volumes:
- name: postgres-scripts
configMap:
defaultMode: 0755
name: postgresql-configmap
containers:
- name: init-postgres-database
image: private-registry/postgres
command: ["/docker-entrypoint-initdb.d/01-review-load-dump.sh"]
volumeMounts:
- name: postgres-scripts
mountPath: /docker-entrypoint-initdb.d/01-review-load-dump.sh
subPath: review-load-dump.sh
env:
{{- include "postgres_env" . | indent 8 }}
restartPolicy: Never
{{- end }}
Комментарии по содержимому манифеста:
if
с .Values.global.review
в первой строке листинга).hook-weight
[3].
Самое интересное находится в уже упомянутом в листинге shell-скрипте (review-load-dump.sh
). Какие вообще есть варианты восстановления БД в PostgreSQL?
В нашем случае разница между двумя этими подходами заключается в первую очередь в скорости создания базы данных для нового окружения. В первом — мы загружаем дамп базы данных и восстанавливаем ее с помощью pg_restore
. И у нас это происходит медленнее второго способа, поэтому был сделан соответствующий выбор.
С помощью второго варианта (восстановление с шаблонами) можно клонировать базу данных на физическом уровне, не отправляя в нее данные удаленно из контейнера в другом окружении — это уменьшает время восстановления. Однако есть ограничение: нельзя клонировать БД, к которой остаются активные соединения. Так как в качестве статического окружения у нас используется именно stage (а не отдельное окружение для review), требуется сделать вторую базу данных и конвертировать ее в шаблон, ежедневно обновляя (например, по утрам). Для этого был подготовлен небольшой CronJob:
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: update-postgres-template
spec:
schedule: "50 4 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
startingDeadlineSeconds: 600
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: registrysecret
volumes:
- name: postgres-scripts
configMap:
defaultMode: 0755
name: postgresql-configmap-update-cron
containers:
- name: cron
command: ["/docker-entrypoint-initdb.d/update-postgres-template.sh"]
image: private-registry/postgres
volumeMounts:
- name: postgres-scripts
mountPath: /docker-entrypoint-initdb.d/update-postgres-template.sh
subPath: update-postgres-template.sh
env:
{{- include "postgres_env" . | indent 8 }}
Полный манифест с ConfigMap, содержащий скрипт, скорее всего не имеет большого смысла (сообщайте в комментариях, если это не так). Вместо него приведу самое главное — bash-скрипт:
#!/bin/bash -x
CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
psql -d "${CREDENTIALS}" -w -c "REVOKE CONNECT ON DATABASE ${POSTGRES_DB_TEMPLATE} FROM public"
psql -d "${CREDENTIALS}" -w -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB_TEMPLATE}'"
curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql
psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template false allow_connections true;"
psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB_TEMPLATE};" || true
psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB_TEMPLATE};" || true
pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB_TEMPLATE} /tmp/${POSTGRES_DB}.psql
psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template true allow_connections false;"
rm -v /tmp/${POSTGRES_DB}.psql
Восстанавливать можно сразу несколько БД из одного шаблона без каких-либо конфликтов. Главное — чтобы подключения к БД были запрещены, а сама база данных — была шаблоном. Это делается в предпоследнем шаге.
Манифест, содержащий shell-скрипт для восстановления БД, получился таким:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: postgresql-configmap
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
review-load-dump.sh: |
#!/bin/bash -x
CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT CASE WHEN EXISTS (SELECT * FROM pg_stat_activity WHERE datname = '${POSTGRES_DB}' LIMIT 1) THEN 1 ELSE 0 END;" )" = '1' ]
then
echo "Open connections has been found in ${POSTGRES_DB} database, will drop them"
psql -d "${CREDENTIALS}" -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB}' -- AND pid <> pg_backend_pid();"
else
echo "No open connections has been found ${POSTGRES_DB} database, skipping this stage"
fi
psql -d "${CREDENTIALS}" -c "DROP DATABASE ${POSTGRES_DB}"
if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" )" = '1' ]
then
echo "Database ${POSTGRES_DB} still exists, delete review job failed"
exit 1
else
echo "Database ${POSTGRES_DB} does not exist, skipping"
fi
psql ${CREDENTIALS} -d postgres -c 'CREATE DATABASE ${POSTGRES_DB} TEMPLATE "loot-stage-copy"'
Как видно, здесь задействованы hook-delete-policy
. Подробно о применении этих политик написано здесь [5]. В приведенном манифесте мы используем before-hook-creation,hook-succeeded
, которые позволяют выполнить следующие требования: удалять предыдущий объект перед созданием нового хука и удалять только тогда, когда хук был выполнен успешно.
Удаление базы данных опишем в таком ConfigMap'е:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: postgresql-configmap-on-delete
annotations:
"helm.sh/hook": "post-delete, pre-delete"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation
data:
review-delete-db.sh: |
#!/bin/bash -e
CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
psql -d "${CREDENTIALS}" -w postgres -c "DROP DATABASE ${POSTGRES_DB}"
Хотя мы и вынесли в отдельный ConfigMap, его можно поместить в обычный command
в Job. Ведь из него можно сделать однострочник, не усложнив вид самого манифеста.
Если вариант с шаблонами PostgreSQL по какой-то причине не устраивает или не подходит, можно вернуться к упомянутому выше «стандартному» пути восстановления с помощью бэкапа. Алгоритм будет тривиален:
В таком случае скрипт для восстановления станет примерно следующим:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: postgresql-configmap
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
review-load-dump.sh: |
#!/bin/bash -x
CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB}" || true
psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB}"
curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql
psql psql -d "${CREDENTIALS}" -w -c "CREATE EXTENSION ip4r;"
pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB} /tmp/${POSTGRES_DB}.psql
rm -v /tmp/${POSTGRES_DB}.psql
Порядок действий соответствует тому, что уже был описан выше. Единственное изменение — добавлено удаление psql-файла после проведения всех работ.
Примечание: и в скрипте восстановления, и в скрипте удаления каждый раз удаляется база данных. Это сделано для избежания возможных конфликтов во время повторного создания review: необходимо убедиться, что база действительно удалена. Также эту проблему потенциально можно решить добавлением флага --clean
в утилите pg_restore
, однако будьте осторожны: этот флаг очищает данные только тех элементов, которые находятся в самом дампе, поэтому в нашем случае такой вариант не подходит.
В итоге, получился рабочий механизм, который требует дальнейших улучшений (вплоть до замены Bash-скриптов на более изящный код). Их мы оставим за рамками статьи (хотя комментарии по теме, конечно, приветствуются).
Следующий компонент — это MongoDB. Главная сложность с ней заключается в том, что для этой СУБД вариант с копированием базы данных (как в PostgreSQL) существует скорее номинально, потому что:
mongo_restore
. Однако отмечу, что тестирование производилось в рамках одного проекта — в вашем случае результаты могут быть совершенно иными.Получается, что в случае большого объема БД может возникнуть серьезная проблема: мы экономим время на восстановлении базы данных в PgSQL, но при этом очень долго восстанавливаем дамп в Mongo. На момент написания статьи и в рамках имеющейся инфраструктуры мы видели три пути (к слову, их можно совместить):
Если же у вас нет потребности быстро создавать review-окружения, то все описанные сложности можно проигнорировать.
Мы же, не имея возможности копировать БД аналогично PgSQL, пойдем первым путем, т.е. стандартным восстановлением из бэкапа. Алгоритм — такой же, как с PgSQL. В этом легко убедиться, если посмотреть на манифесты:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mongodb-scripts-on-delete
annotations:
"helm.sh/hook": "post-delete, pre-delete"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation
data:
review-delete-db.sh: |
#!/bin/bash -x
mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mongodb-scripts
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
review-load-dump.sh: |
#!/bin/bash -x
curl --fail -vsL ${HOST_FORDEV}/latest_${MONGODB_NAME_STAGE}.gz -o /tmp/${MONGODB_NAME}.gz
mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
mongorestore --gzip --nsFrom "${MONGODB_NAME_STAGE}.*" --nsTo "${MONGODB_NAME}.*" --archive=/tmp/${MONGODB_NAME}.gz --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
Здесь есть важная деталь. В нашем случае MongoDB находится в кластере и нужно быть уверенными, что подключение всегда происходит к узлу Primary. Если указать, например, первый хост в кворуме, то он может через некоторое время перейти из Primary в Secondary, из-за чего не получится создать БД. Поэтому нужно подключаться не к одному хосту, а сразу к ReplicaSet [7], перечисляя все хосты в нем. Уже только по этой причине требуется сделать MongoDB в виде StatefulSet, чтобы названия хостов всегда были одинаковыми (не говоря уже о том, что MongoDB является stateful-приложением по своей природе). В таком варианте вы гарантированно будете подключаться именно к узлу Primary.
Для MongoDB мы тоже удаляем БД перед созданием review — это сделано по тем же причинам, что и в PostgreSQL.
Последний нюанс: так как база данных для review находится в том же окружении, что и stage, требуется отдельное название для клонируемой базы данных. Если дамп не является BSON-файлом, то произойдет следующая ошибка:
the --db and --collection args should only be used when restoring from a BSON file. Other uses are deprecated and will not exist in the future; use --nsInclude instead
Поэтому в примере выше используются --nsFrom
и --nsTo
.
Других проблем с восстановлением мы не встречали. Напоследок, добавлю только, что документация по copyDatabase
в MongoDB доступна здесь [6] — на тот случай, если вы захотите попробовать такой вариант.
Последним приложением в списке наших требований стал RabbitMQ. С ним просто: нужно создавать новый vhost от имени пользователя, которым будет подключаться приложение. А затем его удалять.
Манифест для создания и удаления vhost’ов:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rabbitmq-configmap
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
rabbitmq-setup-vhost.sh: |
#!/bin/bash -x
/usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} declare vhost name=${RABBITMQ_VHOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rabbitmq-configmap-on-delete
annotations:
"helm.sh/hook": "post-delete, pre-delete"
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation
data:
rabbitmq-delete-vhost.sh: |
#!/bin/bash -x
/usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} delete vhost name=${RABBITMQ_VHOST}
С большими сложностями в RabbitMQ мы (пока?) не столкнулись. В целом, этот же подход может распространяться и на любые другие сервисы, в которых нет критичной завязки на данные.
Почему это решение не претендует на «лучшие практики»?
Решить две эти проблемы с учетом возможностей инфраструктуры конкретного проекта полноценно не получилось, однако минимизировать потенциальный ущерб можно кластеризацией (добавлением новых узлов) и вертикальным масштабированием.
По мере развития приложения и с увеличением количества разработчиков, рано или поздно повышается нагрузка на review-окружения и добавляются новые требования к ним. Разработчикам важно как можно быстрее доставлять очередные изменения в production, но чтобы это стало возможным, нужны динамические review-окружения, которые делают разработку «параллельной». Как следствие, растет и нагрузка на инфраструктуру, и увеличивается время создания таких окружений.
Эта статья была написана на основе реального и довольно специфичного опыта. Только в исключительных случаях мы выделяем какие-либо службы в статические окружения, и здесь речь шла именно о нем. Такая вынужденная мера позволила ускорить разработку и отладку приложения — благодаря возможности быстрого создания review-окружений с нуля.
Когда мы начинали делать эту задачу, она казалась очень простой, но по мере работы над ней обнаружили множество нюансов. Именно их и собрали в итоговой статье: пусть они не универсальны, но могут послужить примером для основы / вдохновения собственных решений по ускорению review-окружений.
Читайте также в нашем блоге:
Автор: Ilya Andreev
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/postgresql/353023
Ссылки в тексте:
[1] материале про ускорение bootstrap’а БД: https://habr.com/ru/company/flant/blog/417509/
[2] Helm-хуки: https://helm.sh/docs/topics/charts_hooks/
[3] hook-weight
: https://helm.sh/docs/topics/charts_hooks/#writing-a-hook
[4] шаблонов: https://www.postgresql.org/docs/9.3/manage-ag-templatedbs.html
[5] здесь: https://helm.sh/docs/topics/charts_hooks/#hook-deletion-policies
[6] состоянии deprecated: https://docs.mongodb.com/manual/reference/method/db.copyDatabase/
[7] ReplicaSet: https://docs.mongodb.com/manual/reference/replica-configuration/
[8] Беспростойная миграция RabbitMQ в Kubernetes: https://habr.com/ru/company/flant/blog/450662/
[9] Беспростойная миграция MongoDB в Kubernetes: https://habr.com/ru/company/flant/blog/461149/
[10] Запуск команд в процессе доставки нового релиза приложения в Kubernetes: https://habr.com/ru/company/flant/blog/476320/
[11] Источник: https://habr.com/ru/post/501424/?utm_source=habrahabr&utm_medium=rss&utm_campaign=501424
Нажмите здесь для печати.