Od czego więc zacząć? Doświadczenie podpowiada mi, że początkowy próg wejścia jest raczej wysoki, choć obecny i dawny docker, to zupełnie różnie zwierzęta.
Do napisania tego artykułu zainspirowała mnie rozmowa podsłuchana w firmie. Dotyczyła tworzenia obrazów dockerowych i spuentowana została stwierdzeniem, że „Jest to niesamowicie trudne!”. Nie do końca zgadzam się z tym wnioskiem. Według mnie wystarczy znać absolutne podstawy używanej technologii oraz pracy z danym systemem operacyjnym i na naszej drodze nie pojawią się przeszkody nie do pokonania. Ale to oczywiście moja opinia, a Ty przecież nie masz powodu, żeby mi wierzyć, prawda? Na początku mojej przygody z dockerem ja również czytałem poradniki w Internecie, robiąc przy tym wielkie oczy i zachodząc w głowę „W co ja się wpakowałem?!”
Pobawmy się więc – zobaczysz, że oswojony docker nie gryzie (zbyt często).
Jaki jest plan? Omówimy sobie pokrótce podstawy pracy z dockerem, żeby mieć zgodny pogląd na pojawiające się terminy. Następnie umieścimy naszą własną aplikację w kontenerze, zapewnimy jej wszystko to, czego potrzebuje, a na końcu zaszalejemy kompletnie, najpierw absurdalnie komplikując, a następnie konteneryzując cały nasz testowy system.
Uwaga: nasz obraz będzie zawierał aplikację stworzoną w technologii .Net Core, ale jeśli nie po drodze Ci z wujkiem z Redmond – jeszcze nie uciekaj! Poniższe instrukcje można w prosty sposób zaadaptować do całej gamy innych technologii, o ile tylko umiesz skompilować swoją aplikację używając terminala.
Jeszcze krótka dygresja, zanim przejdziemy do meritum. Odpowiedzmy sobie na jedno pytanie – po co? To moja aplikacja, mam do niej wspaniałe IDE, które nawet parzy kawę, czemu miałbym się przejmować dokeryzacją? Abstrahując od pomysłu wdrażania dockera na produkcji (nie będę tutaj niczego sugerował, sam musisz zdecydować, czy w Twoim projekcie jest to dobry pomysł), aplikacje rzadko są samotnymi wyspami. Systemy informatyczne często składają się z dziesiątek, a nawet setek mniejszych kostek, współtworzących (chybotliwą) całość. Część z nich napisana będzie w języku, którego nie znamy, a czasem nawet o nim nie słyszeliśmy. Co muszę zainstalować i jakich poleceń użyć, aby je uruchomić? A jeśli – o zgrozo – coś nie zadziała, gdzie szukać pomocy? Wątek na Stack Overflow? Rytualna ofiara z kozy z kasztanów? Ktokolwiek widział, ktokolwiek wie…
![](https://assets-global.website-files.com/611f85497dc1fc2d6579757e/61697e58eb5551519dc66503_Piotr-Zimnicki_do-artykulu3-1-1024x768.png)
Jednak programy te stworzone zostały przez jakichś programistów, może nadal pracujących z nami a może żyjących już tylko we wspomnieniach i legendach. Można nawet domniemywać, że owi programiści potrafili uruchomić daną aplikację i przekonać do wykonywania swojej pracy. Gdyby tylko pozostawili po sobie jednoznaczne instrukcje działania… Albo, gdyby była możliwość lokalnego uruchomienia aplikacji w sposób całkowicie zgodny z jej działaniem na produkcji… A nie, chwileczkę, właśnie do tego przydaje się docker!
Powtarzalność, standaryzacja, prostota. I oczywiście trochę dodatkowych problemów, tak dla równowagi ;-)
Pogadanka o dockerze
W tytule pada słowo „dokeryzacja” – pochodna nazwy docker. Szerszym odpowiednikiem tego pojęcia jest konteneryzacja, jednak słowa te właściwie stały się synonimami. Docker jest po prostu najpopularniejszym sposobem konteneryzacji, choć oczywiście są również inni dostawcy tego typu usług (na przykład tacy).
Docker pomaga w umieszczeniu aplikacji wewnątrz kontenera. Ale czym właściwie jest ten cały kontener? Docker sam definiuje kontener jako:
A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another.
Dobrze, czyli zyskujemy szybkość i niezawodność przy uruchamianiu aplikacji na różnych środowiskach. Zdarzyło Ci się kiedyś spławić kogoś słowami „U mnie działa”? No to niestety już nie będziesz mógł wykręcić się w ten sposób.
![](https://assets-global.website-files.com/611f85497dc1fc2d6579757e/61697e5912c5a2341300e975_Piotr-Zimnicki_do-artykulu4-1.jpeg)
Czytamy dalej:
A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.
Przyniosłem wszystkie zabawki ze sobą, nie tylko aplikację, ale też całe środowisko uruchomieniowe, więc jestem całkowicie niezależny! Zaraz, to nie to samo, co maszyna wirtualna? Nie do końca – maszyna wirtualna zawiera w sobie system operacyjny, który będzie na niej działał. Kontenery nie posiadają systemu – zamiast tego, wykorzystują jądro systemu hosta. To właśnie dzięki temu są lekkie i szybkie – zawierają tylko i wyłącznie to, co jest potrzebne danej aplikacji.
Mimochodem przemycone zostały tu dwa terminy – kontener i obraz – więc chyba dobrze byłoby ustalić, co oznaczają:
- Kontener to żywy organizm, który możemy uruchomić i który wykonuje jakieś zadania. Docelowo, aplikacja działać będzie właśnie w kontenerze.
- Obraz to zbiór danych umożliwiający stworzenie kontenera. Sam obraz nie może zostać uruchomiony.
Najlepszą analogią, jaką można tu zastosować (pożyczoną, nie jestem jej autorem i niestety nie pamiętam źródła), to klasa i obiekt. Obraz jest jak klasa – opisuje, co będzie robił kontener, samemu nie będąc w żaden sposób uruchamialnym. Kontener jest jak instancja danej klasy, działający i gotów do pracy.
To już prawie koniec suchej teorii, ale wspomnijmy jeszcze o relacji kontenera z systemem operacyjnym. Poza tym, co przynieśliśmy ze sobą w obrazie, aplikacja potrzebuje do szczęścia jeszcze trzech rzeczy: systemu plików (bo musi gdzieś żyć), drzewa procesów (bo gdzieś się musi uruchomić) i konfiguracji sieciowej (bo musi mieć kontakt ze światem). Wszystkie trzy dostarczane są za pomocą docker engine wprost z systemu operacyjnego hosta. Każdy kontener posiada własny system plików, drzewo procesów i adresację, kompletnie odizolowane zarówno od hosta, jak i działających równolegle kontenerów.
Dobrze, skoro już to wszystko wiemy, to bierzmy się do pracy.
Ale jak? Konteneryzujmy!
Przede wszystkim potrzebna jest nam aplikacja. Ponieważ jest to kwestia absolutnie wtórna, zrobimy po prostu tak
λ > dotnet new webapp -o aspnetcoreapp
Dostaniemy prostą aplikację o nazwie aspnetcoreapp. Wrócimy do niej później, jednak teraz w zupełności nam to wystarczy.
Do tworzenia obrazów dockerowych potrzebny nam docker (duh!) oraz specjalny plik zwany Dockerfile (dokumentacja). Opisuje on, jak zbudować obraz, jakiego systemu operacyjnego użyć, skąd wziąć naszą aplikację itp. Jest to po prostu skrypt pakujący wszystko do jednego pudełka, nazywanego obrazem. Pierwszą rzeczą, jakiej potrzebujemy, jest obraz źródłowy – tak, tak samo, jak klasy dziedziczą po sobie, obrazy również budowane są na innych obrazach. Robimy to za pomocą polecenia FROM
1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
Ponieważ działamy w środowisku dotnetowym, użyjemy obrazów zawierających SDK .Net Core 3.1. Znajdziemy tam wszystkie biblioteki i zależności wykorzystywane przez .Net Core, więc nie będziemy musieli dodawać ich samodzielnie. Obrazy te utrzymywane są przez Microsoft, więc na ogół są aktualne. Słówko ostrzeżenia – Microsoft lubi czasem zamieszać w nazewnictwie obrazów bez wyraźnego powodu, więc gdy to czytasz, nazewnictwo obrazów może być już nieaktualne. Najnowsze obrazy można zawsze znaleźć na DockerHub’ie.
Uwaga: jeśli wcale nie jesteś dotnetowcem, tylko po prostu zaplątałeś się tu z Internetu – have no fear! DockerHub zawiera obrazy dla chyba każdej możliwej technologii, wystarczy chwilę poszukać
2 | WORKDIR /app
3 |
4 | COPY aspnetcoreapp/aspnetcoreapp.csproj ./
5 | RUN dotnet restore
Działamy dalej! Za pomocą polecenia WORKDIR ustawiamy obecny katalog roboczy (WORKDIR stworzy katalog, jeśli go nie znajdzie) wewnątrz budowanego obrazu. Za pomocą polecenia COPY skopiujemy nasz plik projektu do wnętrza obrazu, po czym pobierzemy wszystkie wymagane zależności.
Jeszcze słowo o kopiowaniu – plik kopiujemy z build context’u (o tym później) do wnętrza obrazu. Wszystkie ścieżki są więc ścieżkami względnymi, mającymi początek bądź w build context, bądź w WORKDIR.
Skoro zależności mamy załatwione pozostaje nam tylko pobrać wszystkie pozostałe pliki
7 | COPY . ./
i zbudować aplikację
8 | RUN dotnet publish -c Release -p:PublishDir=./output
aspnetcoreapp/aspnetcoreapp.csproj
Na tym właściwie można by zakończyć proces budowania. Banał, prawda? Nie zrobimy tego jednak, ponieważ byłoby to sprzeczne z jednym z założeń konteneryzacji – minimalna paczka pozwalająca uruchomić aplikację. Nasz obraz budowany jest w oparciu o dotnet/core/sdk i musi tak zostać – sdk zawiera wszystkie narzędzia potrzebne do budowania oraz debugowania (tak, można debugować wewnątrz kontenera!) aplikacji. Docelowo jednak nie będzie nam to do niczego potrzebne, w końcu chcemy tylko uruchomić nasz program, a nie kompilować jakiś kod. Użyjmy więc bardziej minimalistycznego obrazu, dotnet/core/aspnet, oraz procesu zwanego budowaniem wieloetapowym.
10| FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
11| WORKDIR /app
12|
13| COPY --from=build /app/output .
Przesiadamy się na inny obraz bazowy poleceniem FROM oraz tworzymy nowy WORKDIR – tym razem w obrazie opartym o dotnet/core/aspnet. Następnie kopiujemy wynik polecenia dotnet publish, ale nie z build contextu, ale z poprzedniego etapu budowania! Bardziej spostrzegawczy zauważyli pewnie, że w pierwszej linijce oznaczyliśmy nasz obraz jako build – zrobiliśmy to właśnie w tym celu. Kolejne etapy mają dostęp do danych zawartych w etapach poprzednich.
Jedyne, co pozostało nam do zrobienia, to wskazać palcem co właściwie powinien robić stworzony kontener (PID 1). Służy do tego polecenie ENTRYPOINT.
17| ENTRYPOINT ["dotnet", "aspnetcoreapp.dll"]
Dodatkowo możemy dodać jeszcze polecenie EXPOSE, informujące użytkowników naszego obrazu, że mogą spodziewać się czegoś na danym porcie. Nie jest to w żaden sposób wymagane do jego działania, ot taka pseudodokumentacja.
15| EXPOSE 80
I już, po bólu! W całej swojej okazałości, nasz Dockerfile wygląda następująco:
1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
2 | WORKDIR /app
3 |
4 | COPY aspnetcoreapp/aspnetcoreapp.csproj ./
5 | RUN dotnet restore
6 |
7 | COPY . ./
8 | RUN dotnet publish -c Release -p:PublishDir=./output aspnetcoreapp/aspnetcoreapp.csproj
9 |
10| FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
11| WORKDIR /app
12|
13| COPY --from=build /app/output .
14|
15| EXPOSE 80
16|
17| ENTRYPOINT ["dotnet", "aspnetcoreapp.dll"]
Mój pierwszy obraz
Mamy już Dockerfile zawierający definicję naszego obrazu, fajnie, ale… co teraz? Dockerfile jeszcze nie jest obrazem, aby coś z niego uzyskać musimy go jeszcze zbudować. Służy do tego polecenie
λ > docker build -f Dockerfile -t aspnetcoreimage .
Parametr -f wskazuje położenie pliku Dockerfile i w tym wypadku moglibyśmy go całkowicie pominąć, bo taka właśnie jest jego domyślna wartość. Parametr -t określa nazwę naszego obrazu jak mcr.microsoft.com/dotnet/core/aspnet w przypadku naszego obrazu bazowego. Kropka na końcu to wcale nie jest literówka, ha, nie tym razem! Kropka ta to właśnie nasz tajemniczy build context, którym straszyłem Cię już wcześniej.
Build context to dane przekazane do obrazu (a właściwie do docker daemon) na etapie jego budowania. Dostęp do nich mamy więc tylko z poziomu Dockerfile i nie będą częścią wyjściowego obrazu – o ile nie zdecydujemy inaczej (vide polecenie COPY). Ponieważ nasz proces budowania zakłada wcześniejsze istnienie źródeł aplikacji (zamiast tego moglibyśmy ją np. pobrać z gita), musimy je przekazać poprzez build context. Kropka oznacza tu katalog, z którego wykonaliśmy polecenie docker build – wszystkie zawarte w nim pliki i katalogi będą mogły być użyte do budowy naszego obrazu.
No właśnie, wszystkie, bez wyjątku. Niezależnie od tego, czy będą nam potrzebne, czy nie. Ponownie kłóci się to z minimalistycznym podejściem, w końcu każdy niepotrzebny megabajt spowolni i skomplikuje proces budowania. Na szczęście z tym również da się coś zrobić! Cóż za szczęśliwy zbieg okoliczności, prawda?
W tym momencie na scenę wkracza (cały na biało…) kolejny plik – .dockerignore. Jest on dla Dockerfile’a tym, czym .gitignore dla gita – pokazuje, na co nie zwracać uwagi. Tak samo, jak nie ma sensu trzymać wszystkiego w repozytorium, tak nie każdy plik będzie nam potrzebny do stworzenia obrazu. Brzmi trywialnie – ile miejsca można zaoszczędzić pozbywając się bin, out i .git? W małych projektach faktycznie różnice będą niewielkie, ale z czasem context zacznie puchnąć i puchnąć, aż w końcu okaże się, że wysyłamy już setki MB śmieci, znacząco wszystko spowalniając. Zaufaj mi na słowo. Byłem, widziałem. Czasem wciąż mam koszmary.
Chwila przerwy, pisze do Ciebie ja z przyszłości. Dosłownie kilka dni po napisaniu tych słów rozmawiałem z kolegą, pracującym w pewnym poważnym projekcie IT. Złorzeczył, że budowanie obrazów Jego aplikacji stało się niesamowicie powolne. Winny okazał się właśnie build context. Zapnij pasy, za chwilę autentyczne logi przed i po dodaniu prostego pliku .dockerignore:
Sending build context to Docker daemon 825MB Sending build context to Docker daemon 2.676MB
Trochę odbiegliśmy od tematu – budowaliśmy nasz obraz. Zobaczmy, co też stało się po wykonaniu polecenia docker build:
λ > docker build -f Dockerfile -t aspnetcoreimage . Sending build context to Docker daemon 4.393MB Step 1/11 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build ---> cf60f383a8d1 Step 2/11 : WORKDIR /app ---> Running in a0141eff2546 Removing intermediate container a0141eff2546 ---> ad1a813fab27 Step 3/11 : COPY aspnetcoreapp/aspnetcoreapp.csproj ./ ---> e7b454992e8a Step 4/11 : RUN dotnet restore ---> Running in f27c6ff45ec6 Determining projects to restore… Restored /app/aspnetcoreapp.csproj (in 157 ms). Removing intermediate container f27c6ff45ec6 ---> 805893f96f5c Step 5/11 : COPY . ./ ---> b70ad4b4d3cb Step 6/11 : RUN dotnet publish -c Release -p:PublishDir=./output aspnetcoreapp/aspnetcoreapp.csproj ---> Running in 4a8a17424f5f Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core Copyright (C) Microsoft Corporation. All rights reserved.Determining projects to restore… Restored /app/aspnetcoreapp/aspnetcoreapp.csproj (in 150 ms). aspnetcoreapp -> /app/aspnetcoreapp/bin/Release/netcoreapp3.1/aspnetcoreapp.dll aspnetcoreapp -> /app/aspnetcoreapp/bin/Release/netcoreapp3.1/aspnetcoreapp.Views.dll aspnetcoreapp -> /app/aspnetcoreapp/output/ Removing intermediate container 4a8a17424f5f ---> ad742806d55f Step 7/11 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime ---> 8aa5a7e8b51a Step 8/11 : WORKDIR /app ---> Running in e8f6b81bd2b1 Removing intermediate container e8f6b81bd2b1 ---> fc4cefd5b42a Step 9/11 : COPY --from=build /app/aspnetcoreapp/output . ---> 0f8072747c1d Step 10/11 : EXPOSE 80 ---> Running in a72be83c219f Removing intermediate container a72be83c219f ---> b06cb3bc5f75 Step 11/11 : ENTRYPOINT ["dotnet", "aspnetcoreapp.dll"] ---> Running in dd0286e426d0 Removing intermediate container dd0286e426d0 ---> a5b89154c9a1 Successfully built a5b89154c9a1 Successfully tagged aspnetcoreimage:latest
Docker deamon posłusznie wykonał wszystkie instrukcje zawarte w pliku Dockerfile, stworzył obraz o nazwie aspnetcoreimage i nadał mu tag latest (o tagach innym razem, roboczo tag == wersja).
Super! Nic tylko uruchomić kontener i można już otwierać szampana!
λ > docker run --name awesomeaspnetcontainer aspnetcoreimage:latest
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
Trochę więcej detali
Nasza aplikacja działa (nawet jeśli nie zainstalowaliśmy dotnet sdk na hoście!), ale nie ma żadnego kontaktu ze światem zewnętrznym. Czas więc uruchomić wyobraźnię i nieco wszystko skomplikować.
![](https://assets-global.website-files.com/611f85497dc1fc2d6579757e/61697e59a2b17a8503d0c325_Piotr-Zimnicki_do-artykulu2-1-1024x683.jpeg)
Nie będziemy nic implementować, bo nie taki jest cel tego ćwiczenia. Będziemy natomiast udawać, że nasza mała apka komunikuje się z innymi usługami wewnątrz systemu – a co! Na potrzeby eksperymentu uznajmy, że:
- Wykorzystujemy jakieś inne API do przetwarzania zapytań, niech będzie to ProductAPI
- Aplikacja używa bazy danych, ustalmy, że będzie to MySQL
- Aplikacja wykorzystuje cache pod postacią redisa, bo kto bogatemu zabroni
Zaszywanie ich adresów w kodzie nie przejdzie, zmodyfikujmy więc nieco plik appsettings.json i dodajmy tam nieco więcej detali
{
"ProductApiAddress": null,
"MySql": {
"ConnectionString": null
},
"Redis": {
"ConnectionString": null
}
}
Moglibyśmy stworzyć kilkanaście wersji tego pliku (localhost, dev, qa, uat, prod, …) i używać odpowiedniej, ale brzmi to mało zachęcająco – nie dość, że będziemy musieli je utrzymywać, to obrazy będą zawierać dane wrażliwe. Zamiast tego, użyjmy do tego celu zmiennych środowiskowych.
Interfejs IConfiguration, odpowiadający m.in. za obsługę plików konfiguracyjnych, posiada kilka wewnętrznych źródeł prawdy. Jednym z nich jest (domyślnie) EnvironmentVariablesConfigurationProvider, potrafiący przekuć zmienne środowiskowe znalezione w systemie (czy, jak w tym wypadku, wewnątrz kontenera) na pola widoczne w pliku appsettings. Wiąże się z tym pewna magiczna składnia, o której warto wiedzieć. Chcąc na przykład dostać się do klucza konfiguracyjnego
Redis:ConnectionString
należy użyć zmiennej środowiskowej o nazwie
Redis__ConnectionString
Banalne, prawda? Nie wiem, dlaczego Microsoft nie chwali się tym bardziej, swego czasu zajęło mi chwilę odkrycie tej zależności…
Tak dostarczona konfiguracja nie jest zawarta w samym obrazie, a więc ryzyko jej utraty jest niższe. Pozostaje nam tylko przekazać odpowiednie wartości do wnętrza kontenera. Zrobimy to podczas jego uruchamiania:
λ > docker run --name awesomeaspnetcontainer –e ”ProductApiAddress=acme.com/api” –e ”MySql__ConnectionString=server=acme.com;database=ToyStoryCharacters;user=andy;password=WoodyPride” –e ”Redis__ConnectionString=acme.com:1337” aspnetcoreimage:latest
Psst, zamiast dopisywać wszystko do jednego polecenia możesz użyć pliku konfiguracyjnego
Zadbaliśmy o komunikację z innymi serwisami, ale jeśli spróbujemy uzyskać jakieś informacje od naszej aplikacji działającej w kontenerze okaże się, że zapytania lecą w próżnię. Co się dzieje? Choć aplikacja uruchomiła się wewnątrz na porcie 80, to docker runtime domyślnie nie udostępnia żadnych portów z wnętrza kontenera światu zewnętrznemu. Musimy zrobić to sami. Port 80 bywa dość mocno okupowany, ale wciąż znajdujemy się wewnątrz kontenera, więc nie ma to najmniejszego znaczenia. Ponieważ adresacja sieciowa poszczególnych kontenerów oraz hosta to zupełnie rozłączne sprawy, możesz mieć kilkadziesiąt kontenerów działających wewnętrznie na tym samym porcie i nie będą sobie wzajemnie przeszkadzać. Żeby jednak jakoś dostać się do naszej aplikacji, musimy zmapować wspomniany już port 80 na dowolny (wolny) port fizycznej maszyny – powiedzmy port 5000. To również należy zrobić podczas uruchamiania kontenera
λ > docker run --name awesomeaspnetcontainer –e ”ProductApiAddress=acme.com/api” –e ”MySql__ConnectionString=server=acme.com;database=ToyStoryCharacters;user=andy;password=WoodyPride” –e ”Redis__ConnectionString=acme.com:1337” –p 5000:80 aspnetcoreimage:latest
Nasz program wie już gdzie znaleźć wszystko, co będzie mu potrzebne do pracy, oraz nauczył się odpowiadać na wysyłane zapytania. Świetnie. Możemy wreszcie zostawić go w spokoju i pozwolić mu spokojnie wypełniać swoje zadania. My natomiast przejdźmy do innego zagadnienia.
Orkiestracja
Nie, wcale nie chodzi o zarządzanie orkiestrą, po prostu jeszcze nie znalazłem na to dobrego określenia w języku polskim. Chodzi tu o zarządzanie kontenerami. Wszystkie poprzednie kroki miały na celu ułatwienie sobie pracy poprzez automatyzację procesu uruchamiania aplikacji, więc kto o zdrowych zmysłach chciałby przy tym pisać ręcznie tasiemce w konsoli, prawda? Przecież widoczne powyżej polecenie to jakiś dramat! Nie, nie, nie, to nie dla nas. Oczywiście jest na to prostszy sposób, a właściwie całe dziesiątki. Jednym z nich jest docker-compose (dokumentacja) – narzędzie, które znów uprości nam pracę. Napiszemy raz, a potem wszystko załatwimy prostą komendą. Serio, obiecuję, że tym razem to będzie takie proste!
Podstawą działania docker compose jest plik docker-compose.yml. Tak, dobrze przeczytałeś, ten plik będzie właśnie w formacie YAML. Tak, YAML nadal żyje i nadal są ludzie, którzy go używają. I tak, jest dosyć uciążliwy, ale można przywyknąć. Zagorzałych przeciwników YAMLa pocieszę, że można zamiast niego stosować format JSON.
Bez zbędnych ceregieli – docker-compose.yml to (między innymi) inny sposób zapisu tego, co do tej pory robiliśmy za pomocą polecenia docker run. W naszym przypadku cały ten plik będzie wyglądał następująco
1 | version: '2'
2 |
3 | services:
4 | awesomeaspnetcontainer:
5 | container_name: awesomeaspnetcontainer
6 | image: aspnetcoreimage:latest
7 | ports:
8 | - "5000:80"
9 | environment:
10| - ProductApiAddress=acme.com/api
11| - MySql__ConnectionString=server=acme.com;database=ToyStoryCharacters;user=andy;password=WoodyPride
12| - Redis__ConnectionString=acme.com:1337
Są to właściwie te same informacje, których używaliśmy dotychczas, przedstawione w innej formie. Dzięki wyrzuceniu wszystkich szczegółów do pliku YAML możemy właściwie o nich zapomnieć. Uruchomienie naszej aplikacji sprowadzi się teraz do jednego prostego polecenia
λ > docker-compose up
Creating network "example_default" with the default driver
Creating awesomeaspnetcontainer ... done
Żadnych dodatkowych czarów, żadnych komplikacji, aplikacja działa. Właściwie nie musimy nawet wiedzieć, co dokładnie uruchamiamy, bo komputer pamięta to za nas.
Dodatkowym plusem jest to, że compose sam sprząta po swoim psie swoje kontenery. Uruchamiając je ręcznie musimy sami zadbać o to, aby miały sensowną i unikalną nazwę, oraz żeby nie pozostawiać po sobie osieroconych kontenerów, które uruchamiając się i psocąc spowodują Twoje przedwczesne siwienie… Docker compose to kolejny stopień automatyzacji, który wszystkim tym zajmie się za nas.
Zaszalejmy – orkiestracja większego systemu
Ktoś mógłby mi teraz zarzucić, że używanie docker compose w przypadku posiadania jednego kontenera to przesada. Jest w tym trochę racji, choć ja osobiście cenię sobie wygodę trzymania wszystkiego w pliku .yml. Compose pokazuje swoje prawdziwe możliwości w sytuacji, gdy kontenerów mamy więcej. Znacznie więcej. Swego czasu stawiając lokalnie system, z którym wówczas pracowałem, potrzebowałem ich ponad czterdziestu. Wiele z działających tam aplikacji znałem tylko z nazwy, nie mając nawet pojęcia, w jakiej technologii zostały stworzone. Konia z rzędem temu, kto uruchomiłby takie monstrum bez konteneryzacji!
Nieco wcześniej przekonaliśmy sami siebie, że nasza aplikacja wymaga do działania MySQLa, redisa i tajemniczego ProductAPI. Popuśćmy nieco wodze fantazji i dorzućmy tam jeszcze garść technologii. Może jakaś aplikacja w Node.js? Może jakiś NGINX? Consul? RabbitMQ? Elasticsearch?
Mimo że bierzemy technologie z sufitu, to nietrudno wyobrazić sobie, że większy system informatyczny mógłby używać ich wszystkich. Wszystko to bez problemu możemy zamknąć w jednym pliku docker-compose i uruchamiać automagicznie, za pomocą jednego kliknięcia.
Nasz rozdmuchany i przesadzony docker-compose.yml mógłby wyglądać tak:
version: '2'
services:
awesomeaspnetcontainer:
container_name: awesomeaspnetcontainer
image: aspnetcoreimage:latest
# konfiguracja
redis_asp:
container_name: redisasp
image: redis
# konfiguracja
mysql_asp:
container_name: mysqlasp
image: mysql
# konfiguracja
volumes:
- mysql-data-volume:/var/lib/mysql
product_api:
container_name: productapi
image: secretregistry.com/productapi:latest
# konfiguracja
nginx:
container_name: nginx
image: nginx
# konfiguracja
rabbitmq:
container_name: rabbitmq
image: rabbitmq
# konfiguracja
consul:
container_name: consul
image: consul
# konfiguracja
elastic:
container_name: elastic
image: elasticsearch
# konfiguracja
volumes:
- elastic-data-volume:/usr/share/elasticsearch/data
nodeapp:
container_name: nodeapp
image: secretregistry.com/nodeapp:latest
# konfiguracja
dotnetcoreapp:
container_name: dotnetcoreapp
image: secretregistry.com/dotnetcoreapp
# konfiguracja
javaapp:
container_name: javaapp
image: secretregistry.com/javaapp
# konfiguracja
mongo:
container_name: mongo
image: mongo
# konfiguracja
volumes:
- mongo-data-volume:/data/db
volumes:
mysql-data-volume:
elastic-data-volume:
mongo-data-volume:
I Bóg jeden wie, co jeszcze. Start całego takiego systemu to jedno polecenie, którym zresztą rzuciłem w Ciebie już wcześniej
λ > docker-compose up
Jako użytkownik (programista/tester) takiego systemu nie musisz nawet wiedzieć, co dzieje się w środku – przynajmniej, dopóki wszystko działa jak należy. Sky is the limit!
Podsumowanie
Mam nadzieję, że powyższy artykuł rozbudził Twoją ciekawość i, przede wszystkim, pozostawił niedosyt. Dowiedzieliśmy się, czym jest konteneryzacja, nauczyliśmy się, jak wpakować aplikację do obrazu i co z nim później zrobić, żeby był przydatny. Jeśli doczytałeś do tego momentu bez przeskakiwania paragrafów – gratuluję wytrwałości! Jesteś na dobrej drodze do bliższego zapoznania się z dockerem. I to dopiero początek – mimo obszernego opisu, z którym zdążyłeś się już zapoznać, są to dopiero podstawy. Jest to jednak ważny pierwszy krok, a od takiego przecież trzeba zacząć. Wszystko wskazuje na to, że docker zostanie z nami na dłużej, więc w interesie wszystkich nas – szarych trybików w projektach informatycznych – jest to, aby się z nim zaprzyjaźnić.
Zanim jednak zaczniesz, muszę Cię ostrzec! Docker uzależnia.
Docker is like the free first hit of coke. Next thing you know you are using k8s behind a dumpster in an alley, wondering where your life went. - Anonymous reddit user