Docker dla amatora danych

Docker dla “amatora” danych – cz. 3 Dostęp z kontenera do plików na przykładzie skryptu Python

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!

Dostęp do plików (bind mounts)

Kolejnym powszechnym scenariuszem jest zapisywanie i odczytywanie danych z i do plików znajdujących się poza kontenerem. Przykładem może być zapis danych pobranych przez aplikację do plików, zapis logów aplikacji do pliku czy też odczytywanie konfiguracji z konkretnego pliku znajdującego się poza kontenerem.

W tym przykładzie kod z poprzedniego postu zostanie zmodyfikowany w taki sposób, aby przekazany do aplikacji parametr, wskazujący ścieżkę do pliku, posłużył jako miejsce docelowe do zapisu danych. Poprzednio dane zostawały zwracane do terminala (konsoli).

import sys
import requests
from bs4 import BeautifulSoup

response = requests.get('https://pl.seequality.net/feed/')

output_file_path = sys.argv[1]

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()))
        with open(output_file_path, "a+") as myfile:
            myfile.write("{0} : {1}\n".format(item.pubdate.get_text(), item.title.get_text()))
else:
    print ('An error has occurred.')

Plik Dockerfile nie ulega zmianie.

# 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"]

Lista dodatkowych bibliotek również nie ulega zmianie.

requests
beautifulsoup4
lxml

Pierwszym krokiem jest zbudowanie obrazu. Sama komenda również nie różni się od tej z poprzedniego przykładu.

docker build --tag='get-seequality-posts-to-file' .

Różnica polega na odpowiednim uruchomieniu kontenera. Kluczową zmianą jest parametr “-v” (“–volume”), który pozwala udostępnić folder z maszyny lokalnej (hosta) do kontenera pod konkretnym aliasem. W tym przypadku “/share/homes/sdrzymala/narnia/data” zostanie udostępnione do kontenera i będzie dostępne z jego poziomu pod aliasem “/app/data”. Należy pamiętać, że jest to zwykły folder znajdujący się na “hoście”. Metoda ta nazywa jest “bind mounts”. Kolejną zmianą w komendzie build jest przekazanie konkretnej ścieżki do pliku, w którym zostaną zapisane dane. Jak widać na zrzucie ekranu, adres pliku odnosi się do aliasu folderu, który został zdefiniowany.

docker run -v /share/homes/sdrzymala/narnia/data:/app/data get-seequality-posts-to-file "/app/data/seequality_posts.txt"
head /share/homes/sdrzymala/narnia/data/seequality_posts.txt

Kontener po uruchomieniu wyświetlił rezultat (posty) w terminalu, natomiast dane te zostały również zapisane do pliku. Mimo, iż jest to chyba najprostszy i najlepszy sposób, istnieje jeszcze kilka metod współpracy z plikami w przypadku Dockera.

Dockerfile VOLUME a DOCKER RUN -V

Inną metodą, która pozwala na operacje pomiędzy Dockerem, a plikami jest możliwość wskazania (utworzenia) wolumenu w pliku Dockerfile korzystając z komendy VOLUME. Plik Dockerfile mógłby wyglądać następująco:

# 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

# Specify container volume
VOLUME ["/data"]

# Define entry point
ENTRYPOINT ["python", "app.py"]

Kod aplikacji również został zmodyfikowany, aby korzystać z konkretnego folderu:

import sys
import requests
from bs4 import BeautifulSoup

response = requests.get('https://pl.seequality.net/feed/')

output_file_path = "/data/seequality_posts.txt"

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()))
        with open(output_file_path, "a+") as myfile:
            myfile.write("{0} : {1}\n".format(item.pubdate.get_text(), item.title.get_text()))
else:
    print ('An error has occurred.')

Lista dodatkowych bibliotek nie ulega zmianie.

requests
beautifulsoup4
lxml

Pierwszym krokiem jest zbudowanie obrazu. Sama komenda również nie różni się od tej z poprzedniego przykładu.

docker build --tag='get-seequality-posts-to-file2' .

W następnym kroku można uruchomić kontener. W tym przypadku nie jest przekazywany żaden parametr, a ścieżka do wolumenu została “hardkodowana” w aplikacji (wartość ta również mogła również zostać przekazana parametrem).

docker run --name="get-to-file-test" get-seequality-posts-to-file2
docker inspect "get-to-file-test"

Druga komenda, czyli “docker inspect <nazwa kontenera>” pozwala na sprawdzenie konfiguracji oraz szczegółów kontenera. Dostarcza ona mnóstwa informacji, o czym być może jeszcze kiedyś będzie, natomiast w kontekście tego wpisu najbardziej kluczowym będzie gałąź “Mounts”.

Znaleźć tutaj można informacje, jakie wolumeny i zasoby zostały dla tego kontenera zamontowane (współdzielone). Na zrzucie ekranu zaznaczono wolumen, który został wyspecyfikowany w pliku Dockerfile. Metoda ta jednak sprawia, że wolumeny są generowane automatycznie i automatycznie nadawana jest im również nazwa.

Różnica polega na tym, że w tym przypadku wolumen zostanie utworzony w ekosystemie Dockera (zwanym docker store), a nie bezpośrednio na komputerze hosta. W praktyce Docker podczas budowania kontenera utworzy nowy folder w swoim “obszarze roboczym” (domyślnie /var/lib/docker), który fizycznie zostanie utworzy pod losową nazwą (GUID) i zmapowany do podanej nazwy. Folder ten będzie zawsze generowany w chwili uruchamiania kontenera. W przypadku uruchomienia dwóch kontenerów z tego samego obrazu zostaną fizycznie utworzone dwa foldery w docker store o dwóch różnych GUID’ach, natomiast zmapowane do tej samej nazwy i z poziomu aplikacji będzie można odwoływać się korzystając z tej samej ścieżki. Warto pamiętać, że nie ma możliwości ustawienia konkretnej nazwy wolumeny.

Jako, iż folder fizycznie znajduje się w strukturach Dockera nie jest możliwe prosty dostęp do jego plików. Korzystając natomiast z wiedzy, że jeden wolumen może być współdzielony dla wielu kontenerów, można w tym celu wykorzystać inny kontener, który zapewni dostęp do terminala, a co za tym idzie, pozwoli podejrzeć dane. W tym przypadku wykorzystano obraz “busybox”, czyli de facto mini dystrybucje linuksa.

docker volume inspect f0e...7b9
docker pull busybox
docker run -it --rm -v=f0e...7b9:/tmp/myvol busybox
cd /tmp/myvol
ls
head seequality_posts.txt
exit

Podczas uruchamiania konteneru mapowany jest utworzony wcześniej wolumen (na podstawie jego nazwy) do konkretnej ścieżki. Następnie zaraz po uruchomieniu kontener uruchamia terminal, gdzie udając się do wcześniej podanej ścieżki, można podejrzeć pliki.

Docker volume

Istnieje również możliwość samodzielnego tworzenia wolumenów za pomocą komendy docker volume. Wolumeny takie będą utworzone w ekosystemie Dockera (docker store) i będą działać dokładnie tak samo, jak zostało to pokazane wcześniej. Przykładowo:

docker volume ls
docker volume create --name='myvolume'
docker volume ls
docker volume inspect myvolume

Powyższy zrzut ekranu prezentuje, że został utworzony nowy wolumen o konkretnej nazwie. Sprawdzenie za pomocą komendy “docker volume inspect” zwraca podobny do poprzedniego rezultat.

Podobnie jak wyżej można również uruchomić kontener korzystając z parametru “-v” (–volume). Warto zaznaczyć, że nie ma możliwości wykorzystania gotowego już obrazu z poziomu pliku Dockerfile. Zatem:

docker run -v myvolume:/app/data get-seequality-posts-to-file "/app/data/seequality_posts.txt"
docker run -it --rm -v=myvolume:/tmp/myvol busybox
cd /tmp/myvol
ls
head seequality_posts.txt
exit

Na początku uruchomiony został kontener z początkowego obrazu (takiego jak w pierwszym przykładzie), natomiast tutaj zamontowany został niekonkretny folder hosta, natomiast volumen Dockera. Korzystając z kontenera “busybox” do którego zamontowany został ten sam wolumen widać, że dane zostały poprawnie zapisane w tym wolumenie.

Dockerfile i komenda ADD

Dodatkowo interakcję z plikami (w pewnym sensie) zapewnia komenda ADD dodana w pliku Dockerfile. Na przykład:

# 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

# Add directory to container
ADD ["/data"]

# Define entry point
ENTRYPOINT ["python", "app.py"]

W tym przypadku natomiast folder wskazany w komendzie ADD zostanie spakowany, następnie przeniesiony oraz rozpakowany do nowego volumeny w kontenerze. Metoda ta służy zatem, raczej do kopiowania skryptów lub innych plików potrzebnych do działania samej aplikacji, aniżeli do zapisu i odczytu danych z i do kontenera.

Docker –volume vs –mount

Należy również wspomnieć, że istnieje jeszcze jedna komenda, która pozwala zarządzać wolumenami oraz udostępnianiem plików. Mowa tutaj o “–mount”. Jak możemy natomiast przeczytać na stronie dokumentacji docs.docker.com/storage/bind-mounts metoda ta nie różni się w zasadzie od komendy “-v” (“–volumes”). Główna różnica opiera się na różnej metodzie zapisu parametrów wolumenu. W przypadku “–volumes” będzie to jeden ciąg tekstu, natomiast “–mount” pozwala na wyspecyfikowanie oddzielnie wszystkich parametrów wolumenu. Mowa tutaj o parametrach, które w poprzednich przykładach jawnie widoczne były po skorzystaniu z komendy “docker volume inspect”. Przykładowo:

docker run -d \
  -it \
  --name nazwa-kontenera \
  --mount type=bind,source=/tmp,target=/usr \
  python

Dodatkową różnicą jest fakt, że w przypadku korzystania z “bind mounts”, czyli montowania i mapowania folderów z komputera hosta, “–volumes” w sytuacji, gdy dany folder nie istnieje, stworzy go za użytkownika, natomiast “–mount” zwróci błąd. Poza tym obie komendy dostarczają takiej samej funkcjonalności. W kwesti szczegółów polecam wcześniej wspomnianą dokumentację.

Podsumowanie

Metod pracy oraz współdzielenia plików do kontenera, pomiędzy kontenerami i pomiędzy kontenerem i “hostem” jest, jak widać, wiele. Różnice pomiędzy nimi szczególnie na początku mogą być niejasne, natomiast każda z nich ma swoje zastosowanie. Warto natomiast pamiętać, że rekomendowaną metodą dostępu i współdzielenia, czy też po prostu pracy z plikami jest korzystanie z wolumenów, a nie współdzielenie pomiędzy kontenerem i “hostem”, a głównie będzie to wynikać z wydajności, to jest szybkości odczytu i zapisu (na korzyść wolumenów).

Uwaga:
Warto pamiętać, że w przypadku korzystania z Windows i Docker Toolbox przed udostępnieniem folderów z systemu operacyjnego (host) należy w pierwszej kolejności udostępnić folder w VirtualBox i zrestartować DockerToolbox

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.

Slawomir Drzymala
Follow me on

1 Comment

Leave a Reply