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

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

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

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

Мы перенесли конфигурацию для MySQL из официального репозитория Laravel. Основные изменения, сделанные в нашем приложении:

  1. Добавление mysql-client и php-модуля pdo_mysql в Dockerfile.
  2. Создание конфигурационного файла config/database.php.

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

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

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

<?php

namespace App\Http\Controllers;

use App\Models\Talker;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class TalkerController extends Controller
{
    public function say(): string
    {
        /** @var Talker $talker */
        $talker = Talker::first();

        if (!$talker) {
            return "I have nothing to say";
        }

        return $talker->answer . ", " . $talker->name . "!";
    }

    public function remember(Request $request): string
    {
        if (!$request->has('answer')) {
            return "You forgot the answer :(";
        }

        if (!$request->has('name')) {
            return "You forgot the name :(";
        }

        /** @var Talker $talker */
        $talker = Talker::firstOrNew();
        $talker->answer = $request->input('answer');
        $talker->name = $request->input('name');
        $talker->save();

        return "Got it.";
    }
}
<?php namespace App\Http\Controllers; use App\Models\Talker; use Illuminate\Http\Request; use Illuminate\Routing\Controller; class TalkerController extends Controller { public function say(): string { /** @var Talker $talker */ $talker = Talker::first(); if (!$talker) { return "I have nothing to say"; } return $talker->answer . ", " . $talker->name . "!"; } public function remember(Request $request): string { if (!$request->has('answer')) { return "You forgot the answer :("; } if (!$request->has('name')) { return "You forgot the name :("; } /** @var Talker $talker */ $talker = Talker::firstOrNew(); $talker->answer = $request->input('answer'); $talker->name = $request->input('name'); $talker->save(); return "Got it."; } }
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
 * @property string $answer
 * @property string $name
 * @property string $created_at
 * @property string $updated_at
 * @property string $deleted_at
 */
class Talker extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'answer',
        'name',
    ];
}
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; /** * @property string $answer * @property string $name * @property string $created_at * @property string $updated_at * @property string $deleted_at */ class Talker extends Model { /** * The attributes that are mass assignable. * * @var string[] */ protected $fillable = [ 'answer', 'name', ]; }

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

<?php

use App\Http\Controllers\PingController;
use App\Http\Controllers\TalkerController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/ping', [PingController::class, 'ping']);

Route::get('/image', function () {
    return view('image');
});

Route::get('/say', [TalkerController::class, 'say']);
Route::get('/remember', [TalkerController::class, 'remember']);
<?php use App\Http\Controllers\PingController; use App\Http\Controllers\TalkerController; use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/ping', [PingController::class, 'ping']); Route::get('/image', function () { return view('image'); }); Route::get('/say', [TalkerController::class, 'say']); Route::get('/remember', [TalkerController::class, 'remember']);

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

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTalkersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('talkers', function (Blueprint $table) {
            $table->id();
            $table->text('answer');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('talkers');
    }
}
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTalkersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('talkers', function (Blueprint $table) { $table->id(); $table->text('answer'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('talkers'); } }
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddNameToTalkers extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('talkers', function(Blueprint $table) {
            $table->string('name');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('talkers', function(Blueprint $table) {
            $table->dropColumn('name');
        });
    }
}
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class AddNameToTalkers extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('talkers', function(Blueprint $table) { $table->string('name'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('talkers', function(Blueprint $table) { $table->dropColumn('name'); }); } }

Новые 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:8
        args: ["--default-authentication-plugin=mysql_native_password"]
        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:8 args: ["--default-authentication-plugin=mysql_native_password"] 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_HOST
  value: mysql
- name: DB_DATABASE
  value: werf-guide-app
- name: DB_USERNAME
  value: root
- name: DB_PASSWORD
  value: password
... - name: DB_HOST value: mysql - name: DB_DATABASE value: werf-guide-app - name: DB_USERNAME value: root - name: DB_PASSWORD value: password

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

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

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

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

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

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

Реализуем это, добавив Job для выполнения миграций/инициализации базы данных:

apiVersion: batch/v1
kind: Job
metadata:
  name: "migrate-db"
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      imagePullSecrets:
      - name: registrysecret
      containers:
      - name: migrate-db
        image: {{ .Values.werf.image.backend }}
        command:
        - sh
        - -euc
        - |
          is_mysql_available() {
            tries=$1
            i=0
            while [ $i -lt $tries ]; do
              mysqladmin -h $DB_HOST -P 3306 -u $DB_USERNAME -p$DB_PASSWORD ping || return 1
              i=$((i+1))
              sleep 1
            done
          }

          # Дождёмся, когда `mysqladmin ping` отработает 10 раз подряд.
          until is_mysql_available 10; do
            sleep 1
          done

          # Выполним миграции.
          php artisan migrate --force
        env:
        - name: DB_HOST
          value: mysql
        - name: DB_DATABASE
          value: werf-guide-app
        - name: DB_USERNAME
          value: root
        - name: DB_PASSWORD
          value: password
apiVersion: batch/v1 kind: Job metadata: name: "migrate-db" annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-weight": "-1" "helm.sh/hook-delete-policy": before-hook-creation spec: backoffLimit: 0 template: spec: restartPolicy: Never imagePullSecrets: - name: registrysecret containers: - name: migrate-db image: {{ .Values.werf.image.backend }} command: - sh - -euc - | is_mysql_available() { tries=$1 i=0 while [ $i -lt $tries ]; do mysqladmin -h $DB_HOST -P 3306 -u $DB_USERNAME -p$DB_PASSWORD ping || return 1 i=$((i+1)) sleep 1 done } # Дождёмся, когда `mysqladmin ping` отработает 10 раз подряд. until is_mysql_available 10; do sleep 1 done # Выполним миграции. php artisan migrate --force env: - name: DB_HOST value: mysql - name: DB_DATABASE value: werf-guide-app - name: DB_USERNAME value: root - name: DB_PASSWORD value: password
Зачем ждать выполнения 10 подряд успешных проверок доступности БД?

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

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

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

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

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

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

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

...
┌ ⛵ image backend
│ ┌ Building stage backend/dockerfile
│ │ backend/dockerfile  Sending build context to Docker daemon  510.5kB
│ │ backend/dockerfile  Step 1/19 : FROM php:8.0-fpm-alpine as base
│ │ backend/dockerfile   ---> 52c511f481c5
...
│ │ backend/dockerfile  Successfully built 0a1d03dc74ac
│ │ backend/dockerfile  Successfully tagged c5514065-5c4c-4e73-9a84-a777c9be56a3:latest
│ ├ Info
│ │      name: <DOCKER HUB USERNAME>/werf-guide-app:c1c78d5859300a5b439f0aff914ca2025ff5af4ad7cac3022d561bac-1633696904980
│ │        id: 0a1d03dc74ac
│ │   created: 2022-10-08 15:41:44 +0000 UTC
│ │      size: 56.7 MiB
│ └ Building stage backend/dockerfile (28.12 seconds)
└ ⛵ image backend (33.26 seconds)

┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile  Sending build context to Docker daemon  510.5kB
│ │ frontend/dockerfile  Step 1/30 : FROM php:8.0-fpm-alpine as base
│ │ frontend/dockerfile   ---> 52c511f481c5
...
│ │ frontend/dockerfile  Successfully built 0644ab57979f
│ │ frontend/dockerfile  Successfully tagged 2128b312-2942-4163-9de5-06f9fd191bc9:latest
│ ├ Info
│ │      name: <DOCKER HUB USERNAME>/werf-guide-app:773d56f3d626435554c0d2d85e7949ecd4cc0a6fea3e8557848366b0-1633696968872
│ │        id: 0644ab57979f
│ │   created: 2022-10-08 15:42:48 +0000 UTC
│ │      size: 9.4 MiB
│ └ Building stage frontend/dockerfile (89.50 seconds)
└ ⛵ image frontend (94.24 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                      REPLICAS          AVAILABLE           UP-TO-DATE
│ │ werf-guide-app                                                                                  2->1/1            2->1                1
│ │ │   POD                                 READY        RESTARTS          STATUS
│ │ ├── guide-app-645c598898-lfncm          0/2          0                 Terminating
│ │ └── guide-app-6b57898748-tjg9k          2/2          0                 Running
│ │ STATEFULSET                                                                                     REPLICAS          READY               UP-TO-DATE
│ │ mysql                                                                                           1/1               0->1                1
│ │ │   POD                                 READY        RESTARTS          STATUS
│ │ └── 0                                   1/1          0                 ContainerCreating ->
│ │                                                                        Running
│ └ Status progress
└ Waiting for release resources to become ready (12.62 seconds)

┌ Waiting for helm hook job/migrate-db termination
│ ┌ job/migrate-db po/migrate-db-ddrgt container/migrate-db logs
│ │ mysqld is alive
│ │ mysqld is alive
│ │ Migration table created successfully.
│ │ Migrating: 2019_12_14_000001_create_personal_access_tokens_table
│ │ Migrated:  2019_12_14_000001_create_personal_access_tokens_table (139.30ms)
│ │ Migrating: 2021_10_04_000000_create_talkers_table
│ │ Migrated:  2021_10_04_000000_create_talkers_table (50.37ms)
│ │ Migrating: 2021_10_04_000001_add_name_to_talkers
│ │ Migrated:  2021_10_04_000001_add_name_to_talkers (40.98ms)
│ └ job/migrate-db po/migrate-db-ddrgt container/migrate-db logs
│
│ ┌ Status progress
│ │ JOB                                                                                             ACTIVE            DURATION            SUCCEEDED/FAILED
│ │ migrate-db                                                                                      0                 29s                 0->1/0
│ │ │   POD                                 READY        RESTARTS          STATUS
│ │ └── db-ddrgt                            0/1          0                 Running -> Completed
│ └ Status progress
└ Waiting for helm hook job/migrate-db termination (29.70 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Fri Oct  8 15:43:57 2022
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 14
TEST SUITE: None
Running time 57.27 seconds
... ┌ ⛵ image backend │ ┌ Building stage backend/dockerfile │ │ backend/dockerfile Sending build context to Docker daemon 510.5kB │ │ backend/dockerfile Step 1/19 : FROM php:8.0-fpm-alpine as base │ │ backend/dockerfile ---> 52c511f481c5 ... │ │ backend/dockerfile Successfully built 0a1d03dc74ac │ │ backend/dockerfile Successfully tagged c5514065-5c4c-4e73-9a84-a777c9be56a3:latest │ ├ Info │ │ name: <DOCKER HUB USERNAME>/werf-guide-app:c1c78d5859300a5b439f0aff914ca2025ff5af4ad7cac3022d561bac-1633696904980 │ │ id: 0a1d03dc74ac │ │ created: 2022-10-08 15:41:44 +0000 UTC │ │ size: 56.7 MiB │ └ Building stage backend/dockerfile (28.12 seconds) └ ⛵ image backend (33.26 seconds) ┌ ⛵ image frontend │ ┌ Building stage frontend/dockerfile │ │ frontend/dockerfile Sending build context to Docker daemon 510.5kB │ │ frontend/dockerfile Step 1/30 : FROM php:8.0-fpm-alpine as base │ │ frontend/dockerfile ---> 52c511f481c5 ... │ │ frontend/dockerfile Successfully built 0644ab57979f │ │ frontend/dockerfile Successfully tagged 2128b312-2942-4163-9de5-06f9fd191bc9:latest │ ├ Info │ │ name: <DOCKER HUB USERNAME>/werf-guide-app:773d56f3d626435554c0d2d85e7949ecd4cc0a6fea3e8557848366b0-1633696968872 │ │ id: 0644ab57979f │ │ created: 2022-10-08 15:42:48 +0000 UTC │ │ size: 9.4 MiB │ └ Building stage frontend/dockerfile (89.50 seconds) └ ⛵ image frontend (94.24 seconds) ┌ Waiting for release resources to become ready │ ┌ Status progress │ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE │ │ werf-guide-app 2->1/1 2->1 1 │ │ │ POD READY RESTARTS STATUS │ │ ├── guide-app-645c598898-lfncm 0/2 0 Terminating │ │ └── guide-app-6b57898748-tjg9k 2/2 0 Running │ │ STATEFULSET REPLICAS READY UP-TO-DATE │ │ mysql 1/1 0->1 1 │ │ │ POD READY RESTARTS STATUS │ │ └── 0 1/1 0 ContainerCreating -> │ │ Running │ └ Status progress └ Waiting for release resources to become ready (12.62 seconds) ┌ Waiting for helm hook job/migrate-db termination │ ┌ job/migrate-db po/migrate-db-ddrgt container/migrate-db logs │ │ mysqld is alive │ │ mysqld is alive │ │ Migration table created successfully. │ │ Migrating: 2019_12_14_000001_create_personal_access_tokens_table │ │ Migrated: 2019_12_14_000001_create_personal_access_tokens_table (139.30ms) │ │ Migrating: 2021_10_04_000000_create_talkers_table │ │ Migrated: 2021_10_04_000000_create_talkers_table (50.37ms) │ │ Migrating: 2021_10_04_000001_add_name_to_talkers │ │ Migrated: 2021_10_04_000001_add_name_to_talkers (40.98ms) │ └ job/migrate-db po/migrate-db-ddrgt container/migrate-db logs │ │ ┌ Status progress │ │ JOB ACTIVE DURATION SUCCEEDED/FAILED │ │ migrate-db 0 29s 0->1/0 │ │ │ POD READY RESTARTS STATUS │ │ └── db-ddrgt 0/1 0 Running -> Completed │ └ Status progress └ Waiting for helm hook job/migrate-db termination (29.70 seconds) Release "werf-guide-app" has been upgraded. Happy Helming! NAME: werf-guide-app LAST DEPLOYED: Fri Oct 8 15:43:57 2022 NAMESPACE: werf-guide-app STATUS: deployed REVISION: 14 TEST SUITE: None Running time 57.27 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   | created_at          | updated_at          | name    |
+----+----------+---------------------+---------------------+---------+
|  1 | Love you | 2021-10-08 12:46:22 | 2021-10-08 12:46:22 | sweetie |
+----+----------+---------------------+---------------------+---------+

Готово!

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

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

назад
далее