Постановка задачи
Начнём с потребности: Я хочу загрузить файл в систему, чтобы потом его обработать. Здесь вроде всё понятно. Дальше формируем требования:
-
Нужна версионность загруженных файлов.
-
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}   {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, поменяйте на свой)

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

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

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


Почти у цели
Теперь, собственно, остаётся спрятать 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/
Добавить комментарий