В этой статье в приложении появится функциональность, которая позволит загружать и скачивать файлы. Будут рассмотрены особенности работы с файлами в Kubernetes, а также продемонстрирован рабочий пример с использованием S3-хранилища.
Приложение в этой статье не предназначено для использования в production без доработки. Готовое к работе в production приложение мы получим в конце руководства.
Подготовка окружения
Если вы ещё не подготовили своё рабочее окружение на предыдущих этапах, сделайте это в соответствии с инструкциями статьи «Подготовка окружения».
Если ваше рабочее окружение работало, но перестало, или же последующие инструкции из этой статьи не работают — попробуйте следующее:
Запустим приложение 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 приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:
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/website $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/website ~/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;
};
Добавим новые пути в маршруты:
...
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
Создадим 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:RELEASE.2023-10-04T06-52-56Z
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"
}
}
Теперь 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: 2022-11-16 20:24:21 +0000 UTC
│ │ 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: 2022-11-16 20:24:20 +0000 UTC
│ │ 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 2022
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 -n werf-guide-app run mc --image=minio/mc:RELEASE.2023-10-04T06-52-56Z --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 $6}')"
Ожидаемый результат:
This is file content.
Теперь, если нам требуется работать с файлами, мы можем получать и хранить их в объектном хранилище, а не в файловой системе контейнера. При таком подходе Pod’ы приложения могут создаваться и удаляться без проблем: файлы не потеряются, будут доступны с любой реплики Deployment’а приложения, а файловая система на узлах Kubernetes не будет заполняться ненужными файлами.
Не забывайте: в контейнере можно хранить только те данные, которые можно потерять. Все остальные данные нужно хранить в соответствующих базах данных/хранилищах. Такой подход, в частности, подтверждается лучшими практиками от инженеров Google Cloud.