В этой статье мы развернём базу данных, реализуем в приложении работу с БД и настроим автоматическое выполнение миграций и инициализации БД.

Приложение в этой статье не предназначено для использования в production без доработки. Готовое к работе в production приложение мы получим в конце руководства.

Подготовка окружения

Если вы ещё не подготовили своё рабочее окружение на предыдущих этапах, сделайте это в соответствии с инструкциями статьи «Подготовка окружения».

Если ваше рабочее окружение работало, но перестало, или же последующие инструкции из этой статьи не работают — попробуйте следующее:

Работает ли 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 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 приложения?

Если вы непреднамеренно удалили 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/java_springboot/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/java_springboot/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/java_springboot/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/java_springboot/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/java_springboot/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/java_springboot/040_db/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show

Подготовка stateful-приложения

Сейчас наше приложение не использует БД и не хранит никаких данных (т.е. stateless). Поэтому, чтобы сделать его stateful, нам в первую очередь нужно подготовить приложение к работе с MySQL, которую мы будем использовать для хранения состояния.

Основные изменения, сделанные в нашем приложении:

  1. Установлены зависимости для работы с MySQL.
  2. Добавлена конфигурация доступа к БД.
  3. Добавлена модель БД.
  4. Добавлены маршруты, использующие новую модель.

Новые зависимости были добавлены в файл конфигурации pom.xml:

...
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
  • spring-boot-starter-data-jpa — стартовый модуль для работы с Hibernate;
  • mysql-connector-java — драйвер для работы с БД MySQL.

Добавление endpoints /remember и /say в приложение

Добавим два новых endpoints, один из которых будет сохранять данные в БД (/remember), а второй — доставать их из БД (/say).

Добавим новую сущность для работы с БД:

package io.werf.werfguidesapp.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;


@Entity
@Table(name = "talkers")
public class Talker {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Integer id;
	
	@JoinColumn(name = "answer")
	private String answer;
	
	@JoinColumn(name = "name")
	private String name;

	public Integer getId() {
		return id;
	}

	public Talker() {

	}

	public Talker(String answer, String name) {
		this.answer = answer;
		this.name = name;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getAnswer() {
		return answer;
	}

	public void setAnswer(String answer) {
		this.answer = answer;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
package io.werf.werfguidesapp.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Table; @Entity @Table(name = "talkers") public class Talker { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @JoinColumn(name = "answer") private String answer; @JoinColumn(name = "name") private String name; public Integer getId() { return id; } public Talker() { } public Talker(String answer, String name) { this.answer = answer; this.name = name; } public void setId(Integer id) { this.id = id; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } public String getName() { return name; } public void setName(String name) { this.name = name; } }

И CRUD репозиторий для работы с ней:

package io.werf.werfguidesapp.repos;

import org.springframework.data.repository.CrudRepository;

import io.werf.werfguidesapp.domain.Talker;

public interface TalkersRepo extends CrudRepository<Talker, Integer> {

}
package io.werf.werfguidesapp.repos; import org.springframework.data.repository.CrudRepository; import io.werf.werfguidesapp.domain.Talker; public interface TalkersRepo extends CrudRepository<Talker, Integer> { }

Добавлять миграции БД не нужно, т.к. ранее мы установили в настройках application.properties свойство spring.jpa.hibernate.ddl-auto=update, при котором Hibernate будет автоматически генерировать структуру БД в процессе работы в соответствии с созданными сущностями.

Добавим новый контроллер:

package io.werf.werfguidesapp.controllers;

import io.werf.werfguidesapp.domain.Talker;
import io.werf.werfguidesapp.repos.TalkersRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class TalkerController {
	
	@Autowired
	private TalkersRepo talkersRepo;

	@GetMapping("/remember")
	public String Remember(@RequestParam String answer, @RequestParam String name) {
		Talker talker = new Talker(answer, name);
		talkersRepo.save(talker);
		return "Got it.\n";
	}
	
	@GetMapping("/say")
	public String Say() {
		Optional<Talker> talker = talkersRepo.findById(1);
		if (talker.isPresent()) {
			return talker.get().getAnswer() + ", " + talker.get().getName() + "\n";
		} else return "I have nothing to say.\n";
	}

}
package io.werf.werfguidesapp.controllers; import io.werf.werfguidesapp.domain.Talker; import io.werf.werfguidesapp.repos.TalkersRepo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Optional; @RestController public class TalkerController { @Autowired private TalkersRepo talkersRepo; @GetMapping("/remember") public String Remember(@RequestParam String answer, @RequestParam String name) { Talker talker = new Talker(answer, name); talkersRepo.save(talker); return "Got it.\n"; } @GetMapping("/say") public String Say() { Optional<Talker> talker = talkersRepo.findById(1); if (talker.isPresent()) { return talker.get().getAnswer() + ", " + talker.get().getName() + "\n"; } else return "I have nothing to say.\n"; } }

Новые 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
        args: ["--mysql-native-password=ON"]
        ports:
        - containerPort: 3306
        env:
          - name: MYSQL_DATABASE
            value: werf-guide-app
          - 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
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 args: ["--mysql-native-password=ON"] ports: - containerPort: 3306 env: - name: MYSQL_DATABASE value: werf-guide-app - 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, все же дальнейшие инструкции остаются без изменений.

Теперь настроим приложение на работу с новой БД:

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.jpa.show-sql: true
spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://${DB_HOST}:3306/${DB_NAME} spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #spring.jpa.show-sql: true
  • spring.jpa.hibernate.ddl-auto=update — вариант работы с БД;
  • spring.datasource.url=jdbc:mysql://${DB_HOST}:3306/${DB_NAME} — адрес сервера БД;
  • spring.datasource.username=${DB_USER} — пользователь БД;
  • spring.datasource.password=${DB_PASSWD} — пароль пользователя;
  • spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver — используемый для подключения драйвер;
  • #spring.jpa.show-sql: true — если раскомментировать этот пункт, в stdout будут показаны выполняемые команды MySQL. Этот пункт бывает полезен при создании миграций БД.

Обратите внимание, что некоторые из параметров заданы через переменные окружения, например ${DB_USER} или ${DB_PASSWD}.

Инициализация и миграции БД

Есть несколько способов выполнять инициализацию и миграции БД при развертывании приложений в Kubernetes. Мы рассмотрим один простой, но хорошо работающий метод.

Модуль Hibernate, встроенный в наше приложение, во время запуска последнего выполняет одно доступных из действий, заданных в параметре spring.jpa.hibernate.ddl-auto файла настроек application.properties. Основными можно выделить четыре варианта работы модуля:

  • validate: валидация БД на соответствие заданным сущностям. Для валидации лучше использовать сторонний валидатор, например, Flyway, подключаемый как зависимость Maven. Такой режим работы полезен, когда приложение уже закончено, и нам нужно только проверить, что БД соответствует нужной нам структуре, либо подготовить чистую БД к работе с приложением;
  • update: динамическое обновление структуры БД во время работы. Используется во время разработки, т.к. автоматически подстраивает БД под изменения сущностей;
  • create: каждый раз перед запуском приложения удаляет все из БД и пересоздает таблицы заново;
  • create-drop: аналогичен предыдущему, но удаляет все из БД после завершения сеанса работы.

Любой из этих методов подразумевает, что нам необходимо перед стартом приложения убедиться, что MySQL запущена и доступна приложению. Поэтому для начала изменим Dockerfile приложения, установив в контейнер с ним mysql-client:

FROM eclipse-temurin:11-alpine AS builder
COPY . /src
WORKDIR /src
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:11-alpine as backend
# Устанавливаем mysql-client.
RUN apk add -U mysql-client
WORKDIR /app
COPY --from=builder /src/target/*.jar ./app.jar
EXPOSE 8080

FROM nginx:stable-alpine as frontend
WORKDIR /www
ADD src/main/resources/static /www/static/
COPY .werf/nginx.conf /etc/nginx/nginx.conf
FROM eclipse-temurin:11-alpine AS builder COPY . /src WORKDIR /src RUN ./mvnw clean package -DskipTests FROM eclipse-temurin:11-alpine as backend # Устанавливаем mysql-client. RUN apk add -U mysql-client WORKDIR /app COPY --from=builder /src/target/*.jar ./app.jar EXPOSE 8080 FROM nginx:stable-alpine as frontend WORKDIR /www ADD src/main/resources/static /www/static/ COPY .werf/nginx.conf /etc/nginx/nginx.conf

Затем внесем изменения в deployment, добавив проверку на доступность БД перед запуском приложения:

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
      containers:
      - name: backend
        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
          # Запускаем приложение
          java -jar ./app.jar

        ports:
        - containerPort: 8080
        env:
        - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB
          value: INFO
        - name: DB_HOST
          value: mysql
        - name: DB_NAME
          value: werf-guide-app
        - name: DB_USER
          value: root
        - name: DB_PASSWD
          value: password
      - name: frontend
        image: {{ .Values.werf.image.frontend }}
        ports:
        - containerPort: 80
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 containers: - name: backend 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 # Запускаем приложение java -jar ./app.jar ports: - containerPort: 8080 env: - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB value: INFO - name: DB_HOST value: mysql - name: DB_NAME value: werf-guide-app - name: DB_USER value: root - name: DB_PASSWD value: password - name: frontend image: {{ .Values.werf.image.frontend }} ports: - containerPort: 80

Также здесь мы передали в переменные окружения необходимые для подключения к БД параметры.

Зачем ждать выполнения 10 подряд успешных проверок доступности БД?

Это предохраняет нас от случая, когда mysqladmin ping выполняется только один раз и до того, как StatefulSet с MySQL начнёт перезапускаться при деплое. В таких случаях во время выполнения инициализации/миграций база данных может оказаться недоступна.

Также в образах с БД при первом запуске главный процесс БД может несколько раз перезапускаться (при этом без перезапуска контейнера). В таком случае, если проверять БД на доступность только один раз, то может оказаться, что после успешной однократной проверки запускаются миграции/инициализация в то же время, когда начинает перезапускаться сам процесс БД. От этого нас тоже страхует выполнение mysqladmin ping несколько раз подряд.

Количество успешных проверок подряд можно изменять — значение 10 приведено как пример.

Проверка работы приложения и БД

Развернём приложение:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app

Ожидаемый результат:

┌ ⛵ image backend
│ Use cache image for backend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:3c1d990b76b29e3e53d4913607207f8d3060e23a94442c7c3c25c01d-1642762275246
│        id: 2cb054a9d1e1
│   created: 2022-01-21 13:51:14 +0000 UTC
│      size: 232.7 MiB
└ ⛵ image backend (4.06 seconds)

┌ ⛵ image frontend
│ Use cache image for frontend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:4d7fe10a14b3d39d37a747bf016e9e8557c5a6d7061cea9d57f2ffd0-1640809566489
│        id: 4325c3e47f17
│   created: 2022-10-29 23:26:06 +0000 UTC
│      size: 9.6 MiB
└ ⛵ image frontend (2.82 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-6c7cbc8b8b-pw7qf                                  0/2                   0                             ContainerCreating                     Waiting for: replicas 2->1                                                                                      
│ │ └── guide-app-7d5d5f74c6-lflsv                                  2/2                   2                             Running                               
│ │ STATEFULSET                                                                                                                                               REPLICAS                     READY                          UP-TO-DATE                                          
│ │ mysql                                                                                                                                                     1/1                          1                              1                                              ↵
│ │      
│ │ │   POD                                                         READY                 RESTARTS                      STATUS                                                                                                                                                
│ │ └── 0                                                           1/1                   0                             Running                               
│ └ Status progress
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                                                                REPLICAS                     AVAILABLE                      UP-TO-DATE                                          
│ │ werf-guide-app                                                                                                                                            2->1/1                       1                              1                                                   
│ │ │   POD                                                         READY                 RESTARTS                      STATUS                                                                                                                                                
│ │ ├── guide-app-6c7cbc8b8b-pw7qf                                  2/2                   0                             ContainerCreating -> Running          
│ │ └── guide-app-7d5d5f74c6-lflsv                                  2/2                   2                             Running -> Terminating                
│ │ STATEFULSET                                                                                                                                               REPLICAS                     READY                          UP-TO-DATE                                          
│ │ mysql                                                                                                                                                     1/1                          1                              1                                              ↵
│ │      
│ │ │   POD                                                         READY                 RESTARTS                      STATUS                                                                                                                                                
│ │ └── 0                                                           1/1                   0                             Running                               
│ └ Status progress
└ Waiting for release resources to become ready (33.78 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Fri Jan 21 13:55:56 2022
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 51
TEST SUITE: None
Running time 41.07 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    |
+----+----------+---------+
|  1 | Love you | sweetie |
+----+----------+---------+

Готово!

Итогом этой статьи стала реализация stateful-приложения, развертывание базы данных вместе с этим приложением, а также автоматическая инициализация БД и выполнение миграций. Подобный подход должен хорошо работать с любыми реляционными БД.

Как и прежде, увидеть все сделанные в этой статье изменения вы можете, выполнив команды, описанные в начале статьи.

назад
далее