В этой главе в приложении появится функциональность, которая позволит загружать и скачивать файлы. Будут рассмотрены особенности работы с файлами в Kubernetes, а также продемонстрирован рабочий пример с использованием S3-хранилища.

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

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

Подготовьте рабочее окружение согласно c инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

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

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/050_s3/. .
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/nodejs/040_db ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/050_s3/* .
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/nodejs/040_db ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/050_s3/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Как хранить файлы

Контейнеры, развертываемые в Kubernetes, часто будут создаваться и удаляться автоматически — например, из-за обновления Deployment. Это значит, что мы не можем хранить файлы, создающиеся приложением, прямо в файловой системе контейнера. Потому что так файлы будут:

  • доступны только одному контейнеру/реплике приложения, а не всем,
  • удаляться при удалении контейнера.

Поэтому контейнеры должны хранить в своей файловой системе только те данные, которые можно потерять.

Кстати, когда такая возможность есть, файловую систему контейнера даже переключают в read-only, что улучшает безопасность и позволяет убедиться, что приложение действительно не сохраняет данные локально.

Но что делать, если какие-то данные все-таки нужно хранить? Для этого обычно используют развертываемые отдельно базы данных. В частности, для хранения обычных файлов часто используют нереляционные базы данных вроде объектных хранилищ. И особенно часто для хранения файлов используют объектные хранилища, предоставляющие API, совместимый с Amazon S3 API.

Далее мы продемонстрируем, как хранить файлы не в локальной файловой системе, а в S3-совместимом хранилище, чтобы ваши приложения оставались stateless и не испытывали проблем при работе с Kubernetes.

Подготовка

Добавим библиотеки для работы с файлами:

npm i @aws-sdk/client-s3 express-fileupload readable-web-to-node-stream
  • @aws-sdk/client-s3 — для работы с хранилищем, совместимым с AWS S3;
  • express-fileupload — для простого получения файла в запросе express;
  • readable-web-to-node-stream — для конвертирования потока web stream в node stream, что пригодится для отправки файлов клиенту.

Нам подойдет библиотека @aws-sdk/client-s3, так как интерфейс minio совместим с AWS S3. Заодно так будет проще мигрировать на сам AWS S3, если предстоит такая необходимость.

Добавление endpoint’ов /upload и /download в приложение

Для демонстрации работы загрузки и отдачи файлов мы добавим два новых endpoints, один из которых будет загружать файл в S3-совместимое объектное хранилище (/upload), а другой — отдавать его оттуда (/download).

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

//@ts-check
const express = require('express');
const router = express.Router();
const asyncHandler = require('express-async-handler');
const config = require('../config/minio.json');
const fileUpload = require('express-fileupload');
const {
  S3,
  GetObjectCommand,
  PutObjectCommand,
} = require('@aws-sdk/client-s3');
const { Readable } = require('stream');
const { ReadableWebToNodeStream } = require('readable-web-to-node-stream');

module.exports = (logger) => {
  const s3 = new S3({ ...config, logger });
  const key = 'thekey';

  router.get(
    '/download',
    asyncHandler(async (req, res) => {
      try {
        const cmd = new GetObjectCommand({
          Bucket: config.bucket,
          Key: key,
        });

        const file = await s3.send(cmd);
        const body = file.Body;
        if (!body) {
          throw new Error('absent object body');
        }

        if (body instanceof Readable) {
          body.pipe(res);
          return;
        }

        if (body instanceof ReadableStream) {
          new Readable(new ReadableWebToNodeStream(body)).pipe(res);
          return;
        }

        if (body instanceof Blob) {
          body.stream().pipe(res);
          return;
        }

        throw new Error('cannot handle S3 response body');
      } catch (e) {
        if (e.name === 'NoSuchKey') {
          res.status(404).send(`You haven't uploaded anything yet.\n`);
          return;
        }
        res.status(500).send(`Something went wrong: ${e.message}\n`);
      }
    })
  );

  router.post(
    '/upload',
    fileUpload(),
    asyncHandler(async (req, res) => {
      try {
        if (!req.files || !req.files.file) {
          res.status(400).send('You forgot to attach a file.\n');
          return;
        }

        let file = req.files.file;
        if (Array.isArray(file)) {
          file = file[0];
        }

        const cmd = new PutObjectCommand({
          Bucket: config.bucket,
          Key: key,
          //@ts-ignore
          Body: file.data,
        });
        await s3.send(cmd);
        res.status(200).send('File uploaded.\n');
      } catch (e) {
        res.status(500).send(`Something went wrong: ${e.message}\n`);
        return;
      }
    })
  );

  return router;
};
//@ts-check const express = require('express'); const router = express.Router(); const asyncHandler = require('express-async-handler'); const config = require('../config/minio.json'); const fileUpload = require('express-fileupload'); const { S3, GetObjectCommand, PutObjectCommand, } = require('@aws-sdk/client-s3'); const { Readable } = require('stream'); const { ReadableWebToNodeStream } = require('readable-web-to-node-stream'); module.exports = (logger) => { const s3 = new S3({ ...config, logger }); const key = 'thekey'; router.get( '/download', asyncHandler(async (req, res) => { try { const cmd = new GetObjectCommand({ Bucket: config.bucket, Key: key, }); const file = await s3.send(cmd); const body = file.Body; if (!body) { throw new Error('absent object body'); } if (body instanceof Readable) { body.pipe(res); return; } if (body instanceof ReadableStream) { new Readable(new ReadableWebToNodeStream(body)).pipe(res); return; } if (body instanceof Blob) { body.stream().pipe(res); return; } throw new Error('cannot handle S3 response body'); } catch (e) { if (e.name === 'NoSuchKey') { res.status(404).send(`You haven't uploaded anything yet.\n`); return; } res.status(500).send(`Something went wrong: ${e.message}\n`); } }) ); router.post( '/upload', fileUpload(), asyncHandler(async (req, res) => { try { if (!req.files || !req.files.file) { res.status(400).send('You forgot to attach a file.\n'); return; } let file = req.files.file; if (Array.isArray(file)) { file = file[0]; } const cmd = new PutObjectCommand({ Bucket: config.bucket, Key: key, //@ts-ignore Body: file.data, }); await s3.send(cmd); res.status(200).send('File uploaded.\n'); } catch (e) { res.status(500).send(`Something went wrong: ${e.message}\n`); return; } }) ); return router; };

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

...
const filesRouter = require('./routes/files');
...
app.use('/', filesRouter(logger));
... const filesRouter = require('./routes/files'); ... app.use('/', filesRouter(logger));

Новые endpoint’ы — /upload и /download — добавлены. Осталось только настроить для них работу с хранилищем.

Развертывание и подключение MinIO

Для демонстрации, в качестве S3-совместимого объектного хранилища мы будем использовать MinIO, но вместо него может использоваться и любое другое S3-хранилище (например, Amazon S3).

Обратите внимание, что если вы используете иное S3-хранилище, то создание нижеописанных StatefulSet и Job для MinIO не потребуется, но остальные инструкции останутся актуальными.

Добавим StatefulSet с MinIO:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
spec:
  serviceName: minio
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: minio/minio
        args: ["server", "/data", "--console-address", ":9001"]
        ports:
        - containerPort: 9000
          name: minio
        - containerPort: 9001
          name: console
        volumeMounts:
        - name: minio-data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: minio-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 100Mi

---
apiVersion: v1
kind: Service
metadata:
  name: minio
spec:
  selector:
    app: minio
  ports:
  - port: 9000
    name: minio
  - port: 9001
    name: console
apiVersion: apps/v1 kind: StatefulSet metadata: name: minio spec: serviceName: minio selector: matchLabels: app: minio template: metadata: labels: app: minio spec: containers: - name: minio image: minio/minio args: ["server", "/data", "--console-address", ":9001"] ports: - containerPort: 9000 name: minio - containerPort: 9001 name: console volumeMounts: - name: minio-data mountPath: /data volumeClaimTemplates: - metadata: name: minio-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 100Mi --- apiVersion: v1 kind: Service metadata: name: minio spec: selector: app: minio ports: - port: 9000 name: minio - port: 9001 name: console

Создадим Job для настройки MinIO:

apiVersion: batch/v1
kind: Job
metadata:
  name: "setup-minio-rev{{ .Release.Revision }}"
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: setup-minio
        image: minio/mc
        command:
        - sh
        - -euc
        - |
          is_minio_available() {
            tries=$1
            i=0
            while [ $i -lt $tries ]; do
              curl -sSL http://minio:9000/minio/health/live || return 1
              i=$((i+1))
              sleep 1
            done
          }

          # Дожидаемся доступности MinIO.
          until is_minio_available 10; do
            sleep 1
          done

          # Настроим доступ к нашем инстансу MinIO.
          mc alias set minio http://minio:9000 minioadmin minioadmin

          # Создадим bucket для нашего приложения.
          mc mb --ignore-existing minio/werf-guide-app
apiVersion: batch/v1 kind: Job metadata: name: "setup-minio-rev{{ .Release.Revision }}" spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: setup-minio image: minio/mc command: - sh - -euc - | is_minio_available() { tries=$1 i=0 while [ $i -lt $tries ]; do curl -sSL http://minio:9000/minio/health/live || return 1 i=$((i+1)) sleep 1 done } # Дожидаемся доступности MinIO. until is_minio_available 10; do sleep 1 done # Настроим доступ к нашем инстансу MinIO. mc alias set minio http://minio:9000 minioadmin minioadmin # Создадим bucket для нашего приложения. mc mb --ignore-existing minio/werf-guide-app

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

{
  "region": "us-east-1",
  "endpoint": {
    "protocol": "http",
    "hostname": "minio",
    "port": 9000,
    "path": "/"
  },
  "bucket": "werf-guide-app",
  "forcePathStyle": true,
  "credentials": {
    "accessKeyId": "minioadmin",
    "secretAccessKey": "minioadmin"
  }
}
{ "region": "us-east-1", "endpoint": { "protocol": "http", "hostname": "minio", "port": 9000, "path": "/" }, "bucket": "werf-guide-app", "forcePathStyle": true, "credentials": { "accessKeyId": "minioadmin", "secretAccessKey": "minioadmin" } }

Теперь MinIO готов к развертыванию, а приложение — настроено на хранение файлов в нём.

Проверка работы хранилища

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

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

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


┌ ⛵ image backend
│ ┌ Building stage backend/dockerfile
│ │ backend/dockerfile  Sending build context to Docker daemon  519.2kB
│ │ backend/dockerfile  Step 1/27 : FROM node:12-alpine as builder
    ...
│ │ backend/dockerfile  Successfully built 1483efb79d3e
│ │ backend/dockerfile  Successfully tagged 80de51be-533b-4360-a0a1-9649b8e5f340:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (15.81 seconds)
│ ├ Info
│ │      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:89aa5375d6575fcbcba7848469df9b7cb1cd5b455c78c403a9922285-1637083461218
│ │        id: 1483efb79d3e
│ │   created: 2021-11-16 20:24:21 +0300 MSK
│ │      size: 52.1 MiB
│ └ Building stage backend/dockerfile (21.74 seconds)
└ ⛵ image backend (29.16 seconds)

┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile  Sending build context to Docker daemon  519.2kB
│ │ frontend/dockerfile  Step 1/31 : FROM node:12-alpine as builder
    ...
│ │ frontend/dockerfile  Successfully built 479b3ed46505
│ │ frontend/dockerfile  Successfully tagged fdeee494-5aad-4d03-93bd-26884b2b7acf:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (12.13 seconds)
│ ├ Info
│ │      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:931432640c233a0b42a2fed6311c9d2e7837792b207ce08516fe7384-1637083461046
│ │        id: 479b3ed46505
│ │   created: 2021-11-16 20:24:20 +0300 MSK
│ │      size: 9.4 MiB
│ └ Building stage frontend/dockerfile (17.97 seconds)
└ ⛵ image frontend (25.02 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ ├── guide-app-66b78c6dd8-49mxt   0/2        0             Init:0/1            Waiting for: replicas 2->1                          ↵
│ │
│ │ └── guide-app-85c5c7c485-vkv28   2/2        0             Running
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           0               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── 0                            0/1        0             ContainerCreating   Waiting for: ready 0->1                             ↵
│ │
│ │ mysql                                                                         1/1           1               1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev19                                                    1             2s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev19--1-cw6l 0/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │     l
│ │ setup-minio-rev19                                                             1             2s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev19--1-xxbbv         0/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ ├── guide-app-66b78c6dd8-49mxt   0/2        0             Init:0/1            Waiting for: replicas 2->1                          ↵
│ │
│ │ └── guide-app-85c5c7c485-vkv28   2/2        0             Running
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           0->1            1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             ContainerCreating
│ │                                                           -> Running
│ │ mysql                                                                         1/1           1               1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev19                                                    1             6s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev19--1-cw6l 1/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │     l                                                     -> Running
│ │ setup-minio-rev19                                                             1             2s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev19--1-xxbbv         0/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│
│ ┌ job/setup-minio-rev19 po/setup-minio-rev19--1-xxbbv container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ job/setup-minio-rev19 po/setup-minio-rev19--1-xxbbv container/setup-minio logs
│
│ ┌ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│
│ ┌ deploy/werf-guide-app po/werf-guide-app-66b78c6dd8-49mxt container/wait-db-readiness logs
│ │
│ │ Sequelize CLI [Node: 12.22.7, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.json".
│ │ Using environment "production".
│ │ up 20211101064002-create-talker.js
│ └ deploy/werf-guide-app po/werf-guide-app-66b78c6dd8-49mxt container/wait-db-readiness logs
│
│ ┌ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ ├── guide-app-66b78c6dd8-49mxt   0/2        0             Init:0/1 ->         Waiting for: replicas 2->1                          ↵
│ │
│ │ │                                                         PodInitializing
│ │ └── guide-app-85c5c7c485-vkv28   2/2        0             Running
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ mysql                                                                         1/1           1               1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev19                                                    1             6s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev19--1-cw6l 1/1        0             Running             Waiting for: pods should be terminated, succeeded 0->1
│ │     l
│ │ setup-minio-rev19                                                             1             11s             0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev19--1-xxbbv         1/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │                                                           -> Running
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│ │
│ │ Sequelize CLI [Node: 12.22.7, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.json".
│ │ Using environment "production".
│ │ No migrations were executed, database schema was already up to date.
│ └ job/setup-and-migrate-db-rev19 po/setup-and-migrate-db-rev19--1-cw6ll container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2->1/1        1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ ├── guide-app-66b78c6dd8-49mxt   2/2        0             PodInitializing ->
│ │ │                                                         Running
│ │ └── guide-app-85c5c7c485-vkv28   2/2        0             Running ->
│ │                                                           Terminating
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ mysql                                                                         1/1           1               1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev19                                                    0             17s             0->1/0                ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── and-migrate-db-rev19--1-cw6l 0/1        0             Running ->
│ │     l                                                     Completed
│ │ setup-minio-rev19                                                             1             11s             0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev19--1-xxbbv         1/1        0             Running             Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
│ ┌ job/setup-minio-rev19 po/setup-minio-rev19--1-xxbbv container/setup-minio logs
│ │ Added `minio` successfully.
│ │ Bucket created successfully `minio/werf-guide-app`.
│ └ job/setup-minio-rev19 po/setup-minio-rev19--1-xxbbv container/setup-minio logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ ├── guide-app-66b78c6dd8-49mxt   2/2        0             Running
│ │ └── guide-app-85c5c7c485-vkv28   2/2        0             Terminating
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ mysql                                                                         1/1           1               1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev19                                                    0             17s             1/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── and-migrate-db-rev19--1-cw6l 0/1        0             Completed
│ │     l
│ │ setup-minio-rev19                                                             0             23s             0->1/0
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── minio-rev19--1-xxbbv         0/1        0             Running ->
│ │                                                           Completed
│ └ Status progress
└ Waiting for release resources to become ready (22.83 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Tue Nov 16 20:24:43 2021
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 19
TEST SUITE: None
Running time 58.21 seconds

Обратимся на /download, который должен попытаться достать файл из S3:

curl http://werf-guide-app.test/download

Так как пока никаких файлов не загружено, получим:

You haven't uploaded anything yet.

Тогда создадим новый файл и загрузим его в S3:

echo "This is file content." > file.txt
curl -F "file=@file.txt" http://werf-guide-app.test/upload
"This is file content." | Out-File -Encoding ascii -FilePath file.txt
curl.exe -F "file=@file.txt" http://werf-guide-app.test/upload

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

File uploaded.

Снова попробуем получить файл из хранилища:

curl http://werf-guide-app.test/download

В ответ должно отобразиться содержимое файла:

This is file content.

Также убедимся, что файл сохранился и достаётся именно из хранилища. Для этого сначала запустим контейнер с утилитой mc для взаимодействия с MinIO:

kubectl run mc --image=minio/mc --rm -it --command -- bash

Теперь, оказавшись внутри контейнера, выполним команды:

# Настроим подключение к MinIO.
mc alias set minio http://minio:9000 minioadmin minioadmin
# Получим содержимое сохранённого файла из S3.
mc cat "minio/werf-guide-app/$(mc ls minio/werf-guide-app | awk 'NR==1 {print $5}')"

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

This is file content.

Теперь, если нам требуется работать с файлами, мы можем получать и хранить их в объектном хранилище, а не в файловой системе контейнера. При таком подходе Pod’ы приложения могут создаваться и удаляться без проблем: файлы не потеряются, будут доступны с любой реплики Deployment’а приложения, а файловая система на узлах Kubernetes не будет заполняться ненужными файлами.

Не забывайте: в контейнере можно хранить только те данные, которые можно потерять. Все остальные данные нужно хранить в соответствующих базах данных/хранилищах. Такой подход, в частности, подтверждается лучшими практиками от инженеров Google Cloud.