В этой статье мы развернём базу данных, реализуем в приложении работу с БД и настроим автоматическое выполнение миграций и инициализации БД.
Приложение в этой статье не предназначено для использования в production без доработки. Готовое к работе в production приложение мы получим в конце руководства.
Подготовка окружения
Если вы ещё не подготовили своё рабочее окружение на предыдущих этапах, сделайте это в соответствии с инструкциями статьи «Подготовка окружения».
Если ваше рабочее окружение работало, но перестало, или же последующие инструкции из этой статьи не работают — попробуйте следующее:
Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:
docker run hello-world
Результат успешного выполнения команды:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
При возникновении проблем обратитесь к документации Docker для их устранения.
Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:
docker run hello-world
Результат успешного выполнения команды:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
При возникновении проблем обратитесь к документации Docker для их устранения.
Запустим Docker:
sudo systemctl restart docker
Убедимся, что Docker запустился:
sudo systemctl status docker
Результат выполнения команды, если Docker успешно запущен:
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 2013888 (dockerd)
Tasks: 36
Memory: 100.3M
CGroup: /system.slice/docker.service
└─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"
Теперь проверим, что Docker доступен и корректно настроен:
docker run hello-world
Результат успешного выполнения команды:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
При возникновении проблем обратитесь к документации Docker для их устранения.
Запустим кластер minikube, уже настроенный в начале статьи “Подготовка окружения”:
minikube start
Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl
:
kubectl config set-context minikube --namespace=werf-guide-app
Результат успешного выполнения команды:
😄 minikube v1.20.0 on Ubuntu 20.04
✨ Using the docker driver based on existing profile
👍 Starting control plane node minikube in cluster minikube
🚜 Pulling base image ...
🎉 minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡 To disable this notice, run: 'minikube config set WantUpdateNotification false'
🔄 Restarting existing docker container for "minikube" ...
🐳 Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎 Verifying Kubernetes components...
▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
▪ Using image registry:2.7.1
▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎 Verifying registry addon...
🔎 Verifying ingress addon...
🌟 Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄 Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default
Убедитесь, что вывод команды содержит строку:
Restarting existing docker container for "minikube"
Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.
Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:
minikube tunnel --cleanup=true
Запустим кластер minikube, уже настроенный в начале статьи “Подготовка окружения”:
minikube start --namespace werf-guide-app
Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl
:
kubectl config set-context minikube --namespace=werf-guide-app
Результат успешного выполнения команды:
😄 minikube v1.20.0 on Ubuntu 20.04
✨ Using the docker driver based on existing profile
👍 Starting control plane node minikube in cluster minikube
🚜 Pulling base image ...
🎉 minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡 To disable this notice, run: 'minikube config set WantUpdateNotification false'
🔄 Restarting existing docker container for "minikube" ...
🐳 Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎 Verifying Kubernetes components...
▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
▪ Using image registry:2.7.1
▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎 Verifying registry addon...
🔎 Verifying ingress addon...
🌟 Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄 Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default
Убедитесь, что вывод команды содержит строку:
Restarting existing docker container for "minikube"
Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.
Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:
kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
--docker-server='https://index.docker.io/v1/' \
--docker-username='<ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>' \
--docker-password='<ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>'
Результат успешного выполнения команды:
namespace/werf-guide-app created
secret/registrysecret created
Если ничего не помогло, то пройдите статью «Подготовка окружения» с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.
Подготовка репозитория
Обновим существующий репозиторий с приложением:
Выполним следующий набор команд в PowerShell:
cd ~/werf-guide/app
# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/040_db/* .
git add .
git commit -m WIP
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show
Выполним следующий набор команд в Bash:
cd ~/werf-guide/app
# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/040_db/. .
git add .
git commit -m WIP
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show
Не работает? Попробуйте инструкции на вкладке «Начинаю проходить руководство с этой статьи» выше.
Подготовим новый репозиторий с приложением:
Выполним следующий набор команд в PowerShell:
# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован.
if (-not (Test-Path ~/werf-guide/guides)) {
git clone https://github.com/werf/website $env:HOMEPATH/werf-guide/guides
}
# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app.
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/030_assets ~/werf-guide/app
# Сделаем из директории ~/werf-guide/app git-репозиторий.
cd ~/werf-guide/app
git init
git add .
git commit -m initial
# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/040_db/* .
git add .
git commit -m WIP
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show
Выполним следующий набор команд в Bash:
# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован.
test -e ~/werf-guide/guides || git clone https://github.com/werf/website ~/werf-guide/guides
# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app.
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/examples/nodejs/030_assets ~/werf-guide/app
# Сделаем из директории ~/werf-guide/app git-репозиторий.
cd ~/werf-guide/app
git init
git add .
git commit -m initial
# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/040_db/. .
git add .
git commit -m WIP
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show
Подготовка stateful-приложения
Сейчас наше приложение не использует БД и не хранит никаких данных (т.е. stateless). Поэтому, чтобы сделать его stateful, нам в первую очередь нужно добавить библиотеки для работы с БД и подготовить для этого приложение.
Основные изменения, сделанные в нашем приложении:
- Установлены зависимости для работы с MySQL.
- Добавлена конфигурация доступа к БД.
- Добавлена модель и миграция схемы БД.
- Добавлены маршруты, использующие новую модель.
Новые зависимости были установлены командой:
npm i mysql2 sequelize sequelize-cli express-async-handler
mysql2
— драйвер работы с MySQL;sequelize
— ORM для работы с базой данных;sequelize-cli
— командная утилита для генерации моделей и миграции;express-async-handler
— обертка над обработчиками запросов, чтобы писать код с async/await.
Для sequelize мы добавили конфигурационный файл, который определяет структуру файлов sequelize и
конфигурацию подключения к базе. Мы определим все связанные с БД каталоги в каталог db
, а
конфигурацию положим в config/database.json
:
const path = require('path');
module.exports = {
config: path.resolve('config', 'database.json'),
'models-path': path.resolve('db', 'models'),
'seeders-path': path.resolve('db', 'seeders'),
'migrations-path': path.resolve('db', 'migrations'),
};
Добавление endpoints /remember
и /say
в приложение
Добавим два новых endpoints, один из которых будет сохранять данные в БД (/remember
), а второй — доставать их из БД (/say
).
Наш новый контроллер и модель:
//@ts-check
const express = require('express');
const router = express.Router();
const asyncHandler = require('express-async-handler');
module.exports = (db) => {
router.get(
'/say',
asyncHandler(async (req, res) => {
try {
const talker = await db.sequelize.transaction(async (t) => {
return await db.Talker.findOne({ where: { id: 1 }, transaction: t });
});
if (!talker) {
res.send(`I have nothing to say.\n`);
return;
}
res.send(`${talker.answer}, ${talker.name}!\n`);
} catch (e) {
res.status(500).send(`Something went wrong: ${e.message}\n`);
}
})
);
router.get(
'/remember',
asyncHandler(async (req, res) => {
const { answer, name } = req.query;
if (!answer) {
res.status(422).send('You forgot the answer :(\n');
return;
}
if (!name) {
res.status(422).send('You forgot the name :(\n');
return;
}
try {
await db.sequelize.transaction(async (t) => {
const [talker] = await db.Talker.findOrCreate({
where: { id: 1 },
transaction: t,
});
talker.set({ answer, name });
await talker.save({ transaction: t });
});
res.send(`Got it.\n`);
} catch (e) {
res.status(500).send(`Something went wrong: ${e.message}\n`);
}
})
);
return router;
};
Модель и миграция были сгенерированы командой:
npx sequelize-cli model:generate --name Talker --attributes answer:string,name:string
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Talker extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// Define association here.
}
}
Talker.init(
{
answer: DataTypes.STRING,
name: DataTypes.STRING,
},
{
sequelize,
modelName: 'Talker',
}
);
return Talker;
};
Добавим новые пути в список маршрутов:
...
const talkersRouter = require('./routes/talkers');
...
app.use('/', talkersRouter(db));
Миграция создалась автоматически вместе с моделью:
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Talkers', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
answer: {
type: Sequelize.STRING,
},
name: {
type: Sequelize.STRING,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Talkers');
},
};
Новые endpoints — /remember
и /say
— готовы к работе.
Развертывание и подключение MySQL
В реальной инфраструктуре базы данных могут быть развернуты как в Kubernetes, так и вне его. Вне Kubernetes базы данных могут развертываться и обслуживаться самостоятельно, либо могут использоваться managed-решения вроде Amazon RDS. Для нашего приложения, в целях демонстрации, мы развернём БД MySQL в Kubernetes с помощью простого StatefulSet:
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:9.1
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
Вы также можете использовать базы данных, развернутые любым другим способом. В таком случае вам не нужно развертывать вышеупомянутый StatefulSet, все же дальнейшие инструкции остаются без изменений.
Теперь настроим приложение на работу с новой БД:
{
"development": {
"username": "root",
"password": "null",
"database": "werf-guide-app",
"host": "mysql",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "werf-guide-app",
"host": "mysql",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": "password",
"database": "werf-guide-app",
"host": "mysql",
"dialect": "mysql"
}
}
Sequelize предполагает, что подключения для всех окружений будут определены в одном файле. Пока что мы оставим данные для production-окружения хардкодом, но в будущих главах это будет исправлено.
Инициализация и миграции БД
Есть несколько способов выполнять инициализацию и миграции БД при развертывании приложений в Kubernetes. Мы рассмотрим один простой, но хорошо работающий метод. В нем миграции БД (и, если требуется, инициализация) будут выполняться отдельной Job одновременно с развертыванием приложения и самой БД.
Чтобы выдержать очередность развертывания ресурсов, мы:
- Требуем от Job, которая выполнит инициализацию/миграции БД, дождаться доступности базы данных перед началом работы.
- Требуем от приложений перед тем, как запуститься, дождаться доступности базы данных и подготовки базы данных и выполнения миграций.
Таким образом, при деплое все K8s-ресурсы будут созданы одновременно, но начнут работу в следующем порядке:
- Запустится БД.
- Затем выполнится Job с инициализацией/миграциями БД.
- Затем запустятся приложения.
Реализуем это, добавив Job для выполнения миграций/инициализации базы данных:
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
выполняется только один раз и до того, как StatefulSet с MySQL начнёт перезапускаться при деплое. В таких случаях во время выполнения инициализации/миграций база данных может оказаться недоступна.
Также в образах с БД при первом запуске главный процесс БД может несколько раз перезапускаться (при этом без перезапуска контейнера). В таком случае, если проверять БД на доступность только один раз, то может оказаться, что после успешной однократной проверки запускаются миграции/инициализация в то же время, когда начинает перезапускаться сам процесс БД. От этого нас тоже страхует выполнение mysqladmin ping
несколько раз подряд.
Количество успешных проверок подряд можно изменять — значение 10 приведено как пример.
apiVersion: apps/v1
kind: Deployment
metadata:
name: werf-guide-app
spec:
replicas: 1
selector:
matchLabels:
app: werf-guide-app
template:
metadata:
labels:
app: werf-guide-app
spec:
imagePullSecrets:
- name: registrysecret
initContainers:
- name: wait-db-readiness
image: {{ .Values.werf.image.backend }}
command:
- sh
- -euc
- |
# Дожидаемся доступности БД и выполнения миграций.
until ./node_modules/.bin/sequelize-cli db:migrate:status; do
sleep 1
done
env:
- name: NODE_ENV
value: production
containers:
- name: backend
image: {{ .Values.werf.image.backend }}
command: ["node", "./bin/www"]
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: frontend
image: {{ .Values.werf.image.frontend }}
ports:
- containerPort: 80
Проверка работы приложения и БД
Развернём приложение:
werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
Ожидаемый результат:
┌ ⛵ image backend
│ ┌ Building stage backend/dockerfile
│ │ backend/dockerfile Sending build context to Docker daemon 315.9kB
│ │ backend/dockerfile Step 1/26 : FROM node:12-alpine as builder
...
│ │ backend/dockerfile Successfully built 968f3f3edddd
│ │ backend/dockerfile Successfully tagged e2aff96b-4d59-4758-a10a-d186f7b6d566:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (15.06 seconds)
│ ├ Info
│ │ name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:602fe44580c949e018eadd58d8f43d155fff39068a513d27482ddb85-1637079271243
│ │ id: 968f3f3edddd
│ │ created: 2022-11-16 19:14:31 +0000 UTC
│ │ size: 46.7 MiB
│ └ Building stage backend/dockerfile (21.05 seconds)
└ ⛵ image backend (29.25 seconds)
┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile Sending build context to Docker daemon 315.9kB
│ │ frontend/dockerfile Step 1/30 : FROM node:12-alpine as builder
...
│ │ frontend/dockerfile Successfully built 8283025c5383
│ │ frontend/dockerfile Successfully tagged 9dd1b277-83e3-4f2e-a84d-72bb41166670:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (11.91 seconds)
│ ├ Info
│ │ name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:ef127812b5a9205c59428381e5d09bad041058e6366ae9d254a59df8-1637079271091
│ │ id: 8283025c5383
│ │ created: 2022-11-16 19:14:30 +0000 UTC
│ │ size: 9.4 MiB
│ └ Building stage frontend/dockerfile (17.94 seconds)
└ ⛵ image frontend (25.53 seconds)
┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-guide-app 2/1 1 1
│ │ │ POD READY RESTARTS STATUS ---
│ │ ├── guide-app-54784dd77-kkhhb 0/2 0 Init:0/1 Waiting for: replicas 2->1 ↵
│ │
│ │ └── guide-app-84fbcf787f-69zmm 2/2 0 Running
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev17 1 0s 0/0 ↵
│ │
│ │ │ POD READY RESTARTS STATUS ---
│ │ └── and-migrate-db-rev17--1-bbsp 0/1 0 ContainerCreating Waiting for: pods should be terminated, succeeded 0->1
│ │ 2
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 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)'
│ └ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 container/setup-and-migrate-db logs
│
│ ┌ deploy/werf-guide-app po/werf-guide-app-54784dd77-kkhhb container/wait-db-readiness logs
│ │
│ │ Sequelize CLI [Node: 12.22.7, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.json".
│ │ Using environment "production".
│ │ up 20211101064002-create-talker.js
│ └ deploy/werf-guide-app po/werf-guide-app-54784dd77-kkhhb container/wait-db-readiness logs
│
│ ┌ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 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)'
│ └ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-guide-app 2/1 1 1
│ │ │ POD READY RESTARTS STATUS ---
│ │ ├── guide-app-54784dd77-kkhhb 0/2 0 Init:0/1 -> Waiting for: replicas 2->1 ↵
│ │
│ │ │ PodInitializing
│ │ └── guide-app-84fbcf787f-69zmm 2/2 0 Running
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev17 1 6s 0/0 ↵
│ │
│ │ │ POD READY RESTARTS STATUS ---
│ │ └── and-migrate-db-rev17--1-bbsp 1/1 0 ContainerCreating Waiting for: pods should be terminated, succeeded 0->1
│ │ 2 -> Running
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 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)'
│ └ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-guide-app 2->1/1 1 1
│ │ │ POD READY RESTARTS STATUS
│ │ ├── guide-app-54784dd77-kkhhb 2/2 0 PodInitializing ->
│ │ │ Running
│ │ └── guide-app-84fbcf787f-69zmm 2/2 0 Running ->
│ │ Terminating
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev17 1 6s 0/0 ↵
│ │
│ │ │ POD READY RESTARTS STATUS ---
│ │ └── and-migrate-db-rev17--1-bbsp 1/1 0 Running Waiting for: pods should be terminated, succeeded 0->1
│ │ 2
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 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)'
│ │
│ │ Sequelize CLI [Node: 12.22.7, 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-rev17 po/setup-and-migrate-db-rev17--1-bbsp2 container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE
│ │ werf-guide-app 1/1 1 1
│ │ │ POD READY RESTARTS STATUS
│ │ ├── guide-app-54784dd77-kkhhb 2/2 0 Running
│ │ └── guide-app-84fbcf787f-69zmm 2/2 0 Terminating
│ │ STATEFULSET REPLICAS READY UP-TO-DATE
│ │ mysql 1/1 1 1
│ │ JOB ACTIVE DURATION SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev17 0 17s 0->1/0 ↵
│ │
│ │ │ POD READY RESTARTS STATUS
│ │ └── and-migrate-db-rev17--1-bbsp 0/1 0 Running ->
│ │ 2 Completed
│ └ Status progress
└ Waiting for release resources to become ready (17.54 seconds)
Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Tue Nov 16 19:14:53 2022
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 17
TEST SUITE: None
Running time 54.20 seconds
Если кажется, что процесс завис, а в сообщениях сплошные ошибки — все нормально, просто идет проверка на состояние MySQL, и нужно немного подождать (обычно не более 1-2 минут).
Попробуем обратиться на /say
, который должен попытаться достать данные из БД:
curl http://werf-guide-app.test/say
Но так как в базе данных пока пусто, должно вернуться следующее:
I have nothing to say.
Тогда сохраним данные в БД через /remember
:
curl "http://werf-guide-app.test/remember?answer=Love+you&name=sweetie"
Ожидаемый результат, означающий, что данные сохранены:
Got it.
Снова попробуем получить данные из БД через /say
:
curl http://werf-guide-app.test/say
Ожидаемый успешный результат:
Love you, sweetie!
Также мы можем убедиться, что данные в базе действительно сохранены, запросив напрямую из БД содержимое таблицы:
kubectl exec -it statefulset/mysql -- mysql -ppassword -e "SELECT * from Talkers" werf-guide-app
Ожидаемый результат:
+----+----------+---------+---------------------+---------------------+
| id | answer | name | createdAt | updatedAt |
+----+----------+---------+---------------------+---------------------+
| 1 | Love you | sweetie | 2021-11-11 16:28:44 | 2021-11-11 16:28:44 |
+----+----------+---------+---------------------+---------------------+
Готово!
Итогом этой статьи стала реализация stateful-приложения, развертывание базы данных вместе с этим приложением, а также автоматическая инициализация БД и выполнение миграций. Подобный подход должен хорошо работать с любыми реляционными БД.
Как и прежде, увидеть все сделанные в этой статье изменения вы можете, выполнив команды, описанные в начале статьи.