Постановка задачи
Начнём с потребности: Я хочу загрузить файл в систему, чтобы потом его обработать. Здесь вроде всё понятно. Дальше формируем требования:
Нужна версионность загруженных файлов.
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
достаточно.
Комментарии (4)
PbIXTOP
00.00.0000 00:00+2Так и не понял зачем эти rewrite, если можно в proxy_pass указывать заменяемый путь https://nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_pass
set $server http://server:8080/; location /server { 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; proxy_http_version 1.1; proxy_set_header Connection ""; chunked_transfer_encoding off; proxy_pass $minio_url; }
Zvava Автор
00.00.0000 00:00С proxy_pass надо быть очень аккуратным.
Конкретно в вашей конфигурации на страничке по ссылке http://192.168.44.76/server/files открывается как http://192.168.44.76/server
Я как-то больше привык к rewrite. Для меня он понятнее работает.
zartdinov
У нас отдельный поддомен просто
Zvava Автор
Да, с поддоменом прокатит.
Но хотелось ещё сделать конфигурацию для разработчиков, чтобы можно было просто по айпишнику обращаться.