Python Flask: взаимодействие с контейнерами Docker
А вам приходила в голову мысль о том, что вам нужно реализовать сервер для обслуживания своих функций? Звучит довольно интересно, может стоит попробовать?
Итак, для этого нужно сделать:
- Создать сервер API, который получает файл изображения;
- Запустить контейнер docker и передать файл изображения в контейнер, и контейнер создаст текстовый файл, созданный из файла изображения;
- Запустить другой контейнер docker и передать текст;
- Сервер API знает, что контейнер знает что контейнер выполнил свою работу, и уведомляет другой сервер (backend, который передает данные пользователям).
Для начала
- pipenv
Pipenv - это инструмент, цель которого - привнести лучшее из мира пакетов (bundler, composer, npm, cargo, yarn и т. д.) в мир Python. Windows - первоклассный гражданин в мире.В самом начале пути знакомства с Python я использовал conda или venv для управления пакетами. Когда я заинтересовался темой управления пакетами, наткнулся на такой инструмент, как npm
, и решил попробовать.
- docker
Docker - платформа, предназначенная для помощи разработчикам в создании, совместном использовании и запуске современных приложений. Мы занимаемся утомительной настройкой, так что вы можете сосредоточится на коде.
- Flask
Flask - это фреймворк веб-приложения, написанный на Python. Он был разработан Армином Ронахером,, который возглавлял команду международных энтузиастов Python под названием Poocco.Чтобы реализовать простой веб-сервер API, я использовалFlask
, существовали и другие библиотеки, такие как Fast API. Так как я привык использовать Flask, я выбрал Flask для сокращения своего рабочего времени.
Это тот процесс, который необходимо реализовать:
- Сервер API получает файл от пользователя;
- Контейнер A обрабатывает файл, а затем сохраняет его в файл B;
- Контейнер B обрабатывает файл B, а затем сохраняет его в файл C;
- Пользователи могут видеть результат через API
Приложения Python и образы Docker
Создадим два изображения и загрузим их в свой репозиторий в docker-hub.
Токенизатор и приложение для подсчета слов.
[Tokenizer]
import sys, json, os, requests
from textblob import TextBlob
def extract_nouns(text):
blob = TextBlob(text)
filtered_tags = list(filter(lambda tag: tag[1] == "NN", blob.tags))
nouns = list(map(lambda tag: tag[0], filtered_tags))
return nouns
def read_file(path):
with open(path) as f:
contents = f.read()
return contents
def save_data(path, data):
with open(path, "w") as f:
json.dump(data, f)
def get_filename_from_path(path):
return os.path.splitext(os.path.basename(path))[0]
def notify_done(url, file_name):
requests.get(f"{url}/docker/tokenizer_done?file_name={file_name}")
if __name__ == "__main__":
if len(sys.argv) < 4:
print("You must pass file path as an argument")
print("python3 main.py [file path to read] [dir to save] [notification api]")
print("Example) python3 main.py ./test.txt ./ http://host.docker.internal:20000")
sys.exit()
api_url = sys.argv[3]
file_path = sys.argv[1]
file_name = get_filename_from_path(file_path)
target_path = os.path.join(sys.argv[2], file_name + ".json")
text = read_file(file_path)
nouns = extract_nouns(text)
save_data(target_path, {"nouns": nouns})
notify_done(api_url, file_name)
print("Done")
[word-counting]
import sys, json, os, requests
def count_word(nouns_list):
count_dict = dict()
for noun in nouns_list:
if noun in count_dict:
count_dict[noun] += 1
else:
count_dict[noun] = 1
return count_dict
def load_data(path):
with open(path) as f:
json_data = json.load(f)
return json_data
def save_data(path, data):
with open(path, "w") as f:
json.dump(data, f)
def get_filename_from_path(path):
return os.path.splitext(os.path.basename(path))[0]
def notify_done(url, file_name):
requests.get(f"{url}/docker/word_count_done?file_name={file_name}")
if __name__ == "__main__":
if len(sys.argv) < 4:
print("You must pass file path as an argument")
print("python3 main.py [file path to read] [dir to save] [notification api]")
print("Example) python3 main.py ./test.txt ./ http://host.docker.internal:20000")
sys.exit()
api_url = sys.argv[3]
file_path = sys.argv[1]
file_name = get_filename_from_path(file_path)
target_path = os.path.join(sys.argv[2], file_name + ".json")
json_data = load_data(file_path)
count_dict = count_word(json_data["nouns"])
save_data(target_path, {"result": count_dict})
notify_done(api_url, file_name)
print("Done")
Для запуска приложений с сервера API создадим оба файла Python с приведенными ниже файлами Docker.
[Tokenizer]
FROM python:3.9
WORKDIR /app
COPY . .
RUN pip install pipenv
RUN pipenv install
RUN pipenv run python3 -m textblob.download_corpora
ENTRYPOINT ["pipenv", "run", "python3", "./main.py"]
[word-counting]
FROM python:3.9
WORKDIR /app
COPY . .
RUN pip install pipenv
RUN pipenv install
ENTRYPOINT ["pipenv", "run", "python3", "./main.py"]
Сервер API
Это основной код, и он довольно прост.
from flask import Flask
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
import routes
Маршруты (routes)
[routes/docker.py]
import os
from flask import jsonify, request
from server import app
from lib import docker, json
result = []
@app.route('/docker/tokenizer_done')
def get_tokenizer_done():
file_name = request.args.get("file_name")
docker.run_word_count_container(file_name)
return "run a word_count container"
@app.route('/docker/word_count_done')
def get_word_count_done():
file_name = request.args.get("file_name")
json_data = json.load_data(
os.path.join(os.getenv("SHARED_VOLUME_PATH"),
"word_count_output",
f"{file_name}.json"
))
result.append(json_data)
return "all works done"
@app.route('/docker/result')
def get_result():
file_name = request.args.get("file_name")
return jsonify({
"result": result
})
[routes/upload.py]
import os
from flask import jsonify, request
from werkzeug.utils import secure_filename
from server import app
from lib import docker
@app.route("/upload", methods=["POST"])
def upload_file():
f = request.files["file"]
file_name = secure_filename(f.filename)
f.save(os.path.join(os.getenv("SHARED_VOLUME_PATH"), "input", file_name))
docker.run_tokenizer_container(file_name)
return "succeed to upload"
[routes/__init__.py]
from routes import docker, upload
[lib/docker.py]
import os
API_URL = os.getenv("API_URL")
VOLUME_ROOT_PATH = os.getenv("SHARED_VOLUME_PATH")
RUN_TOKENIZER_CONTAINER = 'docker run -it --add-host=host.docker.internal:host-gateway -v "' + VOLUME_ROOT_PATH + ':/shared_volume" hskcoder/tokenizer:0.2 /shared_volume/input/{FILE_NAME_WITH_EXTENSION} /shared_volume/tokenizer_output ' + API_URL
RUN_WORD_COUNT_CONTAINER = 'docker run -it --add-host=host.docker.internal:host-gateway -v "' + VOLUME_ROOT_PATH + ':/shared_volume" hskcoder/word_count:0.2 /shared_volume/tokenizer_output/{FILE_NAME_WITHOUT_EXTENSION}.json /shared_volume/word_count_output ' + API_URL
def run_tokenizer_container(file_name):
print(RUN_TOKENIZER_CONTAINER.format(
FILE_NAME_WITH_EXTENSION = file_name
))
os.popen(RUN_TOKENIZER_CONTAINER.format(
FILE_NAME_WITH_EXTENSION = file_name
))
def run_word_count_container(file_name):
os.popen(RUN_WORD_COUNT_CONTAINER.format(
FILE_NAME_WITHOUT_EXTENSION = file_name
))
[iib/json.py]
import json
def load_data(path):
with open(path) as f:
json_data = json.load(f)
return json_data
Это приложение считывает переменные среды из файла .env
, поэтому вам нужно настроить их следующим образом.
Приведенные ниже переменные как раз подходят для моей системы.
API_URL=http://host.docker.internal:20000
ROOT_PATH=C:\Users\hskco\OneDrive\바탕 화면\stuff\docker\api
SHARED_VOLUME_PATH=C:\Users\hskco\OneDrive\바탕 화면\stuff\docker\api\shared_volume
Сервер можно запустить с помощью этого скрипта
python3 -m pipenv run flask run -h 0.0.0.0 --port 20000
Перед выполнением этой команды необходимо установить переменную окружения FLASK_APP
.
Поскольку данный пример разрабатывался в Windows, запустим эту команду в директории api
.
$env:FLASK_APP = './server.py'
Если вы войдете http://127.0.0.1/docker/result
вы увидите эту страницу.
Отправим файл на сервер API и посмотрим на результат.
[1]
[2]
[3]
Заключение
Этот пример - начальный уровень. Это должно было рассматриваться как:
- Авторизация и безопасность (в этом примере сервер API предоставляет все маршруты для общего доступа);
- Связь между контейнерами (использование Restful API для взаимодействия между контейнерами, однако должны быть лучшие способы);
- Управление контейнерами (когда контейнеры будут выполнены, их необходимо удалить. Кроме того, также должна быть необходима балансировка нагрузки. Для этого есть инструменты, такие как Kubernetes);
- Развертывание для производства.
Есть много вещей, о которых не упомянулось в данной статье, и вам нужно подумать (Будем уважать бэкенд-разработчиков). Здесь идет просто сосредоточение на внедрении системы, если попытаться сделать ее совершенной, не получилось бы написать эту статью.