Przygody z dockerem #3: Tworzenie własnych obrazów

Obrazek użytkownika jsobiecki
Przygody z dockerem #3: Tworzenie własnych obrazów

Wprowadzenia

W poprzedniej części tego cyklu przybliżyłem sposób pracy z kontenerami i gotowymi obrazami. Co jednak zrobić, gdy istniejące obrazy nie spełniają do końca naszych potrzeb?

Czy jesteśmy skazani na żmudną zmianę konfiguracji, instalacje pakietów za każdym razem, gdy korzystamy z takiego „prawie gotowego” obrazu? W poprzednim poście pokazałem polecenia docker commit jako możliwość zapisywania stanu obrazów. Na dłuższą metę nie jest to jednak rozwiązanie wygodne, jak np. śledzić zmiany, które zostały wprowadzone wraz z wykonaniem tego polecenia?

W tym wpisie pokażę sposób budowania obrazów od (prawie) podstaw, jak można je współdzielić w zespole / szerszej społeczności oraz jak można zautomatyzować ten proces przez integrację z serwisem Github.

Dockerfile

Tak jak w przypadku Vagranta, potrzebny jest mechanizm automatycznej konfiguracji obrazu. Jak dla tamtego rozwiązania najpopularniejsze mechanizmy konfiguracji to Puppet lub Chef, tak dla Dockera takim standardowym rozwiązaniem jest plik manifestu — Dockerfile. Na podstawie tego pliku zostanie utworzony i skonfigurowany nasz obraz. Przejdźmy przez przykładowy plik Dockerfile i wyjaśnijmy, na czym to polega.


FROM ubuntu:trusty
MAINTAINER Jarek Sobiecki 

# Install packages (this is generic version with all required extensions).
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
  apt-get -y install supervisor git apache2 libapache2-mod-php5 mysql-server php5-mysql pwgen php-apc php5-mcrypt php5-curl php5-xhprof php5-xdebug php5-memcache php5-gd curl unzip

# Configure open ssh
# See: http://docs.docker.com/examples/running_ssh_service/ for more details
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile
RUN echo 'export PATH="$HOME/.composer/vendor/bin:$PATH"' >> /etc/profile


# Add image configuration and scripts
ADD scripts/start-apache2.sh /start-apache2.sh
ADD scripts/start-selenium.sh /start-selenium.sh
ADD scripts/run.sh /run.sh
RUN chmod 755 /*.sh
ADD configs/php/php.ini /etc/php5/apache2/conf.d/40_custom.ini
ADD configs/php/php.ini /etc/php5/cli/conf.d/40_custom.ini
ADD configs/supervisor/supervisord-apache2.conf /etc/supervisor/conf.d/supervisord-apache2.conf
ADD configs/supervisor/supervisord-sshd.conf /etc/supervisor/conf.d/supervisord-sshd.conf
ADD configs/supervisor/supervisord-selenium.conf /etc/supervisor/conf.d/supervisord-selenium.conf

# config to enable .htaccess
ADD configs/apache/apache_default /etc/apache2/sites-available/000-default.conf
ADD configs/apache/apache_default_ssl /etc/apache2/sites-available/000-default-ssl.conf
RUN a2enmod rewrite
RUN a2enmod ssl

# Install composer and newest stable drush
RUN curl -sS https://getcomposer.org/installer | php -- --filename=composer --install-dir=/usr/local/bin
RUN git clone https://github.com/drush-ops/drush.git /usr/local/src/drush && \
  cd /usr/local/src/drush && \
  git checkout 7.x && \
  ln -s /usr/local/src/drush/drush /usr/bin/drush && \
  composer install


# Install tools required for behat testings
# Firefox
# Selenium
# Xvfb and x11vnc
RUN apt-get -y install xvfb x11vnc firefox openjdk-7-jre openbox
RUN mkdir /usr/local/lib/selenium && curl http://selenium-release.storage.googleapis.com/2.46/selenium-server-standalone-2.46.0.jar -o /usr/local/lib/selenium/selenium.jar


# Install helper tools
RUN apt-get -y install vim


#Enviornment variables to configure php
ENV PHP_UPLOAD_MAX_FILESIZE 10M
ENV PHP_POST_MAX_SIZE 10M

EXPOSE 80 22

# Add volumes for Apache logs
VOLUME  ["/var/log/apache2" ]

CMD ["/run.sh"]


Załączony plik jest autentycznym manifestem, który wykorzystujemy dla jednego z naszych projektów open source - FoodCoop. Przejdźmy zatem krok po kroku i wyjaśnijmy komendy, które są w nim wykorzystywane.

FROM ubuntu:trusty

Ta komenda, od której rozpoczyna się nasz manifest, mówi o tym, że nasz obraz bazuje na obrazie "ubuntu:trusty". Można tutaj pomyśleć o analogi tortu. Bierzemy tort "ubuntu:trusty", każdy kolejny krok będzie do tego ciasta dokładał kolejne warstwy do naszego ciasta.

MAINTAINER Jarek Sobiecki 

Czyli informacja kto jest odpowiedzialny za obraz, nudy :)

ENV DEBIAN_FRONTEND noninteractive

Polecenie ENV pozwala na ustawienie zmiennych środowiskowych, które będą użyte przy kolejnych komendach z pliku Dockerfile. W naszym wypadku ustawiamy DEBIAN_FRONTEND tak, by komenda apt-get którą wykonujemy później, nie próbowała wykorzystać interaktywnego (bazującego na bibliotece ncurses) interfejsu konfiguracyjnego. Dzięki temu proces budowania jest automatyczny, nie interaktywny, dlatego taka próba zablokuje proces instalacji.


RUN apt-get update && \
  apt-get -y install supervisor git apache2 libapache2-mod-php5 mysql-server php5-mysql pwgen php-apc php5-mcrypt php5-curl php5-xhprof php5-xdebug php5-memcache php5-gd curl unzip

To polecenie nakazuje utworzenie kontenera na podstawie obrazu bazowego (i poprzednich kroków), wykonanie polecenia (docker run), potem zatwierdzenia zmian (docker commit), a następnie usunięcie tego kontenera (docker rm). Samo polecenie to lista zwykłych komend, które standardowo możemy wydawać np. w bashu.


ADD scripts/start-apache2.sh /start-apache2.sh

Polecenie ADD jest nieco mylące. Tak naprawdę kopiuje ono plik ze wskazanego położenia na środowisku naszej stacji roboczej, to wybranego katalogu w kontenerze.


EXPOSE 80 22

Te polecenie, mówi o tym, że w przypadku gdy łączymy kontenery ze sobą, bieżący kontener udostępnia innym swoje porty 80 i 22 (czyli HTTP i SSH). By odwołać się do nich z naszego hosta, należy przy docker run użyć opisanej w poprzednim poście opcji -p lub -P. Ten drugi przełącznik, udostępni wszystkie porty oznaczone dyrektywą EXPOSE na naszej lokalnej maszynie. Należy jednak pamiętać, że mapowanie portów nie odbywa się na zasadzie 1-1, porty na hoście będą losowe, np. port 80 z kontenera udostępniony będzie na porcie 32768.


VOLUME  ["/var/log/apache2" ]

W ten sposób katalog /var/log/apache2 zostanie oznaczony wolumenem. Oznacza to, że zmiany w nim nie są śledzone (nie da się ich zatwierdzić). Wolumeny mogą być współdzielone między linkowanymi kontenerami.


CMD ["/run.sh"]

Dyrektywa CMD mówi, że domyślnym programem, uruchamianym wraz z utworzeniem kontenera ma być /run.sh

I na tym kończą się (przynajmniej te podstawowe) dyrektywy potrzebne do zbudowania pliku Dockerfile.

Pełną listę wszystkich poleceń, dostępnych w plikach Dockerfile można znaleźć tu.

Niestety, poktrzeba jest trochę więcej pracy – nie wystarczy zainstalować wszystkie wymagane pakiety, i skopiować odpowiednią konfigurację do odpowiednich miejsc w kontenerze.

Obraz dockera nie jest pełnym systemem operacyjnym, brakuje w nim np. systemu odpowiedzialnego za start usług (np. SystemD / Upstart) – wynika to z faktu, że kontener to tylko (lub aż) grupa odizolowanych procesów. Musimy więc w jakiś sposób upewnić się, że nasze procesy (np. serwery apache2 / openssh) będą uruchomione wraz z kontenerem, co więcej, w przypadku awarii będą reaktywowane.

Narzędziem, które jest pomocne w tym zadaniu, i często wykorzystywane jest w kontenerach dockera jest supervisord - jest to demon, który pilnuje by usługi takie jak sshd czy apache2 były aktywne, a jeżeli procesy odpowiedzialne za te usługi umierają, próbuje uruchomić je ponownie. Szczegółowa informacja jak wykorzystywać supervisord można znaleźć tutaj. Oczywiście nikt nie ogranicza użytkowników, i sposób konfiguracji kontenera zależy już od ich fantazji – można radzić sobie w inny sposób, ale dla naszych zastosowań w zupełności wystarcza.

Niemniej, w przypadku opisanego obrazu użyłem supervisord (korzystając z dokumentacji projektu) oraz kilku skryptów odpowiedzialnych za poszczególne usługi. Po szczegóły zapraszam do kodów źródłowych naszego obrazu.

Kiedy wszystko gotowe, pozostaje wykonać polecenie budowania w katalogu projektu


$ docker build -t [nazwa obrazu]:[tag] [katalog z dockerfile]

Sending build context to Docker daemon 291.8 kB
Sending build context to Docker daemon 
Step 0 : FROM ubuntu:trusty
 ---> b39b81afc8ca
Step 1 : MAINTAINER Jarek Sobiecki 
 ---> Using cache
 ---> e2d7f88c6a9b
Step 2 : ENV DEBIAN_FRONTEND noninteractive
 ---> Using cache
 ---> b10de0795c2c
Step 3 : RUN apt-get update &&   apt-get -y install supervisor git apache2 libapache2-mod-php5 mysql-server php5-mysql pwgen php-apc php5-mcrypt php5-curl php5-xhprof php5-xdebug php5-memcache php5-gd curl unzip
 ---> Using cache
 ---> 7d58d087cffa
Step 4 : RUN apt-get update && apt-get install -y openssh-server
 ---> Using cache
 ---> 95176c32e8c7
Step 5 : RUN mkdir /var/run/sshd
 ---> Using cache
 ---> 00bad0caa3d8
Step 6 : RUN echo 'root:root' | chpasswd
 ---> Using cache
 ---> 79043a4d6391
Step 7 : RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
 ---> Using cache
 ---> 6d5cf5b8e652
Step 8 : RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
 ---> Using cache
 ---> 5cc4ae3d98ea
Step 9 : ENV NOTVISIBLE "in users profile"
 ---> Using cache
 ---> 1055de93a95c
Step 10 : RUN echo "export VISIBLE=now" >> /etc/profile
 ---> Using cache
------CIACH-------------

Po poprawnym wykonaniu tej komendy komenda docker images powinna wyświetlić nasz nowo utworzony obraz. Możemy z nim pracować jak z każdym innym obrazem, ściągniętym z repozytorium dockera.

Publikacja obrazu

Utworzyliśmy nasz obraz, ale jak go udostępnić go szerszej grupie (np. naszemu zespółowi). By to zrobić, należy zarejestrować się na serwerze repozytorium obrazów dockera. Kiedy to zrobimy mamy możliwość tworzenia publicznych i prywatnych repozytoriów z obrazami. Darmowe konto pozwala na utworzenie jednego prywatnego (ukrytego repozytorium) oraz dowolnej liczby publicznych repozytoriów.

Dodajemy nowe repozytorium. Załóźmy, że nasze konto dockera to "test_user", a nowo utworzone repozytorium nazwaliśmy "test_repo". By udostępnić nasz świeżo zbudowany obraz, musimy go "wypchnąć" do repozytorium.

By to zrobić, musimy zbudować obraz tak by był skojarzony z naszym kontem i repozytorium. Wykonujemy zatem polecenie docker build


$ docker build -t test_user/test_repo:1.0 [sciezka do katalogu z dockerfile]

Po poprawnym zakończeniu polecenia wystarczy wypchnąć obraz do serwera.


docker push test_user/test_repo:1.0

I gotowe, nasz obraz zostanie wypchnięty do globalnego repozytorium, czynniąc go tym samym gotowym do redystrybucji.

Integracja z Github

Bardzo przyjemną funkcją repozytorium Dockera jest możliwość integracji repozytorium z repozytorium GitHub, zawierajacym plik Dockerfile (jak np. przykład opisywany w tym poście). By utworzyć obraz, który budowany jest automatycznie, należy skorzystać z opcji połączenia naszego konta z repozytorium GitHub lub Bitbucket. Po połączeniu konta repozytorium z kontem bitbucket / github możemy za pomocą interfejsu dodać repozytorium na jednej z tych dwóch źródeł z kodem źrodłowym naszego obrazu.

W W przypadku zmian w kodzie naszego projektu Docker wykryje je i zbuduje nowy obraz wraz z tagiem "latest".

Umożliwia to efektywną wymianę środowiska pomiedzy członkami zespołu. Gdy wprowadzone zostaną zmiany, wystarczy, że członkowie zespołu wykonają polecenie docker pull na swoich maszynach, a następnie odswieżają swoje kontenery.

Nie jest to idealne rozwiązanie, w przyszłym wpisie przybliżę narzędzie docker-compose, które automatyzuje ten proces.

Źródła

  • http://registry.hub.docker.com/ - rejestr obrazów dockera
  • https://docs.docker.com/articles/using_supervisord/ - jak używać supervisord z dockerem