Docker i Django, czyli węże pod kontrolą w kontenerach

Docker to platforma do uruchamiania aplikacji. Izolowane wewnątrz linuksowych kontenerów aplikacje mogą korzystać z potrzebnych im bibliotek bez ingerencji w system bazowy. Brak wirtualizacji eliminuje w znacznym stopniu narzut wydajnościowy, a poręczny sposób zarządzania kontenerami pozwala używać ich w dewelopmencie jak i na produkcji.

Po co nam ten doker?

Dockera można użyć na kilka różnych sposobów. Może nam dostarczać usługi działające w tle - np. bazę PostgreSQL w jednej z wybranych wersji. Może też zawierać naszą aplikację działającą na produkcji jak i lokalnie. W tym drugim przypadku budujemy własny obraz, wysyłamy do repozytorium (własnego, czy też oficjalnego) i na serwerze tylko te obrazy pobieramy-uruchamiamy.

Programiści Pythona mogą postrzegać Dockera jako taki virtualenv rozszerzony na wszystkie możliwe biblioteki i usługi. Czy to wersja Pythona, serwera baz danych i innych usług wykorzystywanych przez naszą aplikację.

Instalujemy dockera

Docker powinien być w repozytorium większości dystrybucji Linuksa. W przypadku OSX czy MS Windows trzeba poszukać opcji z Dockerem wykorzystującym Virtualboksa z wirtualizowanym Linuksem. Warto też wybrać najnowszą wersję. W przypadku Ubuntu użyłem PPA.

Po instalacji na Linuksie warto dodać swoje konto systemowe do grupy docker by móc używać go bez sudo.

Pierwsze kontenery

Gdy mamy dokera to możemy uruchamiać różne kontenery. Na registry.hub.docker.com znajdziemy sporo gotowych kontenerów, w tym szereg wspieranych oficjalnie, np. Python. Jak to działa? Doker ściągnie lokalnie obraz i skeszuje. Mając obraz możemy tworzyć dowolną liczbę instancji. Taki obraz zawiera minimalistyczny zestaw bibliotek i aplikacji linuksowych by w tym przypadku móc uruchamiać interpreter Pythona. Linuksowe kontenery izolują taki stos oprogramowania i aplikacji od bazowego systemu. Nie wirtualizują w żaden sposób warstwy sprzętowej.

Powiedzmy że chcemy uruchomić sobie interpreter Pythona 3.4. Wystarczy że wykonamy:

docker run -it --rm python:3.4

Za pierwszym razem docker ściągnie sobie parę plików. Gdy wszystko będzie pobrane stworzona zostanie instancja kontenera i w efekcie dostaniemy interpreter Pythona. -it uruchamia instancję w trybie interaktywnym i przypisuje do bieżącej konsoli (tty), a --rm usunie instancję gdy wyjdziemy z interpretera. Na końcu mamy nazwę obrazy python oraz po dwukropku tag, który używany jest jako znacznik wersji (przynajmniej w tym przypadku). Obecnie można użyć Pythona 2.7, 3.3 oraz 3.4.

Zamiast domyślnego zachowania można też użyć kontenera do wykonania własnego polecenia, co może być przydatne szczególnie przy kontenerach działających w tle (np. wykonujemy syncdb w kontenerze aplikacji, który stworzy tabele w kontenerze z postgresem działającym w tle). Żeby wykonać własne polecenie wystarczy dodać je na końcu polecenia, np:

docker run -it --rm python:3.4 ls

Bashowe ls wylistuje zawartość głównego katalogu.

Dockerfile

Plik Dockerfile to przepis na własny obraz kontenera. Sam Python wiele nam nie pomoże. Fajnie by było wrzucić do niego nasz kod i np. wykonywać go przy jego uruchomieniu. Oto prosty przykład:

FROM python:3.4
ADD ./test.py /
RUN ls -al
CMD python test.py

FROM oznacza bazowy kontener, w tym przypadku Pythona 3.4. Następnie możemy podać cały przepis za pomocą komend takich jak ADD, RUN, ENV i innych. RUN wykona polecenie w czasie budowania obrazu, natomiast CMD jest poleceniem odpalanym przy starcie instancji. Docker potrafi też keszować ciąg wykonanych już poleceń (więc np. to polecenie RUN może już się nie wykonać przy kolejnym budowaniu kontenera).

Z konsoli w katalogu z plikiem Dockerfile możemy przystąpić do zbudowania obraz i odpalenia jego instancji:

docker build --tag=foo .
docker run  -it --rm foo

Dockerfile z aplikacją Django

django-ckeditor ma aplikację-demo, którą uruchamiamy za pomocą manage.py i runserver. Zróbmy teraz dla niej plik Dockerfile. Zacznijmy od czegoś prostego i jeszcze nie idealnego:

FROM python:3.4
MAINTAINER Piotr Maliński <riklaunim@gmail.com>
ADD . /ckeditor
ENV DJANGO_SETTINGS_MODULE ckeditor_demo.settings
RUN pip install -r /ckeditor/ckeditor_demo_requirements.txt
RUN pip install /ckeditor
RUN python /ckeditor/manage.py validate
RUN python /ckeditor/manage.py collectstatic --noinput
CMD python /ckeditor/manage.py runserver 0.0.0.0:8080

Biorę Pythona 3.4, cały kod z repozytorium dodaję do katalogi ckeditor, ustawiam zmienną środowiskową DJANGO_SETTINGS_MODULE, instaluję zależności jak i pakiet django-ckeditor, a następnie waliduję całość w Django. CMD uruchomi serwer deweloperski. Co ważne to IP 0.0.0.0 pozwalające wystawić je na zewnątrz kontenera. W tym przypadku przyda się to by wystawić na zewnątrz serwer deweloperski.

Budujemy i uruchamiamy:

dockebuild --tag=django-ckeditor .
dockerun -it --rm  --publish=192.168.0.110:8080:8080 django-ckeditor

--publish pozwala zmapować wystawione na portach usługi na IP naszej maszyny. W tym przypadku 192.168.0.110 to IP maszyny, na której uruchomiłem Dockera. Na porcie 8080 chcę udostępnić to co ten kontener ma na porcie 8080, czyli serwer deweloperski. Po odpaleniu serwera wejście na 192.168.0.110:8080 powinno pokazać nam naszą aplikację.

Całość będzie działać o ile obraz zbudował się z plikiem bazy danych SQLite. Aplikacja oczekuje go w głównym katalogu aplikacji. Jak to rozwiązać lepiej? Moglibyśmy dodać polecenie RUN, które wykonuje syncdb czy migrate z --noinput, co mogłoby posłużyć do stworzenia bazy SQLite. Tylko czy chcemy wiązać plik bazy danych w obrazie aplikacji? Dobra zasada Dockera to oddzielne kontenery na poszczególne usługi. SQLite nie jest tu może najlepszym przykładem, ale Postgres byłby już znacznie lepszym.

Zaczynamy od uruchomienia postgresa:

docker run -d postgres:9.4

W tle działać sobie instancja postgresa. Zobaczymy ją za pomocą docker ps. Dostanie jakąś wygenerowaną nazwę, np. u mnie clever_ptolemy. Wypadałoby stworzyć bazę danych na tym serwerze, ale createdb potrzebować będzie adresu IP. docker inspect NAZWA_INSTANCJI wylistuje informacje o niej, w tym adres IP. Gdy go mamy tworzymy bazę danych:

createdb -h ADRES_IP NAZWA_BAZY -U postgres

Mamy bazę, trzeba ją jakoś przekazać do aplikacji działającej w kontenerze. Robi się to za pomocą zmiennych środowiskowych. W przypadku konfiguracji bazy danych warto zainstalować dj_database_url by w settingsach mieć coś takiego:

from os import environ

import dj_database_url


DATABASES = {'default': dj_database_url.parse(environ.get('DATABASE', 'postgres:///'))}

Czyli łańcuch z konfiguracją bazy danych musimy udostępnić pod zmienną środowiskową DATABASE. Możemy to zrobić przy uruchamianiu kontenera aplikacji:

docker run -it --rm --link=NAZWA_MASZYNY_POSTGRESA:NASZA_NAZWA -e DATABASE=postgres://postgres@NASZA_NAZWA/NAZWA_BAZY --publish=192.168.0.110:8080:8080 django-ckeditor

Nazwę maszyny bierzemy z docker ps, a NASZA_NAZWA to taka etykieta, którą możemy użyć w kolejnych parametrach (-e). U mnie wyglądało to tak:

docker run -it --rm --link=clever_ptolemy:db -e DATABASE=postgres://postgres@db/ckeditor --publish=192.168.0.110:8080:8080 django-ckeditor python /ckeditor/manage.py syncdb 
docker run -it --rm --link=clever_ptolemy:db -e DATABASE=postgres://postgres@db/ckeditor --publish=192.168.0.110:8080:8080 django-ckeditor

Pierwsze polecenie stworzy tabele w bazie danych. Jako że kontener postgresa ciągle działa w tle nie stracimy danych z bazy, gdy zbudujemy i uruchomimy nowy kontener z aplikacją.

Fig

Prosty przykład i to robi się skomplikowane. Żeby było łatwiej i czytelniej stworzono fig. W pliku YAML (fig.yml) podajemy całą taką konfigurację:

ckeditor:
  build: .
  command: python /ckeditor/manage.py runserver 0.0.0.0:8080
  links:
   - db
  ports:
   - "8080:8080"
db:
  image: postgres:9.4

Następnie wykonujemy fig build a następnie fig up i cały system powinien zostać podniesiony (kontener bazy, kontener aplikacji, wzajemne powiązanie). Oczywiście w bazie trzeba będzie stworzyć tabele. Na działającym kontenerze w fig możemy wykonać polecenie za pomocą fig run NAZWA_APLIKACJI POLECENIE, gdzie nazwa aplikacji to nazwa jaką nadaliśmy w fig.yml. Kontenery są widoczne w docker ps i można uzyskać ich adres IP.

Składnia pliku fig.yml opisana jest na stronie projektu. Pozwala m.in. zamontować nasz lokalny kod za pomocą komendy volume - wtedy nie trzeba budować nowego obrazu dla kolejnych zmian. Na stronie figa jest też przewodnik dla Django, w którym wykorzystano standardową konfigurację bazy danych kierując ją na domyślnie działający obraz postgresa.

O dockerze w sieci:

Jak widać podejść do wykorzystania i konfiguracji Dockera jest bardzo wiele.

RkBlog

Django, 8 December 2014

Comment article
Comment article RkBlog main page Search RSS Contact