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

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

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

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

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

  1. Добавлены зависимости для работы с MySQL.
  2. Добавлена конфигурация доступа к БД через переменные окружения.
  3. Добавлены маршруты для работы с БД.

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

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

Для начала создадим вспомогательный сервис, который будет доставать из переменных окружения параметры подключения к БД и возвращать их:

package services

import "os"

func GetDBCredentials() (string, string) {
	dbType := os.Getenv("DB_TYPE")
	dbName := os.Getenv("DB_NAME")
	dbUser := os.Getenv("DB_USER")
	dbPasswd := os.Getenv("DB_PASSWD")
	dbHost := os.Getenv("DB_HOST")
	dbPort := os.Getenv("DB_PORT")
	return dbType, dbUser + ":" + dbPasswd + "@tcp(" + dbHost + ":" + dbPort + ")/" + dbName
}
package services import "os" func GetDBCredentials() (string, string) { dbType := os.Getenv("DB_TYPE") dbName := os.Getenv("DB_NAME") dbUser := os.Getenv("DB_USER") dbPasswd := os.Getenv("DB_PASSWD") dbHost := os.Getenv("DB_HOST") dbPort := os.Getenv("DB_PORT") return dbType, dbUser + ":" + dbPasswd + "@tcp(" + dbHost + ":" + dbPort + ")/" + dbName }

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

Создадим два контроллера, отвечающих за новые endpoints:

package controllers

import (
	"database/sql"
	"github.com/gin-gonic/gin"
	"net/http"
	"werf_guide_app/internal/services"

	_ "github.com/go-sql-driver/mysql"
)

func RememberController(c *gin.Context) {
	dbType, dbPath := services.GetDBCredentials()

	db, err := sql.Open(dbType, dbPath)
	if err != nil {
		panic(err)
	}

	answer := c.Query("answer")
	name := c.Query("name")
	_, err = db.Exec("INSERT INTO talkers (answer, name) VALUES (?, ?)",
		answer, name)
	if err != nil {
		panic(err)
	}

	c.String(http.StatusOK, "Got it.\n")

	defer db.Close()
}

func SayController(c *gin.Context) {
	dbType, dbPath := services.GetDBCredentials()

	db, err := sql.Open(dbType, dbPath)
	if err != nil {
		panic(err)
	}

	result, err := db.Query("SELECT * FROM talkers")
	if err != nil {
		panic(err)
	}

	count := 0
	for result.Next() {
		count++
		var id int
		var answer string
		var name string

		err = result.Scan(&id, &answer, &name)
		if err != nil {
			panic(err)
		}

		c.String(http.StatusOK, answer+", "+name+"!\n")
		break
	}
	if count == 0 {
		c.String(http.StatusOK, "I have nothing to say.\n")
	}
}
package controllers import ( "database/sql" "github.com/gin-gonic/gin" "net/http" "werf_guide_app/internal/services" _ "github.com/go-sql-driver/mysql" ) func RememberController(c *gin.Context) { dbType, dbPath := services.GetDBCredentials() db, err := sql.Open(dbType, dbPath) if err != nil { panic(err) } answer := c.Query("answer") name := c.Query("name") _, err = db.Exec("INSERT INTO talkers (answer, name) VALUES (?, ?)", answer, name) if err != nil { panic(err) } c.String(http.StatusOK, "Got it.\n") defer db.Close() } func SayController(c *gin.Context) { dbType, dbPath := services.GetDBCredentials() db, err := sql.Open(dbType, dbPath) if err != nil { panic(err) } result, err := db.Query("SELECT * FROM talkers") if err != nil { panic(err) } count := 0 for result.Next() { count++ var id int var answer string var name string err = result.Scan(&id, &answer, &name) if err != nil { panic(err) } c.String(http.StatusOK, answer+", "+name+"!\n") break } if count == 0 { c.String(http.StatusOK, "I have nothing to say.\n") } }

Добавим новые маршруты и подключим к ним созданные контроллеры:

...
route.GET("/remember", controllers.RememberController)
route.GET("/say", controllers.SayController)
... route.GET("/remember", controllers.RememberController) route.GET("/say", controllers.SayController)

Новые 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, все же дальнейшие инструкции остаются без изменений.

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

...
- name: DB_TYPE
  value: "mysql"
- name: DB_NAME
  value: "werf-guide-app"
- name: DB_USER
  value: "root"
- name: DB_PASSWD
  value: "password"
- name: DB_HOST
  value: "mysql"
- name: DB_PORT
  value: "3306"
... - name: DB_TYPE value: "mysql" - name: DB_NAME value: "werf-guide-app" - name: DB_USER value: "root" - name: DB_PASSWD value: "password" - name: DB_HOST value: "mysql" - name: DB_PORT value: "3306"

Все параметры заданы через переменные окружения.

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

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

  1. Требуем от Job, которая выполнит инициализацию/миграции БД, дождаться доступности базы данных перед началом работы.
  2. Требуем от приложений перед тем, как запуститься, дождаться доступности базы данных и подготовки базы данных и выполнения миграций.

Таким образом, при деплое все K8s-ресурсы будут созданы одновременно, но начнут работу в следующем порядке:

  1. Запустится БД.
  2. Затем выполнится Job с инициализацией/миграциями БД.
  3. Затем запустятся приложения.

Для миграции БД мы воспользуемся утилитой migrate. Она позволяет работать с миграциями как из командной строки (CLI), так и напрямую из кода на Go. Мы воспользуемся первым вариантом.

Чтобы сгенерировать файлы миграций, выполним команду в корне проекта:

migrate create -ext sql -dir db/migrations -seq create_talkers_table

В каталоге db/migrations будут созданы два файла:

db
└── migrations
    ├── 000001_create_talkers_table.down.sql
    └── 000001_create_talkers_table.up.sql

Файл с суффиксом up содержит миграции для инициализации БД:

CREATE TABLE talkers (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    answer TEXT NOT NULL,
    name TEXT NOT NULL
);
CREATE TABLE talkers ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, answer TEXT NOT NULL, name TEXT NOT NULL );

Файл с суффиксом down — инструкции для очистки БД:

DROP TABLE IF EXISTS talkers;
DROP TABLE IF EXISTS talkers;

Изменим Dockerfile, установив в конечный образ backend утилиту migrate:

# Используем многоступенчатую сборку образа (multi-stage build)
# Образ, в котором будет собираться проект
FROM golang:1.18-alpine AS build
# Устанавливаем curl и tar.
RUN apk add curl tar
# Копируем исходники приложения
COPY . /app
WORKDIR /app
# Скачиваем утилиту migrate и распаковываем полученный архив.
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
# Запускаем загрузку нужных пакетов.
RUN go mod download
# Запускаем сборку приложения.
RUN go build -o /goapp cmd/main.go

# Образ, который будет разворачиваться в кластере.
FROM alpine:latest as backend
WORKDIR /
# Копируем из сборочного образа исполняемый файл проекта.
COPY --from=build /goapp /goapp
# Копируем из сборочного образа распакованный файл утилиты migrate и схемы миграции.
COPY --from=build /app/migrate /migrations/migrate
COPY db/migrations /migrations/schemes
# Копируем файлы ассетов и шаблоны.
COPY ./templates /templates
COPY ./static /static
EXPOSE 8080
ENTRYPOINT ["/goapp"]

# NGINX-образ со статическими файлами.
FROM nginx:stable-alpine as frontend
WORKDIR /www
# Копируем статические файлы.
COPY static /www/static/
# Копируем конфигурацию NGINX.
COPY .werf/nginx.conf /etc/nginx/nginx.conf
# Используем многоступенчатую сборку образа (multi-stage build) # Образ, в котором будет собираться проект FROM golang:1.18-alpine AS build # Устанавливаем curl и tar. RUN apk add curl tar # Копируем исходники приложения COPY . /app WORKDIR /app # Скачиваем утилиту migrate и распаковываем полученный архив. RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz # Запускаем загрузку нужных пакетов. RUN go mod download # Запускаем сборку приложения. RUN go build -o /goapp cmd/main.go # Образ, который будет разворачиваться в кластере. FROM alpine:latest as backend WORKDIR / # Копируем из сборочного образа исполняемый файл проекта. COPY --from=build /goapp /goapp # Копируем из сборочного образа распакованный файл утилиты migrate и схемы миграции. COPY --from=build /app/migrate /migrations/migrate COPY db/migrations /migrations/schemes # Копируем файлы ассетов и шаблоны. COPY ./templates /templates COPY ./static /static EXPOSE 8080 ENTRYPOINT ["/goapp"] # NGINX-образ со статическими файлами. FROM nginx:stable-alpine as frontend WORKDIR /www # Копируем статические файлы. COPY static /www/static/ # Копируем конфигурацию NGINX. COPY .werf/nginx.conf /etc/nginx/nginx.conf

Реализуем это, добавив 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
      initContainers:
      - name: waiting-mysql
        image: alpine:3.6
        command: [ '/bin/sh', '-c', 'while ! nc -z mysql 3306; do sleep 1; done' ]
      containers:
      - name: setup-and-migrate-db
        image: {{ .Values.werf.image.backend }}
        command: ["/migrations/migrate",  "-database", "mysql://root:password@tcp(mysql:3306)/werf-guide-app", "-path", "/migrations/schemes", "up"]
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 initContainers: - name: waiting-mysql image: alpine:3.6 command: [ '/bin/sh', '-c', 'while ! nc -z mysql 3306; do sleep 1; done' ] containers: - name: setup-and-migrate-db image: {{ .Values.werf.image.backend }} command: ["/migrations/migrate", "-database", "mysql://root:password@tcp(mysql:3306)/werf-guide-app", "-path", "/migrations/schemes", "up"]

Здесь используются два контейнера:

  • initContainer будет запущен первым, и в нем будет в цикле пинговаться сервер MySQL до получения ответа.
  • В основном контейнере (backend из Dockerfile) запустится утилита migrate и произведет миграции БД.

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

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

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

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

┌ Concurrent build plan (no more than 5 images at the same time)
│ Set #0:
│ - ⛵ image backend
│ - ⛵ image frontend
└ Concurrent build plan (no more than 5 images at the same time)

┌ ⛵ image backend
│ Use previously built image for backend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:4dedde8307a6aa4cecc00f99a44d19a7e716484677bf321c2ba68feb-1687373921162
│        id: e6ea855b7d02
│   created: 2023-06-21 21:58:40 +0300 MSK
│      size: 25.5 MiB
└ ⛵ image backend (2.05 seconds)

┌ ⛵ image frontend
│ Use previously built image for frontend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:328a492a57c7aa12e8af2ead55341f51f696961c2864d83545c6518d-1687253596426
│        id: f05a34a1cdd2
│   created: 2023-06-20 12:33:15 +0300 MSK
│      size: 16.0 MiB
└ ⛵ image frontend (2.08 seconds)

┌ Build summary
│ Set #0:
│ - ⛵ image backend (2.05 seconds)
│ - ⛵ image frontend (2.08 seconds)
└ Build summary

┌ Waiting for resources to become ready
│ ┌ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17-62f6d container/setup-and-migrate-db logs
│ │ 1/u create_talkers_table (129.700116ms)
│ └ job/setup-and-migrate-db-rev17 po/setup-and-migrate-db-rev17-62f6d container/setup-and-migrate-db logs
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                    REPLICAS         AVAILABLE          UP-TO-DATE                       
│ │ werf-guide-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-rev17                                                                    0                5s                 1/0                              
│ │ │   POD                                 READY        RESTARTS          STATUS                                                                                      
│ │ └── and-migrate-db-rev17-62f6d          0/1          0                 Completed              
│ │ RESOURCE                                                              NAMESPACE             CONDITION: CURRENT (DESIRED)                                           
│ │ Service/mysql                                                         werf-guide-app        -                                                                      
│ │ Service/werf-guide-app                                                werf-guide-app        -                                                                      
│ │ Ingress/werf-guide-app                                                werf-guide-app        -                                                                 ↵
│ │      
│ └ Status progress
└ Waiting for resources to become ready (4.98 seconds)

┌ Waiting for resources elimination: jobs/setup-and-migrate-db-rev11, jobs/setup-and-migrate-db-rev12, jobs/setup-and-migrate-db-rev16, jobs/setup-and-migrate-db-re ...
└ Waiting for resources elimination: jobs/setup-and-migrate-db-rev11, jobs/setup-and-migrate-db-rev12, jobs/setup-and-migrate-db-rev16, jobs/setup-an ... (0.30 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Wed Jun 21 21:59:06 2023
LAST PHASE: rollout
LAST STAGE: 0
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 17
TEST SUITE: None
Running time 9.43 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-приложения, развертывание базы данных вместе с этим приложением, а также автоматическая инициализация БД и выполнение миграций. Подобный подход должен хорошо работать с любыми реляционными БД.

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

назад
далее