Docker: nieuleczalne porady

W komentarzach do mojego artykułu Docker: złe rady. było wiele próśb, aby wyjaśnić, co takiego strasznego znajduje się w opisywanym przeze mnie Dockerfile.

Krótka treść poprzedniego odcinka: dwóch programistów w napiętym terminie przygotowuje Dockerfile. W trakcie dołącza do nich Ops Igor Iwanowicz. Końcowy Dockerfile jest tak zły, że Igor Iwanowicz prawie dostaje zawału serca.

Docker: nieuleczalne porady

Zaraz przeanalizujemy, co jest nie tak z tym Dockerfile.

Zatem minął tydzień.

Dev Petya spotyka się w kuchni przy filiżance kawy z Opsem Igorem Iwanowiczem.

P: Igor Iwanowicz, jesteś bardzo zajęty? Chciałbym zrozumieć, gdzie popełniliśmy błąd.

II: To dobrze, rzadko spotyka się programistów, którzy interesują się eksploatacją.
Na początek ustalmy kilka rzeczy:

  1. Ideologia Dockera: jeden kontener — jeden proces.
  2. Im mniejszy kontener, tym lepiej.
  3. Im więcej pochodzi z cache, tym lepiej.

P: A dlaczego w jednym kontenerze powinien być tylko jeden proces?

II: Docker, uruchamiając kontener, śledzi stan procesu z pid 1. Jeśli proces umrze, Docker próbuje ponownie uruchomić kontener. Załóżmy, że w kontenerze działa kilka aplikacji lub główna aplikacja nie jest uruchomiona z pid 1. Jeśli proces umrze, Docker o tym nie wie.

Jeśli nie ma więcej pytań, pokaż swój Dockerfile.

I Petya pokazał:

FROM ubuntu:latest

# Kopiujemy kod źródłowy
COPY .\/ \/app
WORKDIR \/app

# Aktualizujemy listę pakietów
RUN apt-get update 

# Aktualizujemy pakiety
RUN apt-get upgrade

# Instalujemy potrzebne pakiety
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Instalujemy bundler
RUN gem install bundler

# Instalujemy nodejs, który jest używany do budowy statyki
RUN curl -sL https:\/\/deb.nodesource.com\/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Instalujemy zależności
RUN bundle install --without development test --path vendor\/bundle

# Czyścimy za sobą cache
RUN rm -rf \/usr\/local\/bundle\/cache\/*.gem 
RUN apt-get clean 
RUN rm -rf \/var\/lib\/apt\/lists\/* \/tmp\/* \/var\/tmp\/* 
RUN rake assets:precompile
# Uruchamiamy skrypt przy starcie kontenera, który uruchomi wszystko inne.
CMD ["\/app\/init.sh"]

II: Okej, rozbierzmy to po kolei. Zacznijmy od pierwszej linii:

FROM ubuntu:latest

Brałeś tag latest. Użycie tagu latest prowadzi do nieprzewidywalnych konsekwencji. Wyobraź sobie, że twórca obrazu kompiluje nową wersję obrazu z inną listą oprogramowania, ten obraz dostaje tag latest. A twój kontener w najlepszym wypadku przestaje się kompilować, a w najgorszym przypadku łapiesz błędy, których wcześniej nie było.

Zabierasz obraz z pełnoprawnym systemem operacyjnym z dużą ilością niepotrzebnego oprogramowania, co powiększa rozmiar kontenera. A im więcej oprogramowania, tym więcej luk i podatności.

Dodatkowo, im większy obraz, tym więcej miejsca zajmuje na hoście i w rejestrze (gdzieś musisz przecież przechowywać obrazy)?

P: Tak, oczywiście, mamy rejestr, przecież go konfigurowałeś.

AI: Więc, o czym to mówiłem?.. Ach tak, obrazy… Obciążenie sieci również rośnie. W przypadku pojedynczego obrazu jest to niewidoczne, ale gdy zachodzi ciągła budowa, testy i wdrażanie, to jest odczuwalne. A jeśli nie masz trybu Boga na AWS, przyjdzie ci jeszcze kosmiczny rachunek.

Dlatego trzeba wybierać najbardziej odpowiedni obraz, z dokładną wersją i minimalną ilością oprogramowania. Na przykład, weź: FROM ruby:2.5.5-stretch

P: O, rozumiem. Jak i gdzie mogę zobaczyć dostępne obrazy? Jak zrozumieć, który z nich jest mi potrzebny?

AI: Zazwyczaj obrazy pobiera się z Docker Hub, nie myl z Pornhubem :). Dla obrazu zazwyczaj istnieje kilka wydań:
Alpine: obrazy oparte są na minimalistycznym obrazie Linux, mają tylko 5 MB. Jego wadą jest to, że jest zbudowany z własną implementacją libc, standardowe pakiety nie działają. Poszukiwanie i instalacja potrzebnego pakietu zajmie sporo czasu.
Scratch: obraz bazowy, nie jest używany do budowy innych obrazów. Jest przeznaczony wyłącznie do uruchamiania binarnych danych, które są przygotowane. Idealnie nadaje się do uruchamiania aplikacji binarnych, które zawierają wszystko, co potrzebne, na przykład aplikacji w Go.
Na bazie jakiegoś systemu operacyjnego, na przykład Ubuntu lub Debian. No w tym przypadku, myślę, nie trzeba tłumaczyć.

AI: Teraz musimy zainstalować wszystkie dodatkowe pakiety i wyczyścić pamięć podręczną. Można od razu wyrzucić apt-get upgrade. W przeciwnym razie przy każdej budowie, mimo ustalonego tagu bazowego obrazu, będą powstawały różne obrazy. Aktualizacja pakietów w obrazie to zadanie opiekuna, wiąże się to ze zmianą tagu.

P: Tak, próbowałem to zrobić, udało mi się to w ten sposób:

WORKDIR /app
COPY ./ /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean 
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

AI: Nieźle, ale tu też jest nad czym popracować. Patrz, ta komenda:

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean 
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

… nie usuwa danych z finalnego obrazu, a tylko tworzy dodatkową warstwę bez tych danych. Tak powinno być:

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Ale to nie wszystko. Co tam u ciebie, Ruby? To w takim razie nie musisz kopiować całego projektu na początku. Wystarczy skopiować Gemfile i Gemfile.lock.

Przy takim podejściu bundle install nie będzie wykonywane na każdą zmianę kodów źródłowych, a tylko wtedy, gdy zostanie zmieniony Gemfile lub Gemfile.lock.

Te same metody działają również dla innych języków z menedżerem zależności, takich jak npm, pip, composer i innych opartych na pliku z listą zależności.

I wreszcie, pamiętasz, że na początku mówiłem o ideologii Dockera 'jeden kontener — jeden proces'? To oznacza, że supervisor nie jest potrzebny. Nie powinno się również instalować systemd, z tych samych powodów. Właściwie, Docker sam w sobie jest supervisorem. A gdy próbujesz uruchomić w nim kilka procesów, to tak, jakby w jednym procesie supervisora uruchamiać kilka aplikacji.
Podczas budowy stworzysz jeden obraz, a potem uruchomisz odpowiednią liczbę kontenerów, aby w każdym działał jeden proces.

Ale o tym później.

P: Wygląda na to, że zrozumiałem. Zobacz, co wyszło:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle”, “exec”, “passenger”, “start"]

A nadpiszemy uruchamianie demonów przy uruchamianiu kontenera?

AI: Tak, wszystko się zgadza. Zresztą, można użyć zarówno CMD, jak i ENTRYPOINT. A zgłębienie różnicy między nimi to twoje domowe zadanie. Na ten temat na Habrze jest dobre artykuł.

Więc kontynuujmy. Pobierasz plik do instalacji node, ale nie ma żadnej gwarancji, że znajdziesz w nim to, czego potrzebujesz. Należy dodać walidację. Na przykład tak:

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - 
    && bash setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Za pomocą sumy kontrolnej możesz sprawdzić, czy pobrałeś prawidłowy plik.

P: Ale jeśli plik się zmieni, to kompilacja się nie powiedzie.

AI: Tak, i dziwne, że to również jest zaleta. Dowiesz się, że plik się zmienił i będziesz mógł sprawdzić, co się zmieniło. Może dodano na przykład skrypt, który usunie wszystko, do czego dotrze, lub stworzy backdoor.

P: Dziękuję. Czyli ostateczny Dockerfile będzie wyglądał tak:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - 
    && bash setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle”, “exec”, “passenger”, “start"]

P: Igorze Iwanowiczu, dziękuję za pomoc. Już muszę lecieć, muszę jeszcze zrobić 10 commitów dzisiaj.

Igor Iwanowicz, zatrzymując wzrok na pospiesznym koledze, bierze łyk mocnej kawy. Po kilku sekundach rozmyślań o SLA 99,9% i kodzie bez błędów, zadaje pytanie.

AI: A gdzie przechowujecie logi?

P: Oczywiście, w production.log. A tak po prawdzie, jak dostaniemy do nich dostęp bez ssh?

AI: Jeśli zostawisz je w plikach, rozwiązanie już zostało wymyślone. Komenda docker exec pozwala wykonać dowolną komendę w kontenerze. Na przykład, możesz użyć cat do logów. A używając opcji -it i uruchamiając bash (jeśli jest zainstalowany w kontenerze), uzyskasz interaktywny dostęp do kontenera.

Ale nie warto przechowywać logów w plikach. Co najmniej prowadzi to do niekontrolowanego wzrostu rozmiaru kontenera, logi nikt nie rotuje. Wszystkie logi powinny trafiać do stdout. Tam można je już oglądać za pomocą komendy docker logs.

P: Igorze Iwanowiczu, może wynieśmy logi do zamontowanego katalogu, na fizyczną nodę, jak dane użytkowników?

AI: Dobrze, że nie zapomniałeś o danych załadowanych na dysk nody. Z logami też tak można, tylko nie zapomnij skonfigurować rotacji.
Wszystko, możesz biec.

P: Igorze Iwanowiczu, co byś polecił do przeczytania?

II: Na początek przeczytaj zalecenia od twórców Dockera, mało kto zna Dockera lepiej od nich.

A jeśli chcesz zdobyć praktyczne doświadczenie, wybierz się na intensywne szkolenie. W końcu teoria bez praktyki jest martwa.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster