Distroless Images
Na początku, gdy zaczęto używać kontenerów, większość z nas traktowała je jako bardziej wydajne VM-ki. Jeżeli spojrzymy na obrazy na Docker Hubie, publikowane przez dostawców systemów operacyjnych, możemy dostrzec, że w większości przypadków kopiują oni do obrazów to, co do tej pory dostarczali jako cały system operacyjny. Zwykle, obrazy są okrajane tylko o niektóre, niepotrzebne ich zdaniem paczki / binarki.
Pomimo tego, obrazy nadal są za ciężkie, zajmując na dysku setki megabajtów. To spowodowało wzrost popularności dystrybucji takich jak Alpine. Czy kiedykolwiek wcześniej wykorzystywałeś Alpine, zanim zaczęto go używać w obrazach dockerowych? Ja nie.
Dlaczego zaczęto stosować Alpine Linux do obrazów bazowych?
Głównym powodem, dla którego zaczęto na potęgę wykorzystywać Alpine, jest oczywiście jego rozmiar. 5 MB — tyle waży obraz Alpine. W porównaniu do np. Debiana (125 MB) lub Ubuntu (188MB) jest to spora różnica. W pewnym momencie, z uwagi na rozmiar obrazów, Docker Hub zaczął mieć problemy wydajnościowe. Te problemy z czasem zostały zażegnane, lecz popularność Alpine nadal rosła.
Pojawia się zatem pytanie. Co jest złego w stosowaniu Alpine Linux?

Wady Alpine Linux
Pomimo że Alpine Linux jest ultralekki, nadal jego zachowanie może być porównane do wirtualnej maszyny. Dlaczego?
Stosując obraz Alpine, nadal możemy korzystać z shell’a a nawet z managera pakietów. No właśnie, manager pakietów…
1. Brakujące pakiety
Zdarzały się przypadki, w których brakowało podstawowych pakietów. Coś, co innych dystrybucjach takich jak Debian mamy pewne. Przykład – brakująca paczka MySQL.
2. Różnice w repozytoriach dla różnych wersji Alpine
Każda wersja Alpine Linux posiada własny manager pakietów i niestety nie przechowuje archiwalnych wersji. Przykładowo w wersji Alpine 3.5, dostępna paczka Node.js może być w wersji 2.0, w Alpine 3.4 będzie już w wersji 1.9. Chcąc używać Node.js w wersji 1.9, musimy opierać się o starszą wersje Alpine.
3. Brak glibc
Jeśli cokolwiek w życiu kompilowałeś (np. na studiach) to zakładam że w 90% przypadków korzystałeś z glibc. Alpine zastąpił glibc własnym stworem o nazwie musl. Osobiście, natknąłem się na ten problem w momencie w którym aplikacja webowa uruchomiona w kontenerze na bazie Debiana działała poprawnie, a wewnątrz kontenera na bazie Alpine, już nie
Jeżeli chcesz by Twoja aplikacja działała poprawnie, musisz skompilować ją w środowisku wykorzystującym musl.
4. Stabilność
Obecnie obraz dockerowy Alpine Linux jest rozwijany jako projekt open-source.
Poszperałem nieco tu i ówdzie i co znalazłem?
Okazuje się, że aktywnie rozwija go zaledwie kilka osób. A do zatwierdzenia zmiany w repozytorium wystarczą dwie (!), powtarzam, dwie osoby (na dzień pisania tego artykułu). Przykład pull requesta — TUTAJ
Czy w swoich obrazach, chcesz bazować na czymś, czego jakość sprawdza dwie osoby?
Zauważyłem też, że repozytorium z powyższego linka, w którym widnieją osoby rozwijające, zostało przeniesione do https://github.com/alpinelinux/docker-alpine, gdzie kontrybutorem jest tylko jedna osoba. Na Docker Hubie widnieje już adres nowego repozytorium Githuba. Dlaczego?
5. Wydajność
Szukając informacji o wydajności Alpine, natrafiłem na post na Reddicie, w którym mowa o aplikacji Node.js. Jeden z użytkowników twierdzi, że jego aplikacja działała 15% wolniej używając Alpine jako obraz bazowy w porównaniu do Debiana.
Inny przykład to aplikacja w Pythonie. Bardzo ciekawy artykuł opisujący wady Alpine Linux w porównaniu do Ubuntu.
6. Bezpieczeństwo
Nie wiele narzędzi do skanowania obrazów “radzi” sobie z obrazami bazującymi na Alpine. Jeżeli skanowałeś obrazy narzędziami takimi jak Anchore czy Clair, które nie wykryły żadnych podatności – polecałbym porównać wyniki skanowania z wynikami uzyskanymi narzędziem trivy.
Distroless Docker Images
W związku z problemami z rozmiarem obrazów oraz wyżej wymienionymi wadami Alpine Linux nie kto inny jak firma Google, zaprezentowała światu Distroless Images.
Nie muszę chyba przypominać, że to jak zachowuje się nasz kontener, zależy od obrazu, na podstawie którego został utworzony. Co zatem takiego kryje się pod słowem distroless?
Ideą distroless jest zbudowanie minimalnego obrazu zawierającego tylko aplikację i jej zależności.
$ docker pull gcr.io/distroless/base
Rozmiar powyższego obrazu to tylko ~16 MB.
Obraz jest pozbawiony managera pakietów, shell’a (!) oraz innych programów, które możesz znaleźć w tradycyjnej dystrybucji.
UWAGA: brak shella oznacza, że nie mamy możliwości wejścia do kontenera poprzez wykonanie polecenia docker exec -it <container> sh
$ docker run -it gcr.io/distroless/base /bin/sh docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown
Brak shell’a został wprowadzony celowo, z uwagi na bezpieczeństwo. Często przekazujemy sekrety do kontenera jako zmienne środowiskowe. Może być to na przykład connection string do bazy danych. W tradycyjnych obrazach atakujący może go wyłuskać, wchodząc do kontenera i podglądnąć zmienne środowiskowe.
Pojawia się zatem problem – Jak można debuggować kontener, jeżeli nie ma shella?
W razie takiej potrzeby, jednym z podejść jest stworzenie kontenera bazującego na obrazie z tagiem :debug. Obrazy z tym tagiem, mają wbudowany shell.
Przykłady obrazów distroless
Obrazy distroless bazują na na Debianie (wersja 9 i 10). Wszystkie gotowe obrazy możemy znaleźć w oficjalnym repozytorium Google – gcr.io.
- gcr.io/distroless/static-debian10
- gcr.io/distroless/base-debian10
- gcr.io/distroless/java-debian10
- gcr.io/distroless/cc-debian10
Oprócz “czystych” obrazów, możemy znaleźć obrazy dedykowane do języków/technologii. Jednak według Google’a, należy je stosować z rozwagą (na czas pisania tego artykułu, nie są rekomendowane na produkcję)
- gcr.io/distroless/python2.7-debian10
- gcr.io/distroless/python3-debian10
- gcr.io/distroless/nodejs
- gcr.io/distroless/java/jetty-debian10
- gcr.io/distroless/dotnet
Tworzenie własnych obrazów w oparciu o distroless
Chcąc samemu tworzyć obrazy distroless, możemy użyć dwóch podejść.
Pierwszym podejściem jest wykorzystanie Docker multi-stage builds. Wystarczy zastosować się do dwóch kroków:
- Wybierz obraz bazowy odpowiadający Twojej aplikacji
- Stwórz Dockerfile w trybie multi-stage build
Poniżej znajdziesz Dockerfile budujący obraz distroless dla aplikacji Node.js.
FROM node:10.17.0 AS build-env ADD . /app WORKDIR /app FROM gcr.io/distroless/nodejs COPY --from=build-env /app /app WORKDIR /app CMD ["hello.js"]
Budujemy obraz tradycyjnie:
$ docker build . -t myapp:distroless
Druga metoda na tworzenie obrazów distroless to użycie narzędzia Bazel.
Podstawową kwestią jest stworzenie pliku BUILD. Poniżej znajduję się zawartość pliku BUILD. Cały przykład, wraz z plikami *.js znajdziesz TUTAJ
package(default_visibility = ["//visibility:public"]) load("@io_bazel_rules_docker//container:container.bzl", "container_image") # These examples are adapted from: # https://howtonode.org/hello-node container_image( name = "hello", base = "//experimental/nodejs:nodejs", cmd = ["hello.js"], files = [":hello.js"], ) container_image( name = "hello_http", base = "//experimental/nodejs:nodejs", cmd = ["hello_http.js"], files = [":hello_http.js"], ports = ["8000"], ) load("@io_bazel_rules_docker//contrib:test.bzl", "container_test") container_test( name = "hello_test", configs = ["testdata/hello.yaml"], image = ":hello",
Podsumowanie
Pomimo lekkości obrazu Alpine, okazuje się że nie jest on taki idealny. Licznie opublikowane problemy z wydajnością oraz braki niektórych pakietów mogą dawać do myślenia.
Alternatywą są obrazy distroless, które oprócz lekkości, zapewniają bezpieczeństwo. Ich atutem jest fakt, że są rozwijane przez Google oraz stosowane w rozwiązaniach produkcyjnych wraz z Kubernetes.
Na sam koniec, jeśli zastanawiasz się co wybrać – Distroless vs Alpine, wystarczy odpowiedzieć sobie na pytanie: Czy wolisz stosować coś co jest rozwijane przez firmę Google czy też coś nad czym czuwają dwie osoby?
Dla mnie odpowiedź jest prosta.