Wstęp
Kontenery to temat na pewno nie nowy, natomiast w ostatnich latach bijący chyba rekordy popularności. Podczas tej rosnącej fali ja również postanowiłem sprawdzić, z czym to się je i w czym Docker może pomóc mi. W niniejszym serii postów chciałbym podzielić się z własnymi doświadczeniami, notatkami, fragmentami kodów oraz pokazać, w jaki sposób można wykorzystać Docker’a. Będzie to Docker z perspektywy osoby, która pracuje z danymi. Począwszy od ich gromadzenia, po ich przechowywanie, aż po ich przetwarzanie. Na pewno nie zabraknie samego Docker’a, SQL Server’a i Python’a przedstawionych w jak najbardziej praktyczny sposób. Zapraszam!
Uruchomienie skryptu Python w Dockerze
Dzisiejszy artykuł będzie dotyczył uruchamiania skryptów napisanych w Pythonie z wykorzystaniem Dockera. Postaram się pokazać kilka różnych metod w jaki sposób można “konteneryzować”. Przykłady będą dość proste, natomiast powinny pokazać schemat postępowania.
Pierwsze przykłady to uruchomienie skryptu korzystającego z oficjalnego i niezmodyfikowanego obrazu Pythona. Jest to chyba najprostsza metoda, natomiast będzie się tylko sprawdzać w najmniej skomplikowanych skryptach, które korzystają wyłącznie z modułów, które dostępne są w podstawowym Pythonie. Co prawda nawet z tym problemem można sobie poradzić, natomiast w bardziej skomplikowanych skryptach na pewno bardziej sprawdzi się druga metoda, czyli przygotowanie własnego obrazu. Niemniej jednak w prostych skryptach nie ma potrzeby za każdym razem tworzyć nowy obraz.
Do zobrazowania wykorzystane zostaną dwa trywialne skrypty, które wyświetlą tekst “Hello world” oraz bieżącą datę i godzinę korzystając ze standardowego modułu datetime.
#script1 import datetime print ("{} | Hello world!".format(datetime.datetime.now()))
#script2 import datetime print ("{} | Hello world again!".format(datetime.datetime.now()))
Aby uruchomić skrypt wystarczy utworzyć (uruchomić) kontener korzystając z obrazu Python oraz, po dodaniu wolumenu do kontenera, odwołać się do konkretnego skryptu w chwili uruchamiania kontenera. O wolumenach będzie bardziej szczegółowo w innym poście, natomiast w skrócie wolumen jest to folder, który zostanie udostępniony (zamontowany) do kontenera i który będzie potem zmapowany do konkretnego folderu w kontenerze, np. “”-v $PWD”:/usr/src/app” udostępni i zmapuje bieżący folder, w którym znajduje się użytkownik, do ścieżki “/usr/src/app”. Parametr “–it” przygotuje natomiast terminal i będzie aktywnie oczekiwał na połączenie, dzięki czemu możliwe będzie uruchomienie skryptu z tego poziomu.
docker container ls --all --format "{{.Names}}" pwd ls docker run -it -v "$PWD":/usr/src/app python:latest python /usr/src/app/script1.py docker run -it -v "$PWD":/usr/src/app python:latest python /usr/src/app/script2.py docker container ls --all --format "{{.Names}}"
Jak widać, każda z komend utworzyła kontener i uruchomiła wskazany skrypt oraz po jego zakończeniu kontener został zatrzymany.
Inną metodą może być skorzystanie z komendy “docker exec”, która została opisana w poprzednim poście, i uruchamianie skryptów na kontenerze, który jest już uruchomiony. W pierwszej kolejności tworzony zatem jest jeden “główny” kontener, do którego udostępniony (zamontowany) zostaje folder z wszystkimi skryptami. Folder jest zamontowany, a nie kopiowany, więc w przypadku nowego skryptu nie trzeba modyfikować kontenera i bez problemu można uruchamiać nowe skrypty. W następnym kroku wykorzystując wspomnianą komendę “docker exec” następuje połączenie do terminala oraz uruchomienie konkretnego skryptu.
Na powyższym zrzucie widać, że istnieje tylko jeden “główny” kontener, dzięki któremu zostały uruchomione oba skrypty. Jak wspomniano wyżej, w przypadku prostych skryptów, jedna z tych metod może się sprawdzić, natomiast w chwili w której potrzebne jest wykorzystanie innych pakietów i modułów właściwym rozwiązaniem będzie przygotowanie własnego obrazu, który zostanie odpowiedni skonfigurowany i do którego zostaną doinstalowane wszystkie niezbędne zależności.
Dockerfile – na przykladzie Python
W realnym świecie skrypty będą zwykle znacznie bardziej skomplikowane. Będą wymagały dodatkowych pakietów i być może sam system będzie musiał być w jakiś sposób przygotowany przed uruchomieniem samego skryptu lub aplikacji. W kwestii przypomnienia, kontenery uruchamiane są na kernelu systemu operacyjnego hosta, a kernel posiada tylko niezbędne elementy systemowe oraz elementy potrzebne do uruchomienia konteneru, natomiast nie zawiera np. zainstalowanego środowiska Python. Dlatego też w przypadku poprzednich skryptów wykorzystywany był obraz Python, który dostarczał to środowisko. Kontenery są jednak budowane w ten sposób, aby dostarczały tylko i wyłącznie to, co jest niezbędne i w przypadku obrazu Python dostępne jest samo środowisko oraz wyłącznie podstawowe pakiety. W chwili, gdy istnieje potrzeba wykorzystania innych bibliotek należy przygotować swój własny obraz, w którym można wskazać, które pakiety powinny zostać zainstalowane, ewentualnie w jaki sposób powinno zostać skonfigurowane samo środowisko kontenera. Oczywiście Istnieje również możliwość znalezienia w repozytorium (np. hub.docker.com) odpowiedniego obrazu i jego wykorzystanie. Jeżeli obraz posiadałby już zainstalowane wszystkie pakiety, które są potrzebne, istniałaby możliwość uruchomienia skryptu tak jak w poprzednim przykładzie na gotowym obrazie. W repozytorium znajduje się bardzo dużo obrazów, które będą wspierać popularne aplikacje jak np. Django. Oczywiście tutaj jest mowa o Pythonie, ale Docker wspiera znacznie więcej języków/środowisk/aplikacji. Zasada działania jednak będzie jednak zawsze taka sama. W tym przykładzie pokazane zostanie, w jaki sposób przygotować własny obraz do uruchomienia dedykowanego skryptu w Python.
Jako przykład zostanie wykorzystany trochę bardziej skomplikowany skrypt w stosunku do tego poprzedniego. Plik “app.py” prezentuje się następująco:
import requests from bs4 import BeautifulSoup response = requests.get('https://pl.seequality.net/feed/') if response.status_code == 200: feed = BeautifulSoup(response.content, features="lxml") for item in feed.find_all("item"): print ("{0} : {1}".format(item.pubdate.get_text(), item.title.get_text())) else: print ('An error has occurred.')
Zasada działania jest stosunkowo prosta. Korzystając z bibliotek “requests” oraz “BeautifulSoup” pobierana jest lista ostatnich postów z czytnika RSS niniejszego bloga. Następnie ,w przypadku, gdy strona zwróciła prawidłowy rezultat (kod o numerze 200), tytuły postów oraz data ich dodania zwracane są do terminala. Do działania tej aplikacji niezbędne jest wykorzystanie dodatkowych bibliotek, które zostały wymienione w pliku “requirements”
requests beautifulsoup4 lxml
W chwili budowy kontenera powyższe biblioteki powinny zostać pobrane i doinstalowane. Można to osiągnąć, przygotowując odpowiedni plik Dockerfile. Dockerfile jest to swego rodzaju plik konfiguracyjny obrazu. Zawiera on wszelkie komendy, które powinny zostać uruchomione, aby kontener działał tak, jak sobie tego życzymy. Poniżej DockerfIle dla powyższego skryptu:
Kod:
# Use an official Python runtime as a parent image FROM python:latest # Set the working directory to /app WORKDIR /app # Copy the current directory contents into the container at /app COPY . /app # Install any needed packages specified in requirements.txt RUN pip install --trusted-host pypi.python.org -r requirements.txt # Make port 80 available to the world outside this container EXPOSE 80 # Define environment variable ENV NAME World # Define entry point ENTRYPOINT ["python", "app.py"]
Gdzie:
- WORKDIR – folder roboczy aplikacji
- COPY – kopiowanie plików aplikacji do folderu roboczego
- RUN pip install – instalowanie pakietów i zależności (z dodatkowego pliku tekstowego)
- EXPOSE – otwarcie portu numer 80, szczególnie przydatne podczas aplikacji webowych jak np. Flask
- ENV – nazwa środowiska
- ENTRYPOINT – uruchomienie konkretnego skryptu w chwili uruchomienia kontenera (uruchomienia kontenera, a nie podczas budowania obrazu)
Tak więc powyższy skrypt przygotuje folder roboczy, skopiuje do niego pliki aplikacji, następnie zainstaluje wszystkie niezbędne dodatkowe pakiety, ustawi port oraz ustawi plik (skrypt), które powinien się uruchomić w chwili startu kontenera.
Uwaga
Powyższy kod działa prawidłowo, natomiast autor postu pythonspeed.com/articles/dockerizing-python-is-hard porusza szereg kwestii i wytyka częste błędy popełniane w Dockerfile. Po zapoznaniu się z artykułem powyższy przykład okaże się przykładem nieidealnym, natomiast, jako że ten artykuł jest przeznaczony raczej dla początkujących i nie chciałbym bardziej komplikować prostych przykładów, zostawię go w takiej formie. Na początku i szczególnie podczas nauki nie będzie to tak bardzo istotne, natomiast bardzo zachęcam do pogłębienia tematu (być może pojawi się na ten tematu artykuł również tutaj).
Docker build – na przykładzie Python
Obraz tworzony jest z wykorzystaniem komendy “docker build”. Parametr “tag” posłuży do nadania konkretnej nazwy dla obrazu, natomiast kropka znajdująca się na końcu polecania świadczy o tym, że obraz powinien zostać utworzony z plików, które znajdują się w bieżącym folderu (oczywiście ścieżka może być dowolna). Docker sam rozpozna typy plików oraz zależności i z wykorzystaniem pliku “Dockerfile” i przygotuje obraz.
docker build --tag='get-seequality-posts' .
Jak widać na powyższym zrzucie ekranu podczas budowania obrazu zostały między innymi pobrane biblioteki wskazane w pliku “requirements”, a samo polecenie budowania zakończyło się sukcesem. Korzystając z komendy “docker image ls” można zauważyć, że na liście dostępnych obrazów pojawił się nowy, o wcześniej zdefiniowanej nazwie.
docker image ls
Póki co natomiast utworzony został wyłącznie obraz, więc lista kontenerów nadal nie ulegnie zmianie.
docker container ls -a --format="{{.ID}} {{.Names}}"
Uruchomienie kontenera odbywa się tak jak wcześniej, za pomocą polecenia “docker run” oraz wyspecyfikowaniu jego nazwy (tagu) lub ID.
docker run get-seequalitty-posts
Po uruchomieniu kontenera skrypt został uruchomiony, a terminal zwrócił pożądane rezultaty. Wyświetlając jeszcze raz listę bieżących kontenerów, można zauważyć nowy wpis. Nazwa kontenera nie została określona, więc Docker sam nadał losową nazwę – “romantic_leakey”.
docker container ls -a --format="{{.ID}} \t {{.Names}} \t {{.Status}}"
Ponadto utworzony kontener posiada status “Exited”. Aplikacja, która została uruchomiona, wykonała wszystkie polecania (skrypt) i się zakończyła, zakańczając jednocześnie życie samego kontenera.
Zakończenie
Kontenery i Docker dla “amatora danych” mogą okazać się bardzo przydatne, nawet do zastosować w domu (nieprodukcyjnych). Osobiście aplikacje piszę najpierw na komputerze (zwykle język Python), następnie w chwili gdy już są przetestowane oraz sprawnie działają przygotowuję obraz w Dockerze dla takiej aplikacji i następnie uruchamiam aplikację w kontenerze w NAS albo na VPS, który można stosunkowo tanio znaleźć w sieci. Według mnie ułatwia to pracę osoby, która pracuje (hobbystycznie, ale nie tylko) z danymi pomijając już kwestię bardzo szerokiego wykorzystania Dockera w zastosowaniach czysto projektowych i cyklu rozwijania różnego rodzaju aplikacji. Oczywiście jest to tylko wstęp do tej tematyki, a kolejne tematy zostaną poruszone już wkrótce.
Uwaga
Nie jestem z całą pewnością ekspertem w zakresie Dockera oraz Pythona, a wpis ten traktuję bardziej jako zapis moich notatek i prób, które być może przydadzą się również Tobie. W przypadku jakichkolwiek błędów i usprawnień bardzo zapraszam do komentowania lub kontaktu.
- Docker dla amatora danych – Tworzenie środowiska (VM) w Azure i VirtualBox (skrypt) - April 20, 2020
- Power-up your BI project with PowerApps – materiały - February 5, 2020
- Docker dla “amatora” danych – kod źródłowy do prezentacji - November 18, 2019
Last comments