Środowisko produkcyjne z modelem ML

Naszym zadaniem jest wystawić aplikację w pythonie realizującą zadania REST API. Na ządanie klienta serwer udzieli odpowiedzi na podstawie predykcji wygenerowanej z jakiegoś modelu.

Obraz ten zostanie skonteryzowany z wykorzystaniem pliku Dockerfile dzięki czemu uruchomienie serwera stanie się mozliwe nie zaleznie od platformy.

Nasze zadanie zrealizujemy z wykorzystaniem biblioteki Flask w wersji 3.0.3.

Istnieją inne biblioteki realizujące to zadanie. > Sprawdź samodzielnie w domu czy potrafisz je uzyć, albo przynajmniej przeczytać kod.

  1. FastAPI
  2. Seldon
  3. MLFlow
  4. Node js - Express

Kod minimalnej aplikacji flask

Naszą aplikację chcemy uruchomić lokalnie a potem w łatwy sposób przenieść i wykonać na dowolnym komputerze. Z tej przyczyny naturalnym rozwiązaniem jest zapis kodu do pliku z rozszerzeniem .py.

W celu autozapisu kodu aplikacji do pliku app.py wykorzystamy magiczną komendę %%file plik.py.

%%file app.py
from flask import Flask

# Create a flask
app = Flask(__name__)

# Create an API end point
@app.route('/')
def say_hello():
    return "Hello World"

if __name__ == '__main__':
    app.run() # domyślnie ustawia localhost i port 5000

Uwaga! w dokumentacji Flask w kodzie podstawowej aplikacji nie występują ostatnie dwie liniki uruchamiające serwer.

if __name__ == '__main__':
    app.run()

Ponadto poleceniem uruchamiającym serwer jest flask run a nie python app.py.

Wyjaśnijmy co zawiera przykładowy kod.

  1. from flask import Flask Załadowanie biblioteki
  2. app = Flask(__name__) utworzenie interfejsu serwera API
  3. kod podstrony z wykorzystaniem dekoratora
@app.route('/')
def say_hello():
    return "Hello World"

W celu pokazania jak działa dekorator zdefiniujmy następującą funkcję:

def make_pretty(func):
    def inner():
        print("decorator działa")
        func()
    return inner()

Następnie funkcja testowa

def test():
    print("abc")

make_pretty(test)
decorator działa
abc

Ale mozna rowniez inaczej

@make_pretty
def test2():
    print("test2")
decorator działa
test2
@make_pretty
def test3():
    print("jeszcze cos")
decorator działa
jeszcze cos

Środowisko Python

Aby kod aplikacji app.py mógł zostać uruchomiony potrzebujemy aby na naszym komputerze istniał jakiś interpreter języka Python. Samo posiadanie interpretatora nie jest jeszcze wystarczające dla naszej aplikacji. Do pełnego uruchomienia potrzebujemy wygenerować środowisko (najlepiej wirtualne) w który dostępne będą wszystkie potrzebne biblioteki (np. flask).

uwaga: wszystkie polecenia terminala dotyczyć będą wersji linux/mac os.

W pierwszej kolejności sprawdź czy dostępne są polecenia pozwalające realizować kod pythonowy.

which python
which python3
which pip 
which pip3

Wszystkie te polecenia powinny wskazyać na folder z domyślnym środowiskiem Pythona.

Wygeneruj i uruchom środowisko wirtualne lokalnie wpisując w terminalu:

python3 -m venv .venv
source .venv/bin/activate

Dobra praktyka: środowisko python to nic innego jak katalog. W naszej wersji to katalog ukryty o nazwie .venv. Jeśli skopiujesz ten katalog gdzie indziej przestanie pełnić on swoją funkcję środowiska python. Dlatego jego odtworzenie nie polega na jego kopiowaniu. Jeśli Twój projekt jest powiązany ze środowiskiem kontroli wersji GIT zadbaj aby katalog środowiska nie był dodawany do repozytorium. Mozesz wykonać to działanie dodając odpowiedni wpis do pliki .gitignore

Posiadając utworzone nowe środowisko sprawdź jakie biblioteki się w nim znajdują.

pip list 

Package    Version
---------- -------
pip        23.2.1
pyspark    3.4.1
setuptools 65.5.0

Mozemy ponownie sprawdzić polecenia python i pip

which python
which pip 

Domyślnie powinny pojawić się biblioteki pip oraz setuptools (pyspark pochodzi z naszego wewnętrzengo obrazu).

Doinstaluj bibliotekę flask

pip install flask==3.0.3
pip list 
Package      Version
------------ -------
blinker      1.7.0
click        8.1.7
Flask        3.0.3
itsdangerous 2.1.2
Jinja2       3.1.3
MarkupSafe   2.1.5
pip          23.2.1
pyspark      3.4.1
setuptools   65.5.0
Werkzeug     3.0.2

Jak widać instalacja biblioteki flask wymusiła doinstalowanie równiez innych pakietów.

Jedyną mozliwością przeniesienia środowiska python jest jego ponowna instalacja na nowej maszynie i instalacja wszystkich pakietów. Aby jednak nie instalować kazdego pakietu osobno mozemy wykorzystać plik konfiguracyjny requirements.txt zawierający listę pakietów.

Pamiętaj - kazdy pakiet powinien zawierać nr wersji pakietu. W innym przypadku moze okazać się, ze nowe werjse pakietów spowodują brak obsługi twojego kodu.

Aby utworzyć plik konfiguracyjny uzyj polecenia w terminalu:

pip freeze >> requirements.txt

Tak wygenerowany plik mozesz uzywać na dowolnej maszynie do instalacji i odtworzenia potrzebnego środowiska wykonawczego python.

Dygresja. W momencie przygotowywania materiałów Flask był w wersji 3.0.1 - dziś juz realizowany jest w wersji 3.0.3. Zmiany następują szybciej niz się wydaje.

Instalacja pakietów z pliku odbywa się z wykorzystaniem polecenia:

pip install -r requierements.txt

Mamy teraz dwa pliki: app.py, i requirements.txt. Przenosząc je do dowolnego projektu na serwerach github jesteśmy w stanie uruchomić naszą aplikację wszędzie tam gdzie dostępny będzie interpreter python na którym mozemy utworzyć nowe wirtualne środowisko i zainstalować biblioteki z pliku requirements.txt.

Do pełnej automatyzacji przydałaby się jeszcze mozliwość uruchomienia środowiska python na dowolnej maszynie.

W tym celu utwórz plik Dockerfile:

%%file Dockerfile
FROM python:3.11-slim-buster as builder

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

# Użyj innego obrazu dla obrazu końcowego
FROM python3.11-slim-buster
WORKDIR /app
# Skopiuj zależności z obrazu budującego
COPY --from=builder /app /app

COPY app.py .

ENV FLASK_APP=app

EXPOSE 8000
CMD ["flask", "run", "--host", "0.0.0.0", "--port", "8000"]

Powyzszy plik pozwala w docker desktop uruchomić obraz wykorzystujący podstawowy system operacyjny (tutaj linux) wraz z podstawowym środowiskiem python3.11.

Ponadto plik ten kopiuje potrzebne pliki (app.py, requirements.txt) na obraz dockera.

Polecenie RUN pozwala uruchomić dowolne polecenie bash wewnątrz obrazu dockera.

Polecenie CMD pozwala uruchomić polecenie uruchamiające serwer w trybie tak by nie zamknąć tego polecenia.

Ostatnią informacją jest ustalenie portu na 8000.

# utworzenie kontenera na podstawie pliku Dockerfile
docker build -t modelML .
# uruchomienie kontenera
docker run -p 8000:8000 modelML

Uruchomienie serwera lokalnie

Uruchomienie serwera moze odbyć się na przynajmniej na dwa sposoby.

Uruchomienie serwera przez terminal

python app.py

lub (jeśli nie ma kodu app.run() uruchamiającego serwer.)

flask run 

Powinna pojawić się informacja podobna do ponizszej:

 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Uruchomienie serwera przez notatnik

Bezpośrenie uruchomienia kodu w notatniku spowoduje uruchomienie serwera i zatrzymanie jakiejkolwiek mozliwości realizacji kodu. Aby tego uniknąć mozesz wykorzystać bibliotekę subprocess.

import subporcess

p = subprocess.Popen(["python", "app.py"])

Jeśli potrzebujemy zamknąć subprocess wykonaj:

p.kill()

Posiadając uruchomiony serwer mozesz odpytac serwer wykorzystując:

curl localhost:5000

albo kod w notatniku:

import requests

response = requests.get("http://127.0.0.1:5000/")

print(response.content) # Hello World
print(response.status_code) # 200