Nowoczesne aplikacje rozproszone cechują się tym, że mogą wykorzystywać o wiele więcej zasobów niż tylko serwer i baza danych. Co w przypadku kiedy chcemy, aby nasza aplikacja korzystała z dwóch web serwisów, bazy danych, serwera redis dla szybkiego chache-owania, stack ELK (Elasticsearch, Logstash, Kibana) dla logów, wyszukiwania etc. Sami przyznacie, że sporo elementów trzeba monitorować, deploy-ować etc. Chwalcie niebiosa, bo i o to niosę rozwiązanie! Na pomoc przychodzi nam Docker Compose służący do zarządzania kontenerami.
W dzisiejszym poście przybliżę:
- Co to jest docker compose?
- Jak uruchomić wiele kontenerów na raz?
Krótki wstęp
Artykuł ten jest kontynuacją serii o Dockerze. Jeśli jesteś nowy w temacie, zapraszam do zapoznania się z resztą:
Czym jest Docker Compose?
Przed użyciem nowej technologi każdy dobry programista powinien sobie odpowiedzieć na zasadnicze pytanie: czy przygotował sobie kakałko? Po uzyskaniu twierdzącej odpowiedzi nachodzą kolejne wątpliwość: co to jest za technologia i po co ona w ogóle powstała? Taki mały rachunek sumienia i od razu mówię że odpowiedzi "Nie wiem" i "Nie wiem ale chce się przekonać" są dalekie od oczekiwanych. Odpowiadając poprawnie:
Co to jest i do czego to ma mi się przydać?
Docker Compose jest przede wszystkim narzędziem do definiowania i uruchamiania wielu kontenerów naraz. Samą definicje tworzymy w pliku docker-compose.yml
, gdzie za pomocą notacji YAML możemy opisać naszą infrastrukturę.
Samo narzędzie ma sporą gamę zastosowań. Oprócz uruchamiania wszystkich potrzebnych obrazów Docker-a na produkcji, stagingu czy innym środowisku możemy wykorzystać go do testów integracyjnych w naszym pipeline CI.
Jak wygląda plik Docker Compose?
Przed przejściem do praktycznego przykładu użycia docker-compose chce wam opisać składnie pliku na podstawie fragmentu kodu zaciągniętego z dokumentacji docker-a.
Sam plik docker-compose.yml wygląda następująco:
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
links:
- redis
redis:
image: redis
volumes:
logvolume01: {}
Jak widać nie jest to najdłuższy plik konfiguracyjny jaki spotkaliście w życiu. Chyba, że to wasz pierwszy raz. To długi.
Zaczyna się od zdefiniowania jakiej wersji chcemy użyć. Obecnie od silnika dockera 17.06.0 najnowsza wersja jest 3.3. Bardzo często możecie spotkać jeszcze 2-ke ponieważ była standardem przez "lata".
Definicja serwisów
Następnie definiujemy serwisy jakie docker compose ma uruchomić. Każdy musi otrzymać swoją unikalną nazwę. Stąd konwencja nazewnictwa, w której pod nazwą klucza kryje się definicja pojedynczego serwisu. W powyższym przykładzie mamy dwa serwisy "web" i "redis", które od razu obrazują nam dwa przykładowe uruchomienia obrazów.
Najpierw zacznę od wyjaśnienia serwisu "redis". Składa się z bardzo krótkiej definicji. Pod kluczem "image" definiujemy nazwę gotowego obrazu dostępnego w repozytorium docker hub. Narzędzie do uruchamiania automatycznie pobierze i uruchomi dany obraz.
Teraz przejdźmy do serwisu web, jest nieco bardziej skomplikowany. Za pomocą pierwszego klucza "build", przekazujemy do docker compose, że chcemy, aby obraz został zbudowany na naszej maszynie, a nie ściągnięty z jakiegoś repozytorium. W wartości tego klucza przekazujemy ścieżkę do folderu, gdzie przechowujemy plik Dockerfile aplikacji, którą chcemy zbudować.
Kolejnym element definicji naszego plik docker-compose.yml jest definicja portów, które mają zostać udostępnione dla świata. W tym przypadku po porcie 5000 możemy komunikować się z aplikacją znajdującą się wewnątrz obrazu dockera.
Następnym elementem jest definicja volumes. Niestety potraktuje go w tym poście po łebkach. Na pewno za jakiś czas napiszę obszerniejszy artykuł na ten temat. Przechodząc do sedna - za pomocą tej sekcji definiujemy przestrzeń, w których możemy składować dane, których nie chcemy utracić w przypadku restartu kontenera. Tak można zrobić miejsce na logi naszej aplikacji, ewentualnie jakiś store plików (chociaż w czasach aplikacji rozproszonych to i takie usługi powinny być webservisami by w prosty sposób filtrować / agregować różne elementy).
W dalszej części aplikacji pojawia się sekcja link. To właśnie w niej możemy zdefiniować jakie serwisy mogą się ze sobą komunikować. W tym przypadku informujemy docker compose że chcemy aby nasz serwis "web" mógł bez problemów komunikować się z serwisem "redis". Sama komunikacja polega na stworzeniu nowej wirtualnej sieci, widoczną za pomocą aby Docker Network.
Przykładowe wykorzystanie
Teraz przejdźmy do tego co tygryski lubią najbardziej - mięska :) Jako projekt testowy napiszemy prosty serwis Node.js, który zapisze dane do bazy redis-a, a następnie po sekundzie odpyta redis-a o wcześniej zapisaną wartość. Myślę, że jest to dostatecznie prosty przykład, aby nie zagłębiając się w techniczne rozwiązania, skupić się wyłącznie na poprawnym skonfigurowaniu docker compose.
Budujemy aplikację Node.js
Zacznijmy od zbudowania aplikacji Node.js (komunikację aplikacji .NET Core z redis-em opiszę niedługo, w osobnym artykule). W terminalu wyklepmy kilka komend aby mieć na czym pracować:
$ mkdir test-docker-compose
$ cd test-docker-compose
$ npm init -y
$ npm i redis
Do komunikacji z redis-em, wykorzystamy bibliotekę z npm-a o nazwie... redis Łoooo jaka niespodzianka! Kto by się spodziewał? Takie zaskoczenie!
A taj na serio - pozwala na komunikacje z redis-em za pomocą bardzo prostego API. Nie jest może najnowsza, acz dalej jest jedną z dwóch rekomendowanych klientów dla Node.js w oficjalnej dokumentacji redis-a.
No dobra, przejdźmy dalej bo mi kakałko stygnie. Nie no żartuje, jesteście dla mnie najważniejsi <patrzy smutno na kakałko>. Cała implementacja serwisu zmieści się na 12 liniach kodu... nie wierzycie? To patrzcie:
const redis = require('redis');
const client = redis.createClient(6379, 'redis');
client.on('error', (err) => {
console.log('Error ' + err);
});
client.set('test_key', 'Hello World!', redis.print);
setTimeout(() => {
client.get('test_key', (err, value) => { console.log(value) });
}, 1000);
Wszystko co tutaj się dzieje to stworzenie nowego klienta za pomocą funkcji "createClient". W pierwszym argumencie podajemy port, na którym uruchomiony będzie redis. Jako drugi podajemy adres redis-a. Oba parametry są opcjonalne, ba nawet jeżeli pominiemy podanie porta to i tak ten klient będzie go szukał na porcie 6379 (domyślna wartość). Jednak domyślnym adresem host-a jest 127.0.0.1, więc o ile nie uruchomimy tego serwisu i redis-a na naszej lokalnej maszynie to niestety nie będą mogły się ze sobą skomunikować.
Tutaj przychodzi na pomoc wirtualna sieć Docker-a, która pozwala wykorzystać nazwy serwisów, jako ich adresy :)
Następnie wykorzystujemy metody "set" i "get", aby odpowiednio ustawić klucz w bazie redis-a lub jakiś pobrać. I to by było na tyle z naszej implementacji aplikacji.
Tworzymy Dockerfile dla serwisu
Przechodzimy do stworzenia pliku Dockerfile dla naszej aplikacji Node.js. Aby ułatwić sobie życie skorzystam z pliku, który omawiałem przy okazji artykułu Mikroserwis Node z Docker-em. Jeżeli chcesz się z nim zapoznać to zapraszam do treści posta lub repozytorium na Github-ie, do którego link będzie pod koniec artykułu.
Przyszła pora na Docker Compose
Posiadając poprawny plik Dockerfile musimy stworzyć nasz plik konfiguracyjny docker-compose.yml. Zaczynamy od wywolania komendy tworzenia i zdefiniowania numeru wersji, z której chcemy korzystać.
version: '3'
Następnie przechodzimy do definiowania naszych serwisów. Zawsze staram się na początku zdefiniować serwisy, które mogę pobrać jako gotowe obrazy z repozytorium docker hub-a. Tak więc dodaje dwie linijki odpowiedzialne za poprawne uruchomienie obrazu redis-a.
version: '3'
services:
redis:
image: redis
Czas na poinformowanie aplikacji, że chcemy by była budowana i uruchamia. Wykorzystamy do tego klucz o nazwie "build", który opisałem szerzej przy okazji omawiania przykładowego pliku docker-compose.yml.
version: '3'
services:
web:
build: .
links:
- redis
redis:
image: redis
Definiując ścieżkę do naszego serwisu jako "." musimy zamieścić plik docker-compose.yml w tym samym projekcie co plik Dockerfile w naszej aplikacji Node.js
Budujemy i uruchamiamy Docker Compose
Możemy przejść do zbudowania i uruchomienia wszystkiego, co do tej pory zrobiliśmy :)
Aby zbudować cały projekt musimy wykonać komendę "docker-compose build". Ja osobiście (jak na załączonym gifie) dodaje jeszcze flagę --no-cache aby być pewien że moje obrazy budują się na nowo :)
By uruchomić cały projekt musimy tylko wykonać komende "docker-compose up".
Jak widać na załączonym obrazku wszystko uruchomiło się poprawnie :) w przypadku gdyby coś nie działało to cóż, u mnie działa :p. Nawet na konsoli pojawiła się wiadomość, która została sekundę wcześniej zapisana do redis-a.
Wpływ na Docker Network
Ostatnim elementem jaki chciałem omówić jest wpływ docker compose na wirtualne sieci.
Jeżeli teraz w konsoli uruchomimy komendę
$ docker network ls
otrzymamy tabelkę z 4 pozycjami:
Jak widać, po uruchomieniu docker compose została utworzona nowa sieć wirtualna. To właśnie dzięki niej mogliśmy użyć nazwy naszego serwisu jako jego adresu.
Podsumowanie
W dzisiejszym poście przedstawiłem wam podstawowe informację na temat wykorzystania docker compose. Jak zwykle mam nadzieje że wam się miło czytało :)
A i bym zapomniał <odkłada kakałko spowrotem na biurko patrząc smutno>, repozytorium, o którym wcześniej wspominał znajdziecie >>TUTAJ<<. Możecie je forkować, zmieniać, wykorzystać do czegokolwiek wam się przyda :)
Do Następnego!
Cześć :)