Хабрахабр

Docker: не вредные советы

В комментариях к моей статье Docker: вредные советы было много просьб объяснить, чем так ужасен описанный в ней Dockerfile.

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

Сейчас разберемся, что не так с этим Dockerfile.

Итак, прошла неделя.

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

Хотелось бы разобраться, где мы напортачили. П: Игорь Иванович, вы сильно заняты?

ИИ: Это хорошо, не часто встретишь разработчиков, которых интересует эксплуатация.
Для начала давай договоримся о некоторых вещах:

  1. Идеология Docker: один контейнер — один процесс.
  2. Чем меньше контейнер, тем лучше.
  3. Чем больше берется из кэша, тем лучше.

П: А почему в одном контейнере должен быть один процесс?

Если процесс умирает, Docker пытается перезапустить контейнер. ИИ: Docker при запуске контейнера отслеживает состояние процесса с pid 1. Если процесс умрет, Docker об этом не узнает. Допустим, у вас в контейнере запущено несколько приложений или основное приложение запущено не с pid 1.

Если больше нет вопросов, показывай ваш 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-stretch 5.

А как и где посмотреть имеющиеся образы? П: О, понятно. Как понять, какой мне нужен?

Для образа обычно существует несколько сборок:
Alpine: образы собраны на минималистичном образе Linux, всего 5 Мб. ИИ: Обычно образы берутся с докерхаба, не путай с порнхабом :). На поиск и установку нужного пакета уйдет немало времени.
Scratch: базовый образ, не используется для сборки других образов. Его минус: он собран с собственной реализацией libc, стандартные пакеты в нем не работают. Идеально подойдет для запуска бинарных приложений, которые включают в себя все необходимое, например 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 и других базирующихся на файле со списком зависимостей.

Это означает, что supervisor не нужен. Ну и наконец, помнишь, в начале я говорил про идеологию Docker «один контейнер — один процесс»? По сути, Docker сам является supervisor. Так же не стоит устанавливать systemd, по тем же причинам. И когда ты пытаешься запускать в нем несколько процессов, это как в одном процессе 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"]

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

Поразмыслив несколько секунд об SLA 99. Игорь Иванович, взглядом остановив торопливого коллегу, делает глоток крепкого кофе. 9% и коде без багов, он задает вопрос.

ИИ: А где вы логи храните?

Кстати да, а как мы без ssh получим к ним доступ? П: Конечно, в production.log.

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

Как минимум это приводит к бесконтрольному росту контейнера, логи же никто не ротирует. Но хранить логи в файлах не стоит. Там их уже можно посмотреть с помощью команды docker logs. Все логи нужно кидать в stdout.

П: Игорь Иванович, а может вынести логи в смонтированную директорию, на физическую ноду, как данные пользователей?

С логами так тоже можно, только не забудь настроить ротирование.
Все, можешь бежать. ИИ: Хорошо, что вы не забыли вынести данные, загруженные на диск ноды.

П: Игорь Иванович, а посоветуйте, что почитать?

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть