Blog » Linux

Docker w Linuxie cz. II – średnio zaawansowane komendy

Docker w Linuxie

W poprzedniej części naszego cyklu nauczyliśmy się instalować silnik Dockera (Docker Engine) w wersji community. Dowiedzieliśmy się również, jak pobrać, uruchomić i usunąć kontener oraz jego obraz. Dziś będziemy używać obrazu opartego na EuroLinux 7 wraz z repozytorium FBI. Więcej na temat powszechnie dostępnego repozytorium, które udostępniamy nieodpłatnie bez jakichkolwiek ograniczeń, można znaleźć w artykule o FBI w EuroLinuxie.

Podstawy wersjonowania w Dockerze – czym są tagi i warstwy?

Podstawową jednostką budującą współczesne kontenery zgodne ze standardem OCI (Open Container Initiative) są metadane oraz warstwy. Z technicznego punktu widzenia warstwa jest archiwum tar zawierającym zmiany pomiędzy kolejnymi wersjami kontenera. W przypadku prostego budowania obrazu każda dyrektywa jest nową warstwą. Nowsze narzędzia pozwalają jednak na zebranie wielu dyrektyw budujących w jedną warstwę. Temat alternatywnych narzędzi budowania zostanie jednak omówiony dopiero za 2 artykuły. Nowa warstwa jest też budowana przy zapisywaniu stanu (ang. commit tłumaczony też dosłownie jako popełnianie lub tworzenie migawki. Jest ono jednak często używane w przypadku tzw. snapshotów [ang. migawka], co może jednak wprowadzać jeszcze większy bałagan w pojęciach) kontenera przez użytkownika.

Na dodatek w kontenerach zastosowany jest mechanizm CoW (ang. copy-on-write). Pozwala to na użytkowanie jednego obrazu przez wiele kontenerów. Mechanizm CoW jest powszechnie wykorzystywany w informatyce, gdyż pozwala z jednej strony zaoszczędzić pamięć (zarówno operacyjną [RAM], jak i dyskową), z drugiej jest też często wielokrotnie szybszy.

Za zapis i przetrzymywanie poszczególnych warstw opowiada sterownik pamięci masowej (ang. storage driver). W starszych implementacjach Dockera używany był AUFS. W nowszych jest to OverlayFS w wersji 2. Główną ideą tych rozwiązań jest trzymanie zmian w plikach, względem oryginalnego obrazu przy zachowaniu jak najlepszych osiągów. Ponieważ dyskusja i szczegóły techniczne znacznie wybiegają poza zakres artykułu, odsyłam Państwa do dokumentacji Dockera (oczywiście po angielsku):

Docker Doc: Use the OverlayFS storage driver

Docker Doc: Select a storage driver.

Wiedząc już, że warstwy są nakładanymi na siebie zmianami w kontenerze oraz znając podstawowe zagadnienia z nimi związane, można, parafrazując sympatycznego zielonego Ogra, podsumować:

„Warstwy! Cebula ma warstwy! Kontenery mają warstwy! Cebula ma warstwy, dociera? – kontenery mają warstwy… aaaach!”

Po krótkiej dawce teorii stwórzmy teraz obraz EuroLinuxowy, który będzie miał zainstalowany serwer www (httpd). Stworzymy dla niego kolejną warstwę, którą zapiszemy oraz otagujemy.

Na samym początku zaciągnijmy obraz z platformy Docker Hub.

[Alex@Normandy Docker]$  docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
[Alex@Normandy Docker]$ docker pull eurolinux/eurolinux-7
Using default tag: latest
latest: Pulling from eurolinux/eurolinux-7
e3693a234614: Pull complete 
Digest: sha256:5447be149a8c283a67fb49977c4dacc84d6a23e01bae03057fb3d077ffbf798b
Status: Downloaded newer image for eurolinux/eurolinux-7:latest
docker.io/eurolinux/eurolinux-7:latest

Następnie uruchommy kontener i zainstalujmy w nim serwer Apache (httpd). W celu skrócenia wyjścia stdout programu yum zostało przekierowane na /dev/null. Zwróćmy też uwagę na nadanie nazwy kontenerowi jako my_httpd.

[Alex@Normandy Docker]$ docker run -it --name my_httpd eurolinux/eurolinux-7
eurolinux/eurolinux-7         eurolinux/eurolinux-7:latest  
[Alex@Normandy Docker]$ docker run -it --name my_httpd eurolinux/eurolinux-7
[root@1c81d8d92282 /]# yum install -y httpd >/dev/null
warning: /var/cache/yum/x86_64/7/fbi/packages/apr-util-1.5.2-6.el7.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID 18cd4a9e: NOKEY
Importing GPG key 0x18CD4A9E:
 Userid     : "EuroLinux (7) <euro@euro-linux.com>"
 Fingerprint: 2332 b393 7b50 49e5 6415 c200 75c3 33f4 18cd 4a9e
 Package    : el-release-7.7-1.el7.x86_64 (@el-base)
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-eurolinux7

Wystarczy teraz wyjść z kontenera oraz wykonać na nim commit. Zauważmy, że po wyjściu kontener automatycznie przestaje działać. Wynika to z faktu, iż punktem wejścia (domyślnie wykonywaną komendą) do kontenera jest powłoka Bash. Całą procedurę zapisania nowego obrazu kontenera wraz z tagowaniem pokazuje poniższy przykład:

[Alex@Normandy Docker]$ docker ps # nie pokaże nam naszego nieuruchomionego kontenera
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[Alex@Normandy Docker]$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS                          PORTS               NAMES
1c81d8d92282        eurolinux/eurolinux-7   "/bin/bash"         10 minutes ago      Exited (0) About a minute ago                       my_httpd
[Alex@Normandy Docker]$ docker commit my_httpd # Zapisujemy obecny stan kontenera
sha256:b03b612c306d42edb56dec5ea7cbe890d53c45eb3c071aaafeb2e1eada7fcad2
[Alex@Normandy Docker]$ docker images # Jak widać obraz nie ma ani tagu ani repozytorium
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
                                b03b612c306d        9 seconds ago       208MB
eurolinux/eurolinux-7   latest              3eed865a31cf        4 weeks ago         168MB
[Alex@Normandy Docker]$ docker tag b03b612c306d my_httpd # domyślnie zostanie utworzony tag latest
[Alex@Normandy Docker]$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
my_httpd                latest              b03b612c306d        21 seconds ago      208MB
eurolinux/eurolinux-7   latest              3eed865a31cf        4 weeks ago         168MB
[Alex@Normandy Docker]$ docker tag my_httpd:latest my_httpd:v1 # dodajemy tag v1
[Alex@Normandy Docker]$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
my_httpd                latest              b03b612c306d        11 minutes ago      208MB
my_httpd                v1                  b03b612c306d        11 minutes ago      208MB
eurolinux/eurolinux-7   latest              3eed865a31cf        4 weeks ago         168MB

Kolejny przykład prezentuje, iż usunięcie tagu latest nie oznacza usunięcia tagu v1:

[Alex@Normandy Docker]$ docker rmi my_httpd:latest
Untagged: my_httpd:latest
[Alex@Normandy Docker]$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
my_httpd                v1                  b03b612c306d        12 minutes ago      208MB
eurolinux/eurolinux-7   latest              3eed865a31cf        4 weeks ago         168MB

Co prawda zbudowany w ten sposób obraz jest w pełni funkcjonalny, ale ma on jednak jedną poważną wadę – nie buduje się „automagicznie”. Co więcej, wciąż domyślnym punktem wejścia (programem uruchamianym domyślnie) jest powłoka bash. Więcej na temat sprawnego i funkcjonalnego budowania obrazów kontenerowych powiemy w następnej części naszego poradnika.

Wystawianie portu usługi

Mając już przygotowany kontener z serwerem WWW, możemy pozwolić sobie na jego przetestowanie. W tym celu proponuję na samym początku sprawdzić jego wewnętrzny adres IP, a następnie odpowiedź serwera.

Uruchomienie kontenera w tle (daemon przełącznik -d) z nadaniem nazwy httpdv1 oraz uruchomieniem polecania /sbin/httpd z opcją -DFOREGROUND.

[Alex@Normandy Docker]$ docker run --name httpdv1 -d my_httpd:v1 '/sbin/httpd' '-DFOREGROUND'
57ea7edcfe33d97120268cf732803be098f8c892dea86dc125f94dd545da7336
[Alex@Normandy Docker]$  docker

By znaleźć IP, czasami możemy wykonać którąś z ogólnodostępnych komend, takich jak ip, ifconfig, hostname -I. Niestety nie zawsze będą one dostępne. Tak jest między innymi w przypadku EuroLinuxowych kontenerów, które posiadają tylko niezbędne oprogramowanie. W takim wypadku pomocna okazuje się komenda docker inspect, służąca do wyświetlania niskopoziomowych danych na temat zadanego obiektu (nie musi to być kontener). Poniżej przykładowa próba znalezienia adresu IP kontenera:

[Alex@Normandy docker-II]$ docker exec -i -t httpdv1 /bin/bash
[root@53168e4ee170 /]# ip a
bash: ip: command not found
[root@53168e4ee170 /]# ifconfig
bash: ifconfig: command not found
[root@53168e4ee170 /]# hostname -I
bash: hostname: command not found
[root@53168e4ee170 /]# exit
[Alex@Normandy docker-II]$ docker inspect httpdv1 | grep -i ip | grep -i addr
            "LinkLocalIPv6Address": "",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "GlobalIPv6Address": "",
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
                    "GlobalIPv6Address": "",
[Alex@Normandy docker-II]$

Po znalezieniu adresu IP możemy przejść do następnej czynności, czyli przetestowania, czy zapakowany w kontener serwer www rzeczywiście nasłuchuje na odpowiednim porcie. W tym celu można użyć przeglądarki lub prostego cURLa.

[Alex@Normandy docker­II]$ curl  ­s 172.17.0.2:80 | head ­5
<!DOCTYPE html PUBLIC "­//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1
1/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Test Page for the Apache HTTP Server on EuroLinux</title>

W przypadku uruchomionego kontenera nie ma prostej możliwości przypisania portu kontenera do portu hosta. Można oczywiście tego dokonać przy pomocy dodania odpowiedniego przekazywania pakietów z wykorzystaniem frameworka Netfilter (wysokopoziomowym interfejsem do Netfilter jest zarówno firewalld, jak i iptables), ale rozwiązanie takie jest bardzo trudne do utrzymania. W związku z tym faktem najlepiej jest zatrzymać i usunąć nasz dotychczasowy kontener.

[Alex@Normandy docker-II]$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
53168e4ee170        my_httpd:v1         "/sbin/httpd -DFOREG…"   33 minutes ago      Up 33 minutes                           httpdv1
[Alex@Normandy docker-II]$ docker stop httpdv1 ; docker rm httpdv1
httpdv1
httpdv1
[Alex@Normandy docker-II]$

Komenda docker run posiada przełącznik --publish (skrótowo -p) odpowiadający za przypisanie portu kontenera do portu hosta. Przykładowy start kontenera o nazwie httpdv2 z przypisaniem portu 80 kontenera do 8080 hosta, z obrazu my_httpd i startem komendy /sbin/httpd z argumentem -DFOREGROUND. Następnie występuje test pobrania strony.

[Alex@Normandy docker-II]$ docker run --name httpdv2 -d -p 8080:80 my_httpd:v1 '/sbin/httpd' '-DFOREGROUND'
3baf52e6f35b8d1e43348f001f04dc1214f277c5d895392cad75afaca5b7a7c2
[Alex@Normandy docker-II]$ curl -s localhost:8080 | head -1

Montowanie katalogu

Jedną z najważniejszych cech kontenerów jest in tymczasowość. Najłatwiej konteneryzuje się więc usługi, które są bezstanowe. Jak nietrudno się domyślić, jest to tylko niewielki wycinek. Większość usług potrzebuje bowiem gdzieś trzymać swój stan, np. przy pomocy bazy danych. W takim wypadku kontener z reguły montuje odpowiedni wolumen danych (najczęściej katalog z kontenera) do odpowiedniego katalogu na hoście (katalog ten może być np. sieciowym systemem plików).

Docker posiada dwie opcje służące do montowania zasobów dyskowych. Pierwszą z nich jest -v (--volume), drugą --mount. Kiedyś różniły się one ze względu na tryb pracy Dockera (mount był używany w przypadku docker swarm [ang. rój], czyli orkiestracji kontenerami). Dzisiaj można w większości przypadków stosować je zamiennie. Istotną różnicą jest fakt, iż -v automatycznie utworzy katalog, do którego zasoby kontenera mają zostać zamontowane. W przypadku braku odpowiedniego katalogu komenda docker run z opcją mount wyjdzie z błędem i poinformuje użytkownika o braku odpowiedniego folderu.

Poniżej stworzenie trzeciej wersji naszego kontenera, który zaciągnie stronę domyślną (index.html) z lokalnego katalogu:

mkdir pages
echo 'Hello World' > pages/index.html
[Alex@Normandy docker-II]$ docker run --name httpdv3 -d -p 8080:80 -v $(pwd)/pages:/var/www/html my_httpd:v1 '/sbin/httpd' '-DFOREGROUND'
16478c3cd9e4094804ff2f4f2dda5709c80ac603b4fb302226ae4a8a5da7eb93
[Alex@Normandy docker-II]$ curl localhost:8080
Hello World!

Należy pamiętać, iż katalog, który chcemy zamontować, musi być ścieżką bezwzględną. W innym wypadku Docker zwróci błąd.

[Alex@Normandy docker-II]$ docker run --name httpdv3 -d -p 8080:80 -v ./pages:/var/www/html my_httpd:v1 '/sbin/httpd' '-DFOREGROUND'
docker: Error response from daemon: create ./pages: "./pages" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path.
See 'docker run --help'.
[Alex@Normandy docker-II]$

Warto sprawdzić, czy połączenie z kontenerem działa w dwie strony. W celu wykonania prostego testu posłużymy się komendą docker exec wraz z touch.

[Alex@Normandy docker-II]$ ll pages/
total 4
-rw-r--r--. 1 Alex Alex 13 Nov  2 18:33 index.html
[Alex@Normandy docker-II]$ docker exec httpdv3 'touch' '/var//www/html/test'
[Alex@Normandy docker-II]$ ll pages/
total 4
-rw-r--r--. 1 Alex Alex 13 Nov  2 18:33 index.html
-rw-r--r--. 1 root root  0 Nov  2 18:42 test

Podsumowanie i zapowiedź

W tym artykule:

- stworzyliśmy nowy obraz kontenera, zapisując jego stan
- zatagowaliśmy nowy obraz
- uruchomiliśmy obraz z wybraną usługą
- wystawiliśmy port z kontenera na lokalny adres
- użyliśmy volumenu w celu zamontowania danych serwera WWW, które w przeciwieństwie do kontenera i jego danych, są trwałe.

Pomimo naszych starań nasz kontener ma jednak szereg wad:

- uruchamia usługę jako użytkownik root. W wielu środowiskach domyślnie
zabronione jest uruchamianie kontenerów startujących swoje procesy jako root
- nie można nim sterować przy pomocy zmiennych środowiskowych
- domyślnie uruchamianą komendą wciąż jest /bin/bash
- budowanie nie jest automatyczne.

W następnej części naszych dockerowych przygód przyjrzymy się budowaniu własnego obrazu. Nauczymy się też uruchamiać programy wewnątrz kontenera, jako normalny (nieuprzywilejowany) użytkownik. Następnie omówimy zarządzanie kontenerem przy pomocy zmiennych przekazywanych do kontenera. Stworzymy w ten sposób kilka obrazów, które udostępnimy pod EuroLinuxowym szyldem na platformie Docker Hub.

Bonus usuwanie wszystkich kontenerów i ich obrazów

W celu usunięcia wszystkich kontenerów i obrazów użyjemy komend listujących z przełącznikami -a, skrócona opcja --all, i -q, skrócona opcja --quiet. Użycie flagi -q dla komend docker ps i docker images sprawia, że wyświetlane są tylko ID. Przykładowe wywołanie docker images bez flagi -q i z flagą -q.

[root@localhost ~]# docker images 
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
eurolinux/centos-8      latest              5cf35e13f8d3        3 weeks ago         182MB
eurolinux/eurolinux-7   latest              3eed865a31cf        3 weeks ago         168MB
eurolinux/eurolinux-7   eurolinux-7-7.7.1   65cb490a7493        6 weeks ago         168MB
[root@localhost ~]# docker images -q -qa
5cf35e13f8d3
3eed865a31cf
65cb490a7493

Wyjścia z samym ID idealnie nadają się do użycia w innych komendach.
Alternatywą dla nich jest tworzenie potokowych potworków z użyciem grep
i/lub awk.

W celu usunięcia wszystkich działających kontenerów i ich obrazów
można użyć:

docker stop $(docker ps -q) # wstrzymuje wykonywanie uruchomionych konteneróœ kontenerów
docker rm $(docker ps -a -q) # usuwa wszystkie kontenery
docker rmi $(docker images -a -q) # usuwa obrazy kontenerów

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *