pytania rekrutacyjne Docker - okładka

Web developer – pytania rekrutacyjne – Docker

Artykuł dotyczący Dockera, w serii artykułów z pytaniami rekrutacyjnymi na stanowisko Web Developera, może wydawać się dość przewrotny. Niemniej jednak, Docker jest tak przydatnym i szeroko wykorzystywanym narzędziem w branży IT, że jego znajomość jest bardzo wskazana. Ponadto, nawet jeśli rozwijasz wyłącznie aplikacje frontendowe, to najprawdopodobniej integrujesz je z jakąś usługą backendową (np. jakimś API). Dzięki Dockerowi w prosty sposób możesz uruchomić backend lokalnie i polegać wyłącznie na swojej maszynie. Uruchomienie backendu lokalnie pozwala także na napisanie deterministycznych testów E2E, które da się uruchomić lokalnie oraz na środowiskach ciągłej integracji (CI).

Ten wpis jest jednym z wpisów z serii wpisów z pytaniami rekrutacyjnymi. Serdecznie zachęcam do zapoznania się z innymi wpisami z serii:

 1. Czym jest Docker i do czego służy?

Aby rozpocząć pracę z Dockerem warto wiedzieć czym tak naprawdę on jest. Docker jest narzędziem służącym do uruchamiania kontenerów. Dzięki kontenerom można w prosty sposób uruchamiać aplikacje bez fizycznego instalowania ich na wykorzystywanej maszynie. Przykładowo, chcąc uruchomić bazę danych nie trzeba instalować jej lokalnie. Wystarczy, że uruchomiony zostanie kontener, bazując na obrazie wybranej bazy danych oraz zostanie zdefiniowane jak można się z tym kontenerem komunikować. W ten sam sposób można dokonać konteneryzacji aplikacji, którą rozwijasz. Istotnym aspektem konteneryzacji z wykorzystaniem Dockera jest to, że kontenery nic o sobie nie wiedzą, dopóki użytkownik jawnie nie zadeklaruje, że ma być inaczej. Czyli posiadając kontener z bazą danych i kontener z rozwijaną aplikacją należy jeszcze zdefiniować jak te dwa kontenery powinny się ze sobą komunikować.

2. Jaka jest różnica między obrazem i kontenerem?

W tym pytaniu zawiera się zrozumienie czym jest obraz a czym kontener, jakie są różnice między nimi oraz jaka występuje między nimi zależność.

Obrazem (image) można nazwać „snapshot” aplikacji, którą potrzebujesz uruchomić. Używając analogii z programowania obiektowego, obraz można przyrównać do klasy. Oznacza to, że mając jeden obraz można powołać do życia wiele kontenerów. Aby uruchomić własną aplikację z użyciem Dockera niezbędne będzie utworzenie dla niej Dockerfile. Plik Dockerfile zawiera w sobie listę kroków niezbędną do zbudowania obrazu aplikacji. Co ważne, tworząc Dockerfile nie podajemy żadnego rozszerzenia. Przykładowy, najprostszy, Dockerfile może wyglądać następująco:

FROM node:16.13.1

WORKDIR /app

COPY ["package.json", "./"]

RUN npm install --production

COPY . .

CMD [ "node", "server.js" ]

Ta prosta definicja obrazu wykorzystuje obraz bazowy Node.js. Obraz ten został pobrany z Docker Hub, czyli rejestru obrazów, gotowych do uruchomienia lub rozbudowy. Istnieją również inne rejestry obrazów Dockerowych tj. Google Container Registry, Azure Container Registry od Microsoftu czy Elastic Container Registry od AWS. Po wskazaniu obrazu bazowego następuje kopiowanie niezbędnych plików i instalacja zależności produkcyjnych. Ostatnim krokiem w podanym przykładzie jest zdefiniowanie komendy do uruchomienia aplikacji w kontenerze. Mając stworzony Dockerfile, należy na jego przykładzie zbudować obraz za pomocą komendy docker build -t image_name ., gdzie image_name jest nazwą pod jaką dostępny będzie obraz. Oczywiście każdy obraz ma swój identyfikator i podanie nazwy jest opcjonalne. Jednakże prościej jest znaleźć zbudowany obraz w oparciu o konkretną nazwę niż po ciągu losowych znaków przez co rekomenduję z korzystania z flagi -t.

„Natomiast kontener (container) jest to instancja aplikacji uruchomiona na podstawie obrazu. Przyrównując to do programowania obiektowego, kontener byłby obiektem stworzonym na podstawie klasy (obrazu).

3. Czym jest Docker Compose?

Docker Compose jest narzędziem służącym do usprawnienia pracy z wieloma kontenerami. Dzięki Docker Compose użytkownik jest w stanie uruchomić środowisko z wieloma kontenerami za pomocą jednej komendy. Aby skorzystać z Docker Compose niezbędne będzie stworzenie pliku z rozszerzeniem .yml. Domyślną nazwą pliku, którego Docker Compose się spodziewa jest docker-compose.yml. Przykładowy plik może wyglądać następująco:

version: "3.7"
services:
  web:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - .:/code
    links:
      - redis
  redis:
    image: redis

Powyższa konfiguracja zawiera deklarację uruchomienia dwóch kontenerów. Jeden z nich to jakaś lokalna aplikacja, której kod źródłowy znajduje się w folderze code. Aplikacja jest dostępna z zewnątrz na porcie 5000. Należy pamiętać, aby aplikacja miała utworzony Dockerfile! Natomiast drugi konterner wykorzystuje gotowy obraz, w tym przypadku jest to obraz Redisa. Mając taki plik jedyne co należy zrobić to wywołąć polecenie docker-compose up co spowoduje uruchomienie kontenerów. Oczywiście Docker Compose oferuje znacznie więcej niż przedstawia użyty przykład. Dzięki temu narzędziu można na przykład przekazać zmienne środowiskowe do kontenerów, zdefiniować i skonfigurować sieć do komunikacji między kontenerami czy wymusić ich uruchamianie w określonej kolejności. Pełną listę możliwości Docker Compose znajdziesz w dokumentacji Dockera.

4. Do czego służy Docker volume?

Warto zauważyć, że w powyższym przykładzie pierwsza z aplikacji wykorzystuje Docker volume. Dzięki wykorzystaniu wolumenów tworzymy tak jakby link między wskazaną lokalizacją w kontenerze a wskazaną lokalizacją na lokalnej maszynie. Taki zabieg pozwala na bieżąco obserowować jak zmiany w kodzie źródłowym wpływają na działanie aplikacji bez potrzeby przebudowywania obrazu po każdej zmianie. Innym wykorzystaniem wolumenów może być np. zapis logów z aplikacji do pliku na lokalnej maszynie czy przekazywanie inicjalnego stanu bazy danych do aplikacji z lokalnej maszyny przy starcie aplikacji.

5. Jak „wejść” do uruchomionego kontenera?

Dodałem to pytanie do tego wpisu ponieważ jest to czynność, której dokonuję bardzo często. Dzięki „wejściu” do kontenera możemy w prosty sposób dokonywać debugowania aplikacji, szczególnie na środowiskach zdalnych wykorzystując np. połączenie SSH. Gdy występują problemy z poprawną pracą kontenera, często „wchodzę” do kontenera i próbuję znaleźć przyczynę problemu. Aby „wejść” do kontenera potrzebny będzie jego identyfikator. Aby pozyskać id kontenera należy wywołać polecenie docker ps. Z listy działających kontenerów należy wybrać pożądany kontener i skopiować jego id. Następnie w CLI należy skorzystać z polecenia docker exec -it container_id /bin/bash. Polecenie to sprawia, że uruchomiona zostanie powłoka bash we wskazanym kontenerze. Oczywiście można użyć również innej powłoki jeśli jest ona dostępna we wskazanym kontenerze.

6. Czym jest Open Container Initiative?

Open Container Initiative (OCI) jest odpowiedzialne za standaryzację w świecie konteneryzacji. Podążając za stanadardami wytyczanymi przez OCI, w przypadku potrzeby zastąpienia jednego z rozwiązań służycych do konteneryzacji innym, migracja odbywa się w zdecydowanie mniej bolesny sposób. Można tu przywołać analogię do kontenerów używanych na statkach. Wymiary kontenerów używanych w transporcie morskim są ustandaryzowane. Dzięki temu doskonale wiadomo jakie wymiary ma kontener, ile się do niego zmieści oraz ile kontenerów może zostać załadowanych na statek.

Dzięki OCI w prosty sposób możesz podmienić w swoim projekcie Dockera na np. Podmana. Świetny artykuł opisujący czym jest OCI znajdziesz pod tym linkiem.

7. Jakie są korzyści wynikające z konteneryzacji?

Tyle zostało już powiedziane o konteneryzacji, ale nie zostało powiedziane dlaczego warto zainteresować się konteneryzacją. Najważniejsze zalety konterneryzacji to:

  • Brak konieczności uruchamiania aplikacji na lokalnej maszynie. Oznacza to też brak konieczności instalacji dodatkowych komponentów tj. serwer HTTP czy bazy danych.
  • Jeden standard – wynikający z pracy OCI.
  • Przenaszalność – kontenery w latwy sposób można uruchomić na każdej maszynie umożliwiającej uruchomienie Dockera. Co istotne, na każdej maszynie kontener bazujący na tym samym obrazie w tej samej konfiguracji będzie działał tak samo (chyba, że na maszynie docelowej zabraknie zasobów, ale uważam to za edge case).
  • Izolacja – wystawienie endpointu dostępnego z zewnątrz dla kontenera wymaga jawnego zadeklarowania tego faktu. Domyślnie, kontenery są od siebie odizolowane jak i w maksymalnym możliwym stopniu od maszyny na której są uruchamiane.
  • Szybkie dostarczanie – gotowe obrazy można w szybki i prosty sposób udostępnić czy to klientom czy innym programistom. Ich uruchomienie przez odbiorcę jest równie szybkie i proste.
  • Konteneryzacja nie wymaga tylu zasobów co korzystanie z maszyn wirtualnych.

8. Jakie są dobre praktyki z pracą z kontenerami?

Cała masa dobrych praktyk pracy z Dockerem została przedstawiona w dokumentacji Dockera. Przedstawię tutaj małe streszczenie tego co możesz znaleźć w dokumentacji:

  • Nie korzystaj z konta root w kontenerze. Korzystanie z konta root jest złą praktyką z punktu widzenia bezpieczeństwa. Niektóre obrazy, np. obraz dla Node.js oferują wbudowanego użytkownika, a jego użycie sprowadza się do dodania kroku w DockerfileUSER node.
  • Nie instaluj zbędnych zależności – np. nie potrzebujesz w obrazie produkcyjnym developerskich zależności.
  • Używaj ściśle zdefiniowanej wersji obrazów (z tagiem z konkretną wersją). Dodatkowo, nie korzystaj z obrazów oznaczonych tagiem latest. Pozwoli to na zachowanie determinizmu przy pracy z zewnętrznym obrazami. W innym razie nigdy nie ma pewności, która wersja obrazu zostanie wykorzystana.
  • Regularnie skanuj swoje obrazy w poszukiwaniu podatności, na przykład za pomocą narzędzia Snyk.
  • Używaj lżejszych wersji zwenętrznych obrazów jeśli to możliwe. Przykładowo – w dniu publikacji artykułu, obraz dla Node.js z tagiem erbium-buster dostępny w Docker Hub ważył ponad 300MB, gdzie obraz erbium-alpine3.14 ważył zaledwie 30MB! Przy wyborze obrazu należy zadać sobie pytanie, czy któryś z lżejszych wariantów obrazów jest wystarczający.
  • Korzystaj z multistage builds. Przede wszytkim pozwala to na zmniejszenie rozmiaru ostatecznego obrazu. Oprócz tego, jest to dobra praktyka bezpieczeństwa w momencie, gdy przy budowaniu używasz tokenów bezpieczeństwa. W obrazie zapisane są informacje o każdej jego warstwie, co może spowodować wyciek tokenów bezpieczeństwa, które wykorzystano podczas budowania obrazu. Multistage build pozwala temu zapobiec. Należy jednak pamiętać, aby wrażliwe wartości nie były używane podczas ostatniego stage’a.
  • Ignoruj pliki, które nie powinny znaleźć się w obrazie za pomocą .dockerignore.
  • Minimalizuj liczbę warstw w obrazie. Ponadto, Docker potrafi cache’ować poszczególne warstwy, przez co umiejętne konstruowanie definicji obrazów pozwala znacznie skrócić czas przebudowania obrazu.

Podsumowanie

Cieszę się, że udało Ci się dotrzeć do tego miejsca. Jeśli uważasz, że pominąłem jakiś ciekawy aspekt pracy z kontenerami i Dockerem, daj mi znać o tym w komentarzu. Zachęcam też do udostępnienia tego artykułu innym jeśli okazał się on dla Ciebie przydatny. Zachęcam też do zajrzenia w linki w sekcji poniżej, jeśli pragniesz zgłębić temat konteneryzacji nieco bardziej.

Źródła i materiały dodatkowe:

 

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.