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

Приложение в этой главе не предназначено для использования в 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/rails/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/rails/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/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/examples/rails/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/rails/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/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/examples/rails/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/rails/040_db/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

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

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

Мы перенесли конфигурацию для Active Record и MySQL из скелета нового Rails-приложения, сгенерированного следующей командой (в команде добавлено --database mysql и убрано --skip-active-record):

rails new --database mysql --skip-keeps --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-job --skip-active-storage --skip-action-cable --skip-sprockets --skip-spring --skip-listen --skip-turbolinks --skip-jbuilder --skip-test --skip-system-test --skip-bootsnap .

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

  1. Добавление mysql2 в Gemfile.
  2. Включение Active Record в config/application.rb.
  3. Создание конфигурационного файла config/database.yml.

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

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

Наш новый контроллер и модель:

class TalkerController < ActionController::API
  def say
    begin
      talker = Talker.find(0)
      render plain: talker.answer + ", " + talker.name + "!\n"
    rescue ActiveRecord::RecordNotFound => e
      render plain: "I have nothing to say.\n"
    end
  end

  def remember
    unless params.has_key?(:answer)
      render plain: "You forgot the answer :(\n" and return
    end
    unless params.has_key?(:name)
      render plain: "You forgot the name :(\n" and return
    end

    begin
      talker = Talker.find(0)
    rescue ActiveRecord::RecordNotFound => e
      talker = Talker.new
    end
    talker.update(id: 0, answer: params[:answer], name: params[:name])

    render plain: "Got it.\n"
  end
end
class TalkerController < ActionController::API def say begin talker = Talker.find(0) render plain: talker.answer + ", " + talker.name + "!\n" rescue ActiveRecord::RecordNotFound => e render plain: "I have nothing to say.\n" end end def remember unless params.has_key?(:answer) render plain: "You forgot the answer :(\n" and return end unless params.has_key?(:name) render plain: "You forgot the name :(\n" and return end begin talker = Talker.find(0) rescue ActiveRecord::RecordNotFound => e talker = Talker.new end talker.update(id: 0, answer: params[:answer], name: params[:name]) render plain: "Got it.\n" end end
class Talker < ActiveRecord::Base
end
class Talker < ActiveRecord::Base end

Добавим новые пути в список маршрутов:

Rails.application.routes.draw do
  get '/remember', to: 'talker#remember'
  get '/say', to: 'talker#say'

  get '/image', to: 'application#image'
  get '/ping', to: 'application#ping'
end
Rails.application.routes.draw do get '/remember', to: 'talker#remember' get '/say', to: 'talker#say' get '/image', to: 'application#image' get '/ping', to: 'application#ping' end

Также добавим две простых миграции:

class CreateTalkers < ActiveRecord::Migration[6.1]
  def change
    create_table :talkers do |t|
      t.text :answer
    end
  end
end
class CreateTalkers < ActiveRecord::Migration[6.1] def change create_table :talkers do |t| t.text :answer end end end
class AddNameToTalkers < ActiveRecord::Migration[6.1]
  def change
    add_column :talkers, :name, :string
  end
end
class AddNameToTalkers < ActiveRecord::Migration[6.1] def change add_column :talkers, :name, :string end end

… и schema.rb, сгенерированную на основе миграций:

# This file is auto-generated from the current state of the database.

ActiveRecord::Schema.define(version: 2021_08_17_164348) do

  create_table "talkers", charset: "utf8", force: :cascade do |t|
    t.text "answer"
    t.string "name"
  end

end
# This file is auto-generated from the current state of the database. ActiveRecord::Schema.define(version: 2021_08_17_164348) do create_table "talkers", charset: "utf8", force: :cascade do |t| t.text "answer" t.string "name" end end

Новые 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:5.6
        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
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:5.6 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, все же дальнейшие инструкции остаются без изменений.

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

production:
  adapter: mysql2
  encoding: utf8mb4
  pool: 5
  host: mysql
  port: 3306
  username: root
  password: password
  database: werf-guide-app
  encoding: utf8
production: adapter: mysql2 encoding: utf8mb4 pool: 5 host: mysql port: 3306 username: root password: password database: werf-guide-app encoding: utf8

БД и приложение готовы к развертыванию.

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

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

Чтобы выдержать очередность развертывания ресурсов, мы:

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

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

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

Реализуем это, добавив 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

          # Выполним первоначальную настройку базы, если она не выполнена, а иначе выполним миграции.
          bundle exec rails db:prepare
        env:
        - name: RAILS_ENV
          value: production
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 # Выполним первоначальную настройку базы, если она не выполнена, а иначе выполним миграции. bundle exec rails db:prepare env: - name: RAILS_ENV value: production
Зачем ждать выполнения 10 подряд успешных проверок доступности БД?

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

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

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

Теперь добавим в Deployment приложения init-контейнер, который будет дожидаться всего сразу: и доступности базы данных, и выполнения инициализации БД, и выполнения миграций:

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 bundle exec rails db:migrate:status; do
            sleep 1
          done
        env:
        - name: RAILS_ENV
          value: production
      containers:
      - name: backend
        image: {{ .Values.werf.image.backend }}
        command: ["bundle", "exec", "rails", "server"]
        ports:
        - containerPort: 3000
        env:
        - name: RAILS_ENV
          value: production
      - 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 initContainers: - name: wait-db-readiness image: {{ .Values.werf.image.backend }} command: - sh - -euc - | # Дожидаемся доступности БД и выполнения миграций. until bundle exec rails db:migrate:status; do sleep 1 done env: - name: RAILS_ENV value: production containers: - name: backend image: {{ .Values.werf.image.backend }} command: ["bundle", "exec", "rails", "server"] ports: - containerPort: 3000 env: - name: RAILS_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  329.2kB
│ │ backend/dockerfile  Step 1/25 : FROM ruby:2.7 as base
│ │ backend/dockerfile   ---> 1faa5f2f8ca3
...
│ │ backend/dockerfile  Step 25/25 : LABEL werf-version=v1.2.12+fix2
│ │ backend/dockerfile   ---> Running in be5cd3407213
│ │ backend/dockerfile  Removing intermediate container be5cd3407213
│ │ backend/dockerfile   ---> 8a86b0b0ad77
│ │ backend/dockerfile  Successfully built 8a86b0b0ad77
│ │ backend/dockerfile  Successfully tagged 90e9d19d-0e9a-463e-904a-c6ce4ed9ed51:latest
│ │ ┌ Store stage into .../werf-guide-app
│ │ └ Store stage into .../werf-guide-app (17.26 seconds)
│ ├ Info
│ │      name: .../werf-guide-app:0eab31032c0afad388f642f8bcf05fdef43adfa68b9ddd21c1ca5e6d-1629288112709
│ │        id: 8a86b0b0ad77
│ │   created: 2021-08-18 15:01:52 +0300 MSK
│ │      size: 364.7 MiB
│ └ Building stage backend/dockerfile (82.09 seconds)
└ ⛵ image backend (87.87 seconds)

┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile  Sending build context to Docker daemon  329.2kB
│ │ frontend/dockerfile  Step 1/29 : FROM ruby:2.7 as base
│ │ frontend/dockerfile   ---> 1faa5f2f8ca3
...
│ │ frontend/dockerfile  Step 29/29 : LABEL werf-version=v1.2.12+fix2
│ │ frontend/dockerfile   ---> Running in bb3c5dad93fd
│ │ frontend/dockerfile  Removing intermediate container bb3c5dad93fd
│ │ frontend/dockerfile   ---> 4e5dad5c8103
│ │ frontend/dockerfile  Successfully built 4e5dad5c8103
│ │ frontend/dockerfile  Successfully tagged 798eca35-72b4-4bcd-8b1c-9192845bf5c4:latest
│ │ ┌ Store stage into .../werf-guide-app
│ │ └ Store stage into .../werf-guide-app (9.93 seconds)
│ ├ Info
│ │      name: .../werf-guide-app:b6e90d80c9419ca3e1cb93fd3baf0f46632f05c2490881585e9f6598-1629288112504
│ │        id: 4e5dad5c8103
│ │   created: 2021-08-18 15:01:52 +0300 MSK
│ │      size: 9.4 MiB
│ └ Building stage frontend/dockerfile (74.28 seconds)
└ ⛵ image frontend (81.25 seconds)
...
│ ┌ job/setup-and-migrate-db-rev5 po/setup-and-migrate-db-rev5-kdb4t 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)'
...
│ ┌ deploy/werf-guide-app po/werf-guide-app-78f77cf6c4-8fzw6 container/wait-db-readiness logs
│ │
│ │ database: werf-guide-app
│ │
│ │  Status   Migration ID    Migration Name
│ │ --------------------------------------------------
│ │    up     20210817162438  Create talkers
│ │    up     20210817164348  Add name to talkers
│ │
│ └ deploy/werf-guide-app po/werf-guide-app-78f77cf6c4-8fzw6 container/wait-db-readiness logs
...
│ ┌ Status progress
│ │  DEPLOYMENT      REPLICAS  AVAILABLE  UP-TO-DATE
│ │  werf-guide-app  1/1       1          1
│ │  │    POD                         READY  RESTARTS  STATUS
│ │  ├──  guide-app-78f77cf6c4-8fzw6  2/2    0         Running
│ │  └──  guide-app-f98f5ccd9-69xwm   2/2    0         Terminating
│ │  STATEFULSET  REPLICAS  READY  UP-TO-DATE
│ │  mysql        1/1       1      1
│ │  JOB                        ACTIVE  DURATION  SUCCEEDED/FAILED
│ │  setup-and-migrate-db-rev5  0       19s       0->1/0
│ │  │    POD                        READY  RESTARTS  STATUS
│ │  └──  and-migrate-db-rev5-kdb4t  0/1    0         Running  ->  Completed
│ └  Status                     progress
└ Waiting for release resources to become ready (18.57 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Wed Aug 18 15:02:13 2021
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 5
TEST SUITE: None

Попробуем обратиться на /say, который должен попытаться достать данные из БД:

curl http://werf-guide-app/say

Но так как в базе данных пока пусто, должно вернуться следующее:

I have nothing to say.

Тогда сохраним данные в БД через /remember:

curl "http://werf-guide-app/remember?answer=Love+you&name=sweetie"

Ожидаемый результат, означающий, что данные сохранены:

Got it.

Снова попробуем получить данные из БД через /say:

curl http://werf-guide-app/say

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

Love you, sweetie!

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

kubectl exec -it statefulset/mysql -- mysql -ppassword -e "SELECT * from talkers" werf-guide-app

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

+----+----------+---------+
| id | answer   | name    |
+----+----------+---------+
|  0 | Love you | sweetie |
+----+----------+---------+

Готово!

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

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