Docker: не вредни съвети

В коментарите под моята статия Docker: вредни съвети Имаше много искания да обясня, с какво е толкова ужасен описаният в нея Dockerfile.

Резюме на предишния епизод: двама разработчици в критичен срок съставят Dockerfile. По време на процеса при тях идва Ops Игор Иванович. Резултатният Dockerfile е толкова лош, че ИИ е на ръба на инфаркта.

Docker: не вредни съвети

Сега да видим какво не е наред с този Dockerfile.

И така, измина седмица.

Dev Петя се среща в столовата за чаша кафе с Ops Игор Иванович.

П: Игор Иванович, много ли сте зает? Искам да разбера къде сме сбъркали.

ИИ: Добре, не се срещат често разработчици, които се интересуват от експлоатацията.
Първо, нека да се договорим относно някои неща:

  1. Идеология на Docker: един контейнер — един процес.
  2. Колкото по-малък е контейнерът, толкова по-добре.
  3. Колкото повече се взима от кеша, толкова по-добре.

П: Но защо в един контейнер трябва да има един процес?

ИИ: Docker следи състоянието на процеса с pid 1, когато стартира контейнер. Ако процесът умре, Docker опитва да рестартира контейнера. Да предположим, че в контейнера са стартирани няколко приложения или основното приложение не се стартира с pid 1. Ако процесът умре, Docker няма да разбере за това.

Ако нямате повече въпроси, покажете вашия Dockerfile.

И Петя показа:

FROM ubuntu:latest

# Копираме изходния код
COPY ./ /app
WORKDIR /app

# Обновяваме списъка с пакети
RUN apt-get update 

# Обновяваме пакетите
RUN apt-get upgrade

# Инсталираме необходимите пакети
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Инсталираме bundler
RUN gem install bundler

# Инсталираме nodejs, използва се за изграждане на статиката
RUN curl -sL https://deb.nodesource.com/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Инсталираме зависимостите
RUN bundle install --without development test --path vendor/bundle

# Почистваме кеша
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
# Стартираме скрипта при старта на контейнера, който ще стартира всичко останало.
CMD ["/app/init.sh"]

ИИ: Ох, нека да разгледаме нещата по ред. Да започнем с първия ред:

FROM ubuntu:latest

Вие вземате таг latest. Използването на тага latest води до непредсказуеми последствия. Представете си, че поддържачът на образа компилира нова версия на образа с различен списък от софтуер, този образ получава таг latest. И вашият контейнер в най-добрия случай спира да се компилира, а в най-лошия ловите бъгове, които преди това не сте имали.

Вие взимате образ с пълноценна ОС, която включва много ненужен софтуер, което увеличава размера на контейнера. И колкото повече софтуер, толкова повече уязвимости.

Освен това, колкото по-голям е образът, толкова повече място заема на хостинга и в registry (вие я настройвахте, нали?).

П: Разбира се, имаме registry.

ИИ: Та, за какво говорех...? Ах да, размерите... Нараства и натоварването на мрежата. За един образ това не е заметно, но когато става дума за непрекъсната сборка, тестове и деплой, разликата е осезаема. А ако нямате God’s mode на AWS, може да получите и космическа сметка.

Затова е важно да избирате най-подходящия образ, с конкретна версия и минимално количество софтуер. Например, вземете: FROM ruby:2.5.5-stretch

П: О, разбирам. Как мога да видя наличните образи? Как да разбера кой ми е необходим?

ИИ: Обикновено образите се вземат от докерхаб, не го бъркайте с порнхаб :). За образа обикновено съществуват няколко сборки:
Alpine: образи, построени на минималистичната Linux система, само 5 MB. Недостатъкът е, че е изградена с собствена реализация на libc, стандартните пакети не работят. Времето за намиране и инсталиране на необходимия пакет ще бъде значително.
Scratch: основен образ, не се използва за изграждане на други образи. Той е предназначен изключително за стартиране на бинарни, подготвени данни. Идеален е за изпълнение на бинарни приложения, които включват всичко необходимо, например go-приложения.
На база на някоя ОС, напр. Ubuntu или Debian. Тук, предполагам, не е нужно да обяснявам.

ИИ: Сега трябва да инсталираме всички допълнителни пакети и да почистим кешовете. И веднага можем да изтрием apt-get upgrade. В противен случай, при всяка сборка, независимо от фиксирания таг на базовия имидж, ще получавате различни образи. Актуализирането на пакетите в образа е задача на мейнтейнера и крие промяна на тага.

П: Да, опитвах се да го направя, ето какво постигнах:

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/*

ИИ: Не зле, но и тук има какво да се подобри. Виж, тази команда:

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

… не изтрива данни от окончателния образ, а просто създава допълнителен слой без тези данни. Правилно е така:

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/* 

Но това не е всичко. Какво имаш там, Ruby? Тогава не е необходимо в началото да копираш целия проект. Достатъчно е да копираш Gemfile и Gemfile.lock.

При такъв подход bundle install няма да се изпълнява за всяка промяна в изходниците, а само ако Gemfile или Gemfile.lock са променени.

Същите методи работят и за други езици с мениджъри на зависимости, като npm, pip, composer и други, базирани на файл със списък на зависимостите.

И накрая, помниш ли, в началото споменах идеологията на Docker "един контейнер — един процес"? Това означава, че supervisor не е необходим. Не трябва и да инсталираш systemd, по същите причини. Всъщност, Docker сам по себе си е supervisor. И когато опиташ да стартираш няколко процеса в него, е все едно в един процес на supervisor да стартираш няколко приложения.
При изграждането ще направиш единен образ, а след това ще стартираш необходимия брой контейнери, за да работи по един процес в всеки.

Но за това по-късно.

В: Изглежда, че разбрах. Виж какво се получава:

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"]

А как ще преопределим стартирането на демоните при стартиране на контейнера?

ИИ: Да, точно така. Между другото, можеш да използваш както CMD, така и ENTRYPOINT. А да разбереш в какво е разликата, това е твоето домашно. На тази тема в Хабра има добра статия. статия.

И така, да продължим. Сваляш файла за инсталиране на node, но по този начин няма никаква гаранция, че в него ще има това, от което имаш нужда. Трябва да добавиш валидация. Например, така:

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/* 

С помощта на контролната сума можеш да провериш дали файлът е изтеглен правилно.

П: Но ако файлът се промени, сглобката няма да мине.

ИИ: Да, и, странно, това е плюс. Ще разбереш, че файлът се е променил и ще можеш да видиш какво е променено. Възможно е, например, да са добавили скрипт, който изтрива всичко, до което успее да се добере, или създава задна вратичка.

П: Благодаря. Значи, финалният Dockerfile ще изглежда така:

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"]

П: Игор Ивaнович, благодаря за помощта. Вече трябва да бягам, трябва да направя още 10 коммита днес.

Игор Ивaнович, спирайки нетърпеливия си колега с поглед, взима глътка от силното кафе. Следвайки няколко секунди относно SLA 99.9% и код без грешки, задава въпрос.

ИИ: Къде съхранявате логовете?

П: Разбира се, в production.log. Между другото, как ще получим достъп до тях без ssh?

ИИ: Ако ги оставите в файлове, за вас вече е намерено решение. Командата docker exec позволява да изпълните всяка команда в контейнера. Например, можете да направите cat за логовете. А като използвате ключа -it и стартирате bash (ако е инсталиран в контейнера), ще получите интерактивен достъп до контейнера.

Но е по-добре да не се съхраняват логовете в файлове. Минимум, това води до неконтролируемо нарастване на контейнера, нали логовете не се ротират. Всички логове трябва да се изпращат в stdout. Там вече могат да бъдат разгледани с командата docker logs.

П: Игор Ивaнович, а какво ще кажете да изнесем логовете в монтирана директория, на физическа нода, подобно на потребителските данни?

ИИ: Добре, че не забравихте да изнесете данните, натоварени на диска на нодата. С логовете също е възможно, само не забравяйте да настроите ротацията.
Всичко, можеш да бягаш.

П: Игор Ивaнович, какво можете да препоръчате за четене?

ИИ: За начало прочетете препоръките на разработчиците на Docker, едва ли някой друг познава Docker по-добре от тях.

А ако искате да направите практика, запишете се на интензивен курс. Все пак теорията без практика е мъртва.

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster