Как в MySQL добавить Node.js приложение, развернуть базу данных в кластере, как правильно инициализировать её и выполнить миграции.
4К открытий4К показов
В прошлый раз мы рассмотрели базовое Node.js-приложение и его деплой в Kubernetes. Теперь мы дополним его, подключив к нему базу данных. В качестве последней будем использовать MySQL, добавив в приложение необходимые для работы с ней компоненты, развернем БД в кластере, а также рассмотрим, как правильно инициализировать базу и выполнить миграции.
Подготовка окружения
Перед работой убедитесь, что у вас установлена актуальная версия werf из стабильной ветки. В случае, если вы еще не устанавливали werf, сделайте это по официальной инструкции.
Инструкции из статьи полностью совместимы и тестировались на операционной системе Linux Ubuntu 20.04.03. Для ОС других семейств, таких как macOS или Windows, могут быть отличия. Подробнее с ними можно ознакомиться в официальной документации к werf.
Также убедитесь, что у на вашем компьютере запущен кластер minikube, в котором подготовлено пространство имён werf-first-app. Если вы уже выполняли инструкции из первой статьи, то достаточно только запустить кластер следующей командой:
minikube start --namespace werf-first-app
А затем установить пространство имён по умолчанию:
Если же вы впервые устанавливаете werf и проходите инструкции, то полное руководство по настройке minikube можно найти в главе «Предварительная настройка окружения» предыдущей статьи.
Подключение базы данных
Доработка приложения
На текущий момент у нас есть stateless-приложение, то есть оно не использует БД и не хранит в ней никаких данных. В реальности такое случается нечасто, поэтому давайте доработаем его до stateful, добавив поддержку MySQL.
Основные изменения, которые мы произведем далее:
Добавим зависимости, необходимые для взаимодействия с MySQL.
Настроим конфигурацию для соединения с БД.
Создадим миграцию и модель схемы БД.
Создадим маршруты в приложении, по которым будет происходить сохранение и чтение данных.
Установим зависимости, требуемые для работы с БД:
npm i sequelize mysql2 express-async-handler sequelize-cli
Здесь мы добавили следующие пакеты:
sequelize — ORM, обеспечивающий работы с БД;
mysql2 — драйвер MySQL;
express-async-handler — расширение контроллеров запросов, позволяющее работать с async/await;
sequelize-cli — утилита, позволяющая генерировать миграции и модели.
Чтобы sequelize мог работать, мы создали для него конфигурационный файл .sequelizerc, в котором задали параметры подключения к БД, а также структуру файлов самого sequelize. Все директории, имеющие отношение в БД, мы определили в каталог db, а конигурацию в файл config/database.json.
Чтобы убедиться, что приложение и правда умеет обращаться к БД, добавим ему пару новых endpoint’ов — /remember и /say — первый будет записывать данные в БД, а другой извлекать и показывать. В каталоге routes добавим файл talkers.js:
В реальных условиях БД можно поднять как внутри кластера Kubernetes, так и за его пределами. При использовании внешней базы доступны различные варианты: от собственной базы, развернутой на своей инстраструктуре, до managed-решений наподобие Amazon RDS.
В качестве примера с нашим приложением мы развернем базу данных MySQL внутри кластера, воспользовавшись типом ресурсов StatefulSet. Для этого создадим файл .helm/templates/database.yaml со следующими параметрами:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: password
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql
ports:
- port: 3306
Теперь настроим наше приложение на подключение к созданной БД. Стоит отметить, что Sequelize ожидает определения подключения для всех окружений в единственном файле — config/database.json. Пропишем в нем нужные параметры:
Развернуть MySQL — это еще половина дела. Далее следует ее правильно инициализировать, проведя нужные миграции и дождавшись ее готовности к работе.
В Kubernetes есть несколько проверенных способов для развертывания и инициализации базы данных. Один из них — использование отдельной Job’ы одновременно с разввертыванием основного приложения. В ней будут выполнять все необходимые для инициализации действия.
Очень важно соблюсти правильный порядок развертывания ресурсов, поэтому мы будем следовать двум правилам:
Job’а должна убедиться, что база доступна, и только затем выполнить миграции;
приложение ожидает перед запуском следующего, что:
БД доступна;,
БД подготовлена и готова к работе;,
миграции выполнены.
При соблюдении этих правил все компоненты создаются одновременно, но функционировать начнут в заданном порядке:
Поднимается БД;
Выполнятся миграции и инициализация с помощью Job’ы.
Стартанут приложения..
Приступим к реализации. Для начала добавим Job (.helm/templates/job-db-setup-and-migrate.yaml), которая будет заниматься инициализацией и миграциями БД:
apiVersion: batch/v1
kind: Job
metadata:
# Версия Helm-релиза в имени Job заставит Job каждый раз пересоздаваться.
# Так мы сможем обойти то, что Job неизменяема.
name: "setup-and-migrate-db-rev{{ .Release.Revision }}"
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: registrysecret
containers:
- name: setup-and-migrate-db
image: {{ .Values.werf.image.backend }}
command:
- sh
- -euc
- |
is_mysql_available() {
tries=$1
i=0
while [ $i -lt $tries ]; do
mysqladmin -h mysql -P 3306 -u root -p=password ping || return 1
i=$((i+1))
sleep 1
done
}
# Дождёмся, когда `mysqladmin ping` отработает 10 раз подряд.
until is_mysql_available 10; do
sleep 1
done
# Выполним первоначальную настройку базы, если она не выполнена, а иначе выполним миграции.
./node_modules/.bin/sequelize-cli db:create
./node_modules/.bin/sequelize-cli db:migrate
env:
- name: NODE_ENV
value: production
Следует пояснить, почему мы ожидаем целых десять проверок БД. Если этого не сделать, то mysqladmin ping выполнится всего один раз перед тем, как StatefusSet с MySQL начнет перезапускаться при деплое. При этом база данные окажется недоступна.
Дополнительно следует обратить внимание, что при первом запуске основной процесс MySQL может перезапуститься несколько раз без перезапуска самого контейнера. Это может привести к ситуации, когда БД ушла в перезапуск, а Job’а уже начала выполнять миграции. Последние при этом само собой будут неудачными, и весь процесс разворачивания закончится ошибкой.
Число 10 здесь выбрано просто как пример, его можно изменять в зависимости от ситуации.
В Deployment’е также необходимо внести изменения, добавив проверку доступности БД перед тем, как стартовать приложение:
┌ Concurrent builds plan (no more than 5 images at the same time)
│ Set #0:
│ - ⛵ image backend
│ - ⛵ image frontend
└ Concurrent builds plan (no more than 5 images at the same time)
┌ ⛵ image backend
│ Use cache image for backend/dockerfile
│ name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app:e37f25c6618bd6e16325671eb0260f19408f26eb0420091b96b7ae12-1645523149535
│ id: 1da0820c6995
│ created: 2022-02-22 12:45:49 +0300 MSK
│ size: 47.9 MiB
└ ⛵ image backend (1.33 seconds)
┌ ⛵ image frontend
│ Use cache image for frontend/dockerfile
│ name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app:45a9a4a848d8821f7db9c0547000414fff924ae5ca8911d62a18d1ea-1645523148164
│ id: 4da6bc98bb74
│ created: 2022-02-22 12:45:47 +0300 MSK
│ size: 9.6 MiB
└ ⛵ image frontend (1.70 seconds)
┌ Waiting for release resources to become ready
│ ┌ job/setup-and-migrate-db-rev14 po/setup-and-migrate-db-rev14--1-jl2h8 container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev14 po/setup-and-migrate-db-rev14--1-jl2h8 container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-first-app 1/1 1 1
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev14 1 2s 0/0
│ │ │ POD READY RESTARTS STATUS ---
│ │ └── and-migrate-db-rev14--1-jl2h8 1/1 0 Running Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
| ...
│
│ ┌ job/setup-and-migrate-db-rev14 po/setup-and-migrate-db-rev14--1-jl2h8 container/setup-and-migrate-db logs
│ │
│ │ Sequelize CLI [Node: 12.22.10, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.json".
│ │ Using environment "production".
│ │ Database werf-first-app created.
│ │
│ │ Sequelize CLI [Node: 12.22.10, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.json".
│ │ Using environment "production".
│ │ No migrations were executed, database schema was already up to date.
│ └ job/setup-and-migrate-db-rev14 po/setup-and-migrate-db-rev14--1-jl2h8 container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-first-app 1/1 1 1
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev14 0 13s 0->1/0
│ │ │ POD READY RESTARTS STATUS
│ │ └── and-migrate-db-rev14--1-jl2h8 0/1 0 Running -> Completed
│ └ Status progress
└ Waiting for release resources to become ready (13.31 seconds)
Release "werf-first-app" has been upgraded. Happy Helming!
NAME: werf-first-app
LAST DEPLOYED: Tue Feb 22 12:58:17 2022
NAMESPACE: werf-first-app
STATUS: deployed
REVISION: 14
TEST SUITE: None
Running time 17.17 seconds
Обратите внимание, что не стоит пугаться сообщений об ошибках, если они появятся. Это говорит лишь о том, что Sequelize не смог подключиться к базе данных, потому что они еще не создана (не прошли миграции). Нужно просто подождать, и все заработает.
Проверим, что все работает как нужно. Попробуем обратиться на /say:
curl http://werf-first-app.test/say
Так как в базе данных пока ничего нет, мы увидим ответ:
Убедимся, что текст, который мы увидели, взят прямо из базы данных. Посмотрим содержимое таблицы talkers напрямую:
kubectl exec -it statefulset/mysql -- mysql -ppassword -e "SELECT * from Talkers" werf-first-app
Мы должны увидеть табличку с нашим текстом:
+----+----------+---------+---------------------+---------------------+
| id | answer | name | createdAt | updatedAt |
+----+----------+---------+---------------------+---------------------+
| 1 | Love you | sweetie | 2022-02-22 10:06:32 | 2022-02-22 10:06:32 |
+----+----------+---------+---------------------+---------------------+
Все работает! Наше приложение стало stateful. Подобный подход к взаимодействию с базами данных аналогичен с любыми другими БД, не только с MySQL.
Заключение
Мы посмотрели на пример развертывания в кластере MySQL, ее инициализации, выполнение миграций, а также настроили наше приложение на подключение и использование базы данных.
В основе этой статьи лежит наш онлайн-самоучитель. В ней рассказано не все, более подробные инструкции или теорию можно найти в полной версии самоучителя, в разделе «Реалистичные приложения».
Надеемся, что эта статья окажется полезной для вас в части приобретения нового опыта в области деплоя приложений в Kubernetes.
Ждем вас в комментариях к статье, а также в Telegram-каналe werf_ru. Мы всегда рады оказать вам любое содействие на этом трудном пути. Также не забывайте про GitHub-репозиторий werf, там всегда будут рады любым issues (и звездам).
Для эффективной работы, и разработчику и тестировщику работающих с базой данных важно иметь возможность исследовать все необходимые компоненты. Рассмотрим как это сделать в MySQL при помощи выражений SHOW
Создатели JavaScript Брендан Айк и Node.js Райан Дал, совместно с более чем 10 000 разработчиков, подписали открытое письмо к Oracle, призывая компанию отказаться от торговой марки JavaScript
Рассмотрели преимущества, недостатки, сходства, различия, возможности карьерного роста для Django и Node.js, чтобы выяснить, какую из них лучше использовать.