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

Сега ще разберем какво не е наред с този Dockerfile.
И така, измина седмица.
Dev Петя се среща в столовата на чаша кафе с Ops Игор Иванович.
П: Игор Иванович, много ли сте зает? Бих искал да разбера къде сбъркахме.
ИИ: Това е добре, не често срещаш разработчици, които се интересуват от експлоатацията.
Първо, да се разберем за някои неща:
- Идеология на Docker: един контейнер — един процес.
- Колкото по-малък е контейнерът, толкова по-добре.
- Колкото повече се взема от кеша, толкова по-добре.
П: Защо в един контейнер трябва да има само един процес?
ИИ: 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 Мб. Неговият минус: той е събран с собствена реализация на 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, но при това няма никаква гаранция, че в него ще има това, което ти е нужно. Трябва да добавиш валидация. Например, така:
Изпълнете 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"]П: Игор Иванович, благодаря за помощта. Вече трябва да бягам, трябва да направя още 10 комита за днес.
Игор Иванович, спирайки любопитния си колега с поглед, прави глътка от силното кафе. След като размисли няколко секунди за SLA 99.9% и код без бъгове, задава въпрос.
ИИ: А къде съхранявате логовете?
П: Разбира се, в production.log. Между другото, как ще получим достъп до тях без ssh?
ИИ: Ако ги оставите в файлове, ваше решение вече е измислено. Командата docker exec позволява изпълнението на всяка команда в контейнера. Например, можете да направите cat за логовете. А, използвайки ключа -it и стартирайки bash (ако е инсталиран в контейнера), ще получите интерактивен достъп до контейнера.
Но не трябва да съхранявате логовете в файлове. Най-малкото, това води до неконтролируемо нарастване на контейнера, логовете никой не ротуира. Всички логове трябва да се изхвърлят в stdout. Там можете да ги видите с помощта на командата docker logs.
П: Игор Иванович, а може би да изнесем логовете в монтирана директория, на физическата нода, както данните на потребителите?
ИИ: Добре е, че не забравихте да изнесете данните, записани на диска на нодовете. С логовете може да се направи същото, само не забравяйте да настроите ротация.
Всичко, можеш да бягаш.
П: Игор Иваков, какво бихте препоръчали да прочета?
ИИ: За начало прочети , малко вероятно е някой да знае Docker по-добре от тях.
А ако искаш да преминеш практика, заповядай на . Все пак теорията без практика е мъртва.
Източник: habr.com
