data.mdx.frontmatter.hero_image

Docker Compose - Kontenerów Wincej!

2017-09-25 | Docker | bd90

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ą:

Zrozumieć docker-a:

Mikroserwis Node z Docker-em:

Docker Network

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ść :)

By Bd90 | 25-09-2017 | Docker