7 rzeczy, których warto unikać przy konteneryzacji
W poprzednim artykule przekonywałem na czym może skorzystać branża telekomunikacyjna przy przejściu na kontenery. Tematem wiodącym był czas. Dziś parę porad, które także pozwolą zaoszczędzić czas przy samym procesie konteneryzacji.
Spis treści
Wielkość ma znaczenie, a podstawa to… podstawa
Przy deploymencie aplikacji w ramach jednego lub dwóch środowisk takich jak Kubernetes czy Docker wielkość obrazu może nie mieć takiego znaczenia. Gdy jednak potrzeba zainstalować ten sam obraz na setkach czy tysiącach urządzeń albo aplikacja ma wiele instancji w ramach środowiska, każdy megabajt jest na wagę złota. Z takimi sytuacjami spotykamy się szczególnie w telekomunikacji i tym samym w Ericsson. Poniżej parę rzeczy, których powinno się unikać, aby nie tworzyć “potworów”.
Wybieranie podstawy do obrazu bez sprawdzenia jej rozmiarów
Załóżmy, że mamy aplikację, która działa wyśmienicie na wirtualnej maszynie opartej na dystrybucji Ubuntu. Jako że “dockery” są i tak mniejsze od takiej maszynki to najprościej jest wybrać po prostu obraz oparty na Ubuntu. I tutaj pierwszy błąd, różnice w wielkości końcowego obrazu mogą być nawet o rząd wielkości.
Poniżej zamieściłem przykłady Dockerfile dla dwóch różnych dystrybucji Linux’a Ubuntu i Alpine obrazu aplikacji z poprzedniego artykułu:
Przykładowy Dockerfile w oparciu o dystrybucję Alpine:
FROM alpine:3.7 RUN apk --no-cache --no-progress add python python-dev py-pip build-base && pip install --no-cache-dir nameko && apk del python-dev py-pip build-base COPY http.py config.yaml / EXPOSE 8000 ENV RABBITMQ_HOST=localhost CMD nameko run --config config.yaml http
Dockerfile w oparciu o Ubuntu:
FROM ubuntu:18.04 RUN apt-get update -y && apt-get install -y --no-install-recommends netbase python python-pip python-setuptools && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir nameko && apt-get remove -y python-pip python-setuptools && apt-get autoremove -y COPY http.py config.yaml / EXPOSE 8000 ENV RABBITMQ_HOST=localhost CMD nameko run --config config.yaml http
Efekt można zobaczyć w poniższej tabelce, prawie dwukrotnie mniejszy obraz:
Wielkość obrazu wynikowego
Ubuntu 18.04 | Alpine 3.07 |
112 MB | 61.3 MB |
Jeszcze bardziej drastycznie widać to na aplikacjach napisanych w Golang, gdzie podstawowy obraz jest pozbawiony nawet shell’a i waży zaledwie parę megabajtów.
Podsumowując dobra podstawa to podstawa.
Zbyt słaba kontrola menadżera pakietów
Przy tworzeniu własnych obrazów istotna jest dobra kontrola menadżera pakietów tak, aby przy okazji nie zainstalował on dodatkowych nieużywanych pakietów. W przykładzie powyżej została użyta opcja --no-install-recommends
, która jak sama nazwa wskazuje, blokuje takie zakusy w przypadku apt-get
. Oczywiście w celu dalszej oszczędności miejsca dobrze jest posprzątać po sobie, stąd kasowanie ściągniętych paczek: rm -rf /var/lib/apt/lists/*
. W przypadku apk
została użyta opcja --no-cache
, która załatwia nam sprawę ściągania pakietów na dysk. Podobnie dla paczek Pythona dobrym pomysłem jest stosowanie parametru --no-cache-dir
wraz z komendą pip
. Podsumowując, poznaj dobrze swojego menadżera.
Wybieranie podstawy obrazu tylko z uwagi na wielkość
Teraz przestroga dla fanów dystrybucji Alpine. Bo skoro rozmiar ma znaczenie, to czemu nie przerzucić się na tę dystrybucję? Mimo że faktycznie jest mała, to kosztem kompatybilności z glibc. Ma to olbrzymi wpływ na stabilność niektórych aplikacji. Dodatkowo stosowanie tej dystrybucji dla aplikacji w Pythonie może się zemścić czasem tworzenia kontenera. Pakiety binary wheel kompilowane są dla glibc – efekt – wszystkie pakiety Python’a z koniecznością kompilacji kodu w C muszą być kompilowane ze źródeł.
A prócz samego czasu kompilacji należy do tego dorzucić czas szukania wszystkich zależności koniecznych żeby kompilator nie wypluł na sam koniec błędu. Więcej na ten temat można poczytać tutaj.
Co robić? Jak żyć, gdy zależy nam bardzo na wielkości? Proponuje uśmiechnąć się do “bezdystrybucyjnych” obrazów Google (ang. distroless), które nie dość, że są małe to i bezpieczniejsze, bo nie zawierają nawet shell’a.
Brak kontroli kontekstu podczas budowy obrazu
Podczas budowy obrazu istotny jest zdefiniowany kontekst. Zwykle jest to katalog, w którym znajduje się plik Dockerfile. To właśnie z kontekstu można skopiować do tworzonego obrazu pliki. Gdy się o tym zapomni, może się okazać, że “pchamy” gigabajty danych do demona Docker’a w postaci kontekstu, z którego wykorzystujemy raptem parę kilobajtów. Efekt? Niepotrzebne wykorzystujemy zasoby sprzętowe i tracimy czas w pętlach CI/CD.
Do lepszej kontroli, co powinno znaleźć się w kontekście służy .dockerignore o składni rodem z .gitignore.
Nie dostosowanie systemu logowania do “nowego świata”
Standardowo aplikacje zawsze coś powinny logować i zwykle robią to do plików. Niestety takie podejście staje się problematyczne w przypadku kontenerów, bo jak się dostać do pliku wewnątrz kontenera? Oczywiście można rozwiązać problem na wiele “tradycyjnych” sposobów – użyć serwer syslog’a, podmontowywać zasoby dyskowe pod pliki logów, etc. Na szczęście jest zdecydowanie lepszy sposób – system logowania Docker’a.
W kontenerze logowane jest “standardowe wyjście” (dokładniej stdout i stderr) uruchomionej aplikacji. Potrzebujemy zatem tylko skonfigurowania aplikacji do wysyłania logów na standardowe wyjście.
Potem to już bajka – mamy zunifikowany dostęp do logów z poziomu komend CLI Dockera lub jego RESTowego API, jak również środowiska Kubernetes, itd. Można przetwarzać z łatwością dalej te logi np. korzystając z Fluentd i gromadzić je zaindeksowane w bazach danych tj. Elasticsearch, etc.
Mam nadzieję, że nie muszę tu przekonywać, że centralizacja logów i ich normalizacja w przypadku rozproszonych architektur znacząco ułatwia wyszukiwanie problemów w systemie.
Uruchomienie wielu aplikacji w ramach jednego kontenera
Przy okazji opisanego wyżej logowania pojawia się problem – co zrobić, gdy mamy uruchomioną w ramach kontenera więcej niż jedną aplikację. Jak logować na jedno “standardowe wyjście” w sposób czytelny logi z wielu aplikacji? Jest na to stosunkowo proste rozwiązanie – nie robić tego!
Co do zasady kontener powinien być stworzony tylko do uruchomienia jednej aplikacji. Można się szczególnie o tym przekonać, gdy trzeba się trochę “nahakować” żeby odpalić dodatkowe aplikacje w tle. Jest to “ograniczenie” zaprojektowane z premedytacją, aby tworzyć bardziej skalowalne/bezpieczne rozwiązania. Więc dobrze się tej zasady trzymać, nie tylko z uwagi na logi.
Tworzenie obrazów na podstawie publicznych repozytoriów
To akurat tyczy się nie tylko konteneryzacji, ogólnie dobry zwyczaj to unikanie zależności w pętlach CI/CD od zewnętrznych repozytoriów. Proces tworzenia obrazów kontenerów nie jest tu wyjątkiem. Ot gdy nagle coś się stanie z zewnętrznym serwisem, czy też wykorzystywanym obrazem to cały nasza maszyneria może stanąć. Polecam zatem odciąć się od takich zależności tworząc kopie bazowych obrazów. Tak, żeby w pełni kontrolować procesy CI/CD po naszej stronie.
Podsumowanie
Jak widać po powyższych tematach, inwestycja własnego czasu w nauczenie się nowego środowiska zwykle procentuje w… oszczędności czasu później. W tym przypadku dotyczy to zarówno procesów CI/CD, jak i dalszej rozbudowy architektury, czego Wam gorąco życzę. Natomiast z punktu widzenia biznesowego dla Ericsson jest to przede wszystkim skrócenie czasu dostarczenia produktu (ang. time to market). Biorąc pod uwagę jak ważny jest to temat zachęcam do poszerzenia swojej wiedzy na temat tworzenia poprawnych plików Dockerfile chociażby w artykule Najlepsze praktyki tworzenia Dockerfile.
Zdjęcie główne artykułu pochodzi z unsplash.com.