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

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

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

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

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

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

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

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

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

Установка зависимостей

Подключим необходимые зависимости в pom.xml:

...
<dependency>
	<groupId>io.minio</groupId>
	<artifactId>minio</artifactId>
	<version>8.2.1</version>
</dependency>
... <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.2.1</version> </dependency>

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

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

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

package io.werf.werfguidesapp.controllers;

import io.werf.werfguidesapp.components.MinioComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

@RestController
public class MinioController {

	@Autowired
	private MinioComponent minioComponent;

	public MinioController() {

	}

	@PostMapping("/upload")
	public String uploadFileToMinIO(@RequestParam("file") MultipartFile file) {
		try {
			InputStream in = new ByteArrayInputStream(file.getBytes());
			String fileName = file.getOriginalFilename();
			minioComponent.putObject(fileName, in);
			return "File uploaded.";
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "Something wrong.";
	}

	@GetMapping("/download")
	public String downloadFile() throws Exception {
		return minioComponent.getObject("file.txt");
	}
}
package io.werf.werfguidesapp.controllers; import io.werf.werfguidesapp.components.MinioComponent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.InputStream; @RestController public class MinioController { @Autowired private MinioComponent minioComponent; public MinioController() { } @PostMapping("/upload") public String uploadFileToMinIO(@RequestParam("file") MultipartFile file) { try { InputStream in = new ByteArrayInputStream(file.getBytes()); String fileName = file.getOriginalFilename(); minioComponent.putObject(fileName, in); return "File uploaded."; } catch (Exception e) { e.printStackTrace(); } return "Something wrong."; } @GetMapping("/download") public String downloadFile() throws Exception { return minioComponent.getObject("file.txt"); } }

Новые 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

Добавим в Deployment init container для настройки MinIO. Он будет выполняться до старта основного приложения и ожидать готовности MinIO (чтобы избежать падения приложения во время старта из-за невозможности достучаться до MinIO):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-guide-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: werf-guide-app
  template:
    metadata:
      labels:
        app: werf-guide-app
    spec:
      imagePullSecrets:
      - name: registrysecret
      initContainers:
      - name: 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

      containers:
      - name: backend
        image: {{ .Values.werf.image.backend }}
        command: 
        - sh
        - -euc
        - |
          is_mysql_available() {
            tries=$1
            i=0
            while [ $i -lt $tries ]; do
              mysqladmin -h mysql -P 3306 -u root -p=password ping || return 1
              i=$((i+1))
              sleep 1
            done
          }
          # Дождёмся, когда `mysqladmin ping` отработает 10 раз подряд.
          until is_mysql_available 10; do
            sleep 1
          done
          
          java -jar ./app.jar

        ports:
        - containerPort: 8080
        env:
        - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB
          value: INFO
        - name: DB_HOST
          value: mysql
        - name: DB_NAME
          value: werf-guide-app
        - name: DB_USER
          value: root
        - name: DB_PASSWD
          value: password
      - name: frontend
        image: {{ .Values.werf.image.frontend }}
        ports:
        - containerPort: 80
apiVersion: apps/v1 kind: Deployment metadata: name: werf-guide-app spec: replicas: 1 selector: matchLabels: app: werf-guide-app template: metadata: labels: app: werf-guide-app spec: imagePullSecrets: - name: registrysecret initContainers: - name: 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 containers: - name: backend image: {{ .Values.werf.image.backend }} command: - sh - -euc - | is_mysql_available() { tries=$1 i=0 while [ $i -lt $tries ]; do mysqladmin -h mysql -P 3306 -u root -p=password ping || return 1 i=$((i+1)) sleep 1 done } # Дождёмся, когда `mysqladmin ping` отработает 10 раз подряд. until is_mysql_available 10; do sleep 1 done java -jar ./app.jar ports: - containerPort: 8080 env: - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB value: INFO - name: DB_HOST value: mysql - name: DB_NAME value: werf-guide-app - name: DB_USER value: root - name: DB_PASSWD value: password - name: frontend image: {{ .Values.werf.image.frontend }} ports: - containerPort: 80

Добавим в приложение конфигурацию для подключения к MinIO:

...
minio.url=minio
minio.accessKey=minioadmin
minio.secretKey=minioadmin
minio.bucket-name=werf-guide-app
minio.secure=false
... minio.url=minio minio.accessKey=minioadmin minio.secretKey=minioadmin minio.bucket-name=werf-guide-app minio.secure=false

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

Создадим конфигурационный класс, куда передадим настройки из application.properties и активируем MinIO:

package io.werf.werfguidesapp.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.minio.MinioClient;

@Configuration
public class MinioConfig {

	@Value("${minio.url}")
	private String minioUrl;
	@Value("${minio.accessKey}")
	private String accessKey;
	@Value("${minio.secretKey}")
	private String secretKey;
	@Value("${minio.bucket-name}")
	private String bucketName;
	@Value("${minio.secure}")
	private Boolean minioSecure;

	@Bean
	public MinioClient minioClient() {
		MinioClient minioClient = MinioClient.builder().credentials(accessKey, secretKey)
				.endpoint(minioUrl, 9000, minioSecure).build();
		return minioClient;
	}
}
package io.werf.werfguidesapp.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.minio.MinioClient; @Configuration public class MinioConfig { @Value("${minio.url}") private String minioUrl; @Value("${minio.accessKey}") private String accessKey; @Value("${minio.secretKey}") private String secretKey; @Value("${minio.bucket-name}") private String bucketName; @Value("${minio.secure}") private Boolean minioSecure; @Bean public MinioClient minioClient() { MinioClient minioClient = MinioClient.builder().credentials(accessKey, secretKey) .endpoint(minioUrl, 9000, minioSecure).build(); return minioClient; } }

Теперь создадим компонент, который будет отвечать за работу с файлами в MinIO, добавив в него два основных метода putObject и getObject:

package io.werf.werfguidesapp.components;

import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@Component
public class MinioComponent {

    public MinioComponent() {

    }

    @Autowired
    private MinioClient minioClient;

    @Value("${minio.bucket-name}")
    private String bucketName;

    public void putObject(String objectName, InputStream inputStream) {
        try {
            minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName)
                    .stream(inputStream, -1, 10485760).build());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            }
        }
    }

    public String getObject(String objectName) {
        try (InputStream stream = minioClient
                .getObject(GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());) {
            return new String(stream.readAllBytes());
        } catch (ErrorResponseException | InsufficientDataException |
                InternalException | InvalidKeyException | InvalidResponseException |
                IOException | NoSuchAlgorithmException | ServerException |
                XmlParserException | IllegalArgumentException e) {
            e.printStackTrace();
        }
        return "You haven't uploaded anything yet.";
    }
}
package io.werf.werfguidesapp.components; import io.minio.GetObjectArgs; import io.minio.MinioClient; import io.minio.PutObjectArgs; import io.minio.errors.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @Component public class MinioComponent { public MinioComponent() { } @Autowired private MinioClient minioClient; @Value("${minio.bucket-name}") private String bucketName; public void putObject(String objectName, InputStream inputStream) { try { minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName) .stream(inputStream, -1, 10485760).build()); } catch (Exception e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException ioException) { ioException.printStackTrace(); } } } } public String getObject(String objectName) { try (InputStream stream = minioClient .getObject(GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build());) { return new String(stream.readAllBytes()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException | IllegalArgumentException e) { e.printStackTrace(); } return "You haven't uploaded anything yet."; } }

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

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

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

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

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

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

┌ ⛵ image frontend
│ Use cache image for frontend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:4d7fe10a14b3d39d37a747bf016e9e8557c5a6d7061cea9d57f2ffd0-1640809566489
│        id: 4325c3e47f17
│   created: 2022-10-29 23:26:06 +0000 UTC
│      size: 9.6 MiB
└ ⛵ image frontend (1.98 seconds)

┌ ⛵ image backend
│ Use cache image for backend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:08454d39888855040def10d02d3687964e8484eca19b9208a10ab2de-1643016826637
│        id: 6eae976ee732
│   created: 2022-01-24 12:33:46 +0000 UTC
│      size: 243.9 MiB
└ ⛵ image backend (3.26 seconds)

┌ Waiting for release resources to become ready
│ ┌ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                  REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-guide-app                                                                                              2/1                 1                     1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                      ---                                                                            
│ │ ├── guide-app-6c7cbc8b8b-pw7qf               2/2            2                   Running                     Waiting for: replicas 2->1                                                     
│ │ └── guide-app-778cf48fb7-z8jqf               0/2            0                   Init:0/1                    
│ │ 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                                    
│ └ Status progress
│ 
│ ┌ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                  REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-guide-app                                                                                              2/1                 1                     1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                      ---                                                                            
│ │ ├── guide-app-6c7cbc8b8b-pw7qf               2/2            2                   Running                     Waiting for: replicas 2->1                                                     
│ │ └── guide-app-778cf48fb7-z8jqf               0/2            0                   Init:0/1                    
│ │ 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                                    
│ └ Status progress
│ 
│ ┌ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ │ Added `minio` successfully.
│ │ Bucket created successfully `minio/werf-guide-app`.
│ └ deploy/werf-guide-app po/werf-guide-app-778cf48fb7-z8jqf container/setup-minio logs
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                  REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-guide-app                                                                                              2/1                 1                     1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                      ---                                                                            
│ │ ├── guide-app-6c7cbc8b8b-pw7qf               2/2            2                   Running                     Waiting for: replicas 2->1                                                     
│ │ └── guide-app-778cf48fb7-z8jqf               0/2            0                   PodInitializing             
│ │ 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                                    
│ └ Status progress
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                  REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-guide-app                                                                                              2->1/1              1                     1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                                                                                                     
│ │ ├── guide-app-6c7cbc8b8b-pw7qf               2/2            2                   Running -> Terminating      
│ │ └── guide-app-778cf48fb7-z8jqf               2/2            0                   PodInitializing -> 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                                    
│ └ Status progress
└ Waiting for release resources to become ready (41.20 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Mon Jan 24 12:36:55 2022
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 52
TEST SUITE: None
Running time 47.57 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 --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.

назад
далее