Взаимодействие с MinIO по относительному пути. Получится или нет?

от автора

Постановка задачи

Начнём с потребности: Я хочу загрузить файл в систему, чтобы потом его обработать. Здесь вроде всё понятно. Дальше формируем требования:

  • Нужна версионность загруженных файлов.

  • GUI не обязательно, основное взаимодействие будет по API.

  • Файлы доступны другим сервисам по внутренней сети.

  • Загрузка файлов должна быть через относительный URL сервиса (без открытия порта напрямую).

Из OpenSource решили выбрать MinIO: потребность закрывает, с настройками разберёмся в процессе. ?

Разворачиваем

Для теста решения будем использовать простенькое приложение, имитирующее сервер. И на его примере посмотрим сложности реализации. Весь код для самостоятельного тестирования можно взять здесь.

Архитектура

Возьмём для теста простейший http.server из Python, который будет хостить веб страничку для загрузки файлов. В первом приближении получили архитектуру как на картинке ниже.

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

server.py
import http.server import socketserver import os from minio import Minio  PORT = 8080  remote_url = os.getenv('REMOTE_URL') minio_host = os.getenv('MINIO_HOST') minio_port = os.getenv('MINIO_PORT') minio_url = f'{minio_host}:{minio_port}'  minio_user = os.getenv('MINIO_USER') minio_password = os.getenv('MINIO_PASSWORD')  bucket_name = 'uploads'  client = Minio(     minio_url,     access_key=minio_user,     secret_key=minio_password,     secure=False )  if not client.bucket_exists(bucket_name):     client.make_bucket(bucket_name)   class Handler(http.server.SimpleHTTPRequestHandler):     def do_GET(self):         if '/presignedUrl' in self.path:             self.send_response(200, 'OK')             self.send_header('Content-type', 'text/html')             self.end_headers()              filename = self.path.split('=')[1]             original_minio_str = client.presigned_put_object(bucket_name, filename)             self.wfile.write(str.encode(original_minio_str))             return         elif '/files' in self.path:             self.send_response(200, 'OK')             self.send_header('Content-type', 'text/html')             self.end_headers()              files = list(client.list_objects(bucket_name, recursive=True))             files_str = '<br>'.join(                 [f'<p>{file.object_name} &emsp; {client.stat_object(bucket_name, file.object_name).metadata}</p>' for                  file in files])              self.wfile.write(str.encode(files_str))              return         else:             with open('index.html', 'r') as web_page_file:                 web_page = web_page_file.read()              self.send_response(200, 'OK')             self.send_header('Content-type', 'text/html')             self.end_headers()              self.wfile.write(str.encode(web_page))              return   with socketserver.TCPServer(("", PORT), Handler) as httpd:     print("serving at port", PORT)     httpd.serve_forever() 

index.html
 <input type="file" id="selector" multiple> <button onclick="upload()">Upload</button>  <div id="status">No uploads</div>  <script type="text/javascript">   // `upload` iterates through all files selected and invokes a helper function called `retrieveNewURL`.   function upload() {         // Get selected files from the input element.         var files = document.querySelector("#selector").files;         for (var i = 0; i < files.length; i++) {             var file = files[i];             // Retrieve a URL from our server.             retrieveNewURL(file, (file, url) => {                 // Upload the file to the server.                 uploadFile(file, url);             });         }     }      // `retrieveNewURL` accepts the name of the current file and invokes the `/presignedUrl` endpoint to     // generate a pre-signed URL for use in uploading that file:      function retrieveNewURL(file, cb) {         fetch(`/presignedUrl?name=${file.name}`).then((response) => {             response.text().then((url) => {                 cb(file, url);             });         }).catch((e) => {             console.error(e);         });     }      // ``uploadFile` accepts the current filename and the pre-signed URL. It then uses `Fetch API`     // to upload this file to S3 at `play.min.io:9000` using the URL:     function uploadFile(file, url) {         if (document.querySelector('#status').innerText === 'No uploads') {             document.querySelector('#status').innerHTML = '';         }         fetch(url, {             method: 'PUT',             body: file         }).then(() => {             // If multiple files are uploaded, append upload status on the next line.             document.querySelector('#status').innerHTML += `<br>Uploaded ${file.name}.`;         }).catch((e) => {             console.error(e);         });     } </script>

Dockerfile
FROM python:alpine  RUN pip install minio  WORKDIR /server  COPY server.py index.html ./  ENTRYPOINT ["python", "/server/server.py"] 

docker-compose.yml
 version: "3.8"  services:   minio:     image: minio/minio     command: server /data/minio     ports:       - 9000:9000     environment:       - MINIO_ROOT_USER=minioadmin       - MINIO_ROOT_PASSWORD=minioadmin     networks:       - minio-local   server:     container_name: server     ports:       - 8080:8080     build: .     environment:       - MINIO_HOST=192.168.44.76       - MINIO_PORT=9000       - MINIO_USER=minioadmin       - MINIO_PASSWORD=minioadmin     networks:       - minio-local   networks:   minio-local:     driver: bridge

Запуск (не забудьте поменять значение переменной MINIO_HOST на своё).

 docker-compose up -d --build

Проверить загрузку можно по айпишнику и порту в браузере (ниже везде будет http://192.168.44.76, поменяйте на свой)

http://192.168.44.76:8080
http://192.168.44.76:8080

Проверить, что файлы действительно загрузились можно через относительный путь /files, где произойдёт обращение к MinIO и вернётся список файлов с метаданными.

Прячем сервер за Nginx

Сервер спрячем за Nginx по пути /server. MinIO пока оставим на порту 9000.

Просто добавляем nginx в docker-file.yml и монтируем конфиг nginx. Ещё на веб страничке надо поменять путь обращения к серверу для генерации ссылки на загрузку: /presignedUrl -> /server/presignedUrl.

Запускаем, проверяем.

Загрузка и проверка
http://192.168.44.76/server
http://192.168.44.76/server

http://192.168.44.76/server/files
http://192.168.44.76/server/files

Почти у цели

Теперь, собственно, остаётся спрятать MinIO за Nginx и задача решена. Но это часть оказалась самой сложной… Есть несколько issue на гитхабе, которые частично решены, но доступ всё равно через порт. Даже в родных клиентах MinIO при попытке подключения с относительным путём возникает ошибка. Но нам нужна просто ссылка на загрузку файла, поэтому можно «сгенерировать ссылку» во внутренней сети докера, видоизменить её, добавив относительный путь, отдать пользователю, а при загрузке файла на Nginx проделать обратную операцию. Попробуем.

Берём рекомендованные настройки Nginx отсюда, добавляем location /minio-api. Получаем:

nginx.conf
 user  nginx; worker_processes auto; worker_priority -20;  error_log  /var/log/nginx/error.log; pid        /var/run/nginx.pid;  events {     worker_connections  4000; }  http {     server {         listen 80;         resolver 127.0.0.11 valid=5s ipv6=off;          # To allow special characters in headers         ignore_invalid_headers off;         # Allow any size file to be uploaded.         # Set to a value such as 1000m; to restrict file size to a specific value         client_max_body_size 0;         # To disable buffering         proxy_buffering off;          proxy_redirect off;          set $server http://server:8080;          location /server {                 rewrite ^/server(.*) /$1 break;                 proxy_pass $server;         }          set $minio_port 9000;         set $minio_host minio;         set $minio_url http://$minio_host:$minio_port;          location ~* ^/minio-api {            proxy_set_header X-Real-IP $remote_addr;            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header X-Forwarded-Proto $scheme;            proxy_set_header Host $http_host;             proxy_connect_timeout 300;            # Default is HTTP/1, keepalive is only enabled in HTTP/1.1            proxy_http_version 1.1;            proxy_set_header Connection "";            chunked_transfer_encoding off;             proxy_pass $minio_url;         }     } } 

Перезапускаем, пробуем загрузить файл и….. ФИАСКО… Возвращается 403 ошибка.

Сделаем через curl запрос и посмотрим ответ (привёл к читаемому виду):

 curl -X 'PUT' 'http://192.168.44.76/minio-api/uploads/sausage.drawio?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20230214%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230214T124222Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=83d00d24d975f6c6263129d85f9a820b319d78d7d6738513a86429a2441b5f03'  <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>SignatureDoesNotMatch</Code> <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message> <Key>uploads/sausage.drawio</Key> <BucketName>minio-api</BucketName> <Resource>/minio-api/uploads/sausage.drawio</Resource> <RequestId>1743B1AB6535B5E1</RequestId> <HostId>e96154ee-0e6d-422b-8102-feea5d741fae</HostId> </Error>

Видим ошибку, что забыли добавить rewrite, чтобы убрать minio-api из пути, добавляем, перезапускаем.

rewrite ^/minio-api(.*) $1 break;

Загружаем файл, проверяем на странице с файлами — информация обновилась.

Итого

Нам удалось спрятать сервисы за Nginx без открытия портов напрямую, а пользователю отдавать ссылку, по которой можно грузить файлы.

А что там с HTTPS? Всё будет корректно работать, если осуществить SSL Termination на Nginx. Плюс в том, что это не надо делать отдельно для minIO. Единственное, надо менять в ссылке для пользователя http на https. Оставлю это для самостоятельной работы.

Код файлов можно найти тут.

PS

Во время написания статьи мне удалось всё структурировать и самый «костыль», ради которого я и хотел написать статью, ушёл… В ходе моего тестирования у меня не работала загрузка файлов без переделки http загловка Host на значение minio:9000. Оказалось, что обычного rewrite достаточно.


ссылка на оригинал статьи https://habr.com/ru/articles/716910/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *