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 Мб. Неговият минус: той е събран с собствена реализация на 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, малко вероятно е някой да знае Docker по-добре от тях.

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

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

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