В этом мире контейнеров многие изображения определяются через файлы Dockerfile. Некоторые простые, некоторые сложные

Абстрактный

Проблемы

Сложные файлы Dockerfiles обычно содержат большие и длинные команды RUN.
Когда вы смотрите на них, ваш мозг начинает крутиться. Таким образом, мы получаем нашу первую проблему: понятность длинных файлов Docker.

Пример ссылки A. (вы можете оставить ее открытой, я буду ссылаться на нее):

Https://raw.githubusercontent.com/nginxinc/docker-nginx/5971de30c487356d5d2a2e1a79e02b2612f9a72f/mainline/buster/Dockerfile

Другая проблема: конечные образы обычно должны иметь как можно меньше слоев ради размера образа, в то время как во время разработки вам нужно больше слоев, чтобы использовать функции кэширования Docker при исправлении некоторых из ваших скриптов. Противоположные направления…

И еще одна проблема: возможность повторного использования.

Давай попробуем сделать лучше

В этой статье я рассмотрю один довольно простой способ улучшить вышеперечисленные проблемы.

Вы получите удобный для разработки способ создания файлов Docker с многократно используемыми фрагментами в различных файлах Docker. Это может привести к созданию более качественных файлов Docker и автоматической генерации небольшого конечного изображения при использовании кешей слоев во время разработки.

И я думаю, что это можно улучшить, добавив дополнительные преимущества.

Примечание. Это не вводный урок по контейнерам, Docker или Dockerfiles. Для этого есть много других хороших ресурсов.

Код можно найти в:

Давайте посмотрим …

Начнем наше путешествие. Мы здесь: мы должны написать Dockerfile для нашего супер-убийственного… проекта… эээ… контейнера.

Познавательная нагрузка

В зависимости от потребностей этот файл Dockerfile может быть:

  • очень простой, более или менее состоящий из пары команд RUN, пары COPY, ENTRYPOINT и вуаля! Готово, время для мороженого или кофе!
  • большие и сложные, с командами RUN, которые делают много - обновляют данные репозитория пакетов, загружают ключи GPG, вносят пакеты, устанавливают файлы конфигурации и так далее. См. Снова ссылку A , выше. Мороженое может подождать, принеси кофе.

Итак, нам предстоит написать длинный. А потом мы снова хотим забрать его без суеты, верно? Мы не хотим попадать в ситуацию «напиши один раз, не прочитай никогда» (я уже много лет программировал на Perl, спасибо.)

Конечно, мы всегда будем сталкиваться с большими кодовыми базами, но мы всегда хотим легко понять, о чем они. И эти большие команды RUN Dockerfile точно не помогают, верно?

Здесь откройте другой пример, Ссылка B:

Https://github.com/docker-library/cassandra/blob/master/Dockerfile.template.

Я недавно столкнулся с этим по ошибке. (К счастью, у них есть хоть какие-то комментарии - спасибо!)

Оптимизация - количество слоев

Приведем еще один аспект: оптимизации. Вы, наверное, уже знаете, что лучше иметь изображения как можно меньшего размера. И вы знаете, что каждая команда Dockerfile (включая RUN) создает новый слой.

Когда вы сохраняете или переносите изображение, все слои сохраняются / передаются - это означает, что если в одной команде RUN вы устанавливаете какой-то вспомогательный инструмент, который не нужен в конечном образе, а позже, в другой команде RUN вы удаляете его, он действительно делает не имеет значения. Эти данные по-прежнему будут в вашем изображении, но не на последнем слое.

Совет: из-за этого с точки зрения безопасности вы не должны использовать секретные данные на одном из слоев, а затем удалять их, потому что эта информация на самом деле все еще существует!

Примечание: слои не обязательно плохи сами по себе (подлежат дальнейшему обсуждению), но когда они также содержат данные, это становится проблематичным.

Хорошо, мы получили - как можно меньше слоев! Так как же это сделать?

Вы берете все или почти все свои команды RUN и объединяете их в один слой или в меньшее количество команд. Вы видели этот большой текст со всеми строками, начинающимися с && и заканчивающимися \, верно? Нет? Вы не открыли Ссылку А?

Вот более простой вымышленный пример ниже, основанный на начальной точке образа Debian / Ubuntu:

RUN \
    apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install apt-transport-https ca-certificates \
    && apt-key adv --keyserver SERVER --recv-keys KEY_ID \
    && DEBIAN_FRONTEND=noninteractive apt-get install some_packages \
    && sed -e s/something/otherthing/ \
    && rm somefile \
    && mkdir someDir \
    && run_a_script with_some_parameters \
    && repeat_above a_few_times

Это должно быть легче читать. Без комментариев не так просто увидеть, где разделы начинаются и заканчиваются, и что каждый делает.

Docker действительно предлагает альтернативу проблеме возможного наличия всевозможного мусора в нашем конечном образе из-за использования нескольких уровней: многоступенчатые сборки. Однако, если вы обратите пристальное внимание на то, что он делает, кажется, что его можно использовать в основном, когда результат ваших этапов сборки состоит только из одного-трех каталогов. То есть вам легко перечислить их, чтобы вы могли скопировать их из образов докеров многослойного здания в окончательный.

Если вы попали в такую ​​ситуацию, то это хорошо для вас: вы устранили проблему с окончательным размером изображения и, вероятно, также снизили когнитивную нагрузку. Остальные проблемы остаются, поэтому, пожалуйста, продолжайте читать это.

Docker также имеет параметр командной строки --squash для объединения всех слоев в один, но это экспериментальная функция, которую необходимо вручную включить на сервере. Хм.

Кроме того, если вы действительно хотите иметь пару слоев вместо одного, я думаю, что этот параметр командной строки будет бесполезен.

И тот же комментарий, что и раньше - это решает только проблему с конечным размером изображения, а не все их.

Dockerfile во время разработки по сравнению с финальной версией

Предположим, во время разработки Dockerfile вы допускаете ошибки (мы все делаем, верно?). В конечном итоге вам придется запускать docker build процесс много раз.
Docker хорошо кэширует каждый уровень и повторно использует их, если это возможно, во время последующих сборок. Чем больше у вас будет при восстановлении, тем больше шансов, что некоторые из них будут использованы повторно.

«Ооо, - скажете вы, - кэширование - это хорошо!» Что ж, да, но, как и в предыдущем разделе, это противоположное направление в нашем поиске меньшего изображения с как можно меньшим количеством слоев.

Короче говоря, похоже, что во время разработки вам понадобится много команд RUN, но для последнего варианта - как можно меньше. Хм.

Один из способов сделать это - разделить этот большой список команд при разработке на несколько не очень больших команд RUN и в конце кодирования снова объединить их в единый список. А потом, при изменении / исправлении чего-либо, повторение процесса.

Взяв вымышленный пример сверху, мы могли бы разбить его следующим образом:

RUN \
    apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install apt-transport-https ca-certificates
# split point 1
RUN    
    && apt-key adv --keyserver SERVER --recv-keys KEY_ID \
    && DEBIAN_FRONTEND=noninteractive apt-get install some_packages
    
# split point 2    
RUN \    
    && sed -e s/something/otherthing/ \
    && rm somefile \
    && mkdir someDir \
    && run_a_script with_some_parameters
# split point 3    
RUN \
    repeat_above a_few_times

Итак, мы делаем это, мы редактируем / исправляем / улучшаем, и в конце мы рекомбинируем эти фрагменты.

Скучный. Иди выпей кофе, это еще не все.

Почему эти точки разделения?

Точка разделения 1: маловероятно, что apt-get update будет запускаться каждый раз. И это трудоемкий процесс, поэтому вы хотите, чтобы он был кэширован, независимо от того, что будет дальше.

Точка разделения 2: маловероятно, что вы допустили (много) ошибок в части со списком необходимых шагов по установке пакетов, поэтому вы хотите, чтобы эта, возможно, трудоемкая часть была кэширована. Итак, вы отделили это от того, что следует ниже. А что дальше? Конфигурация. Вероятность того, что потребуются некоторые обновления, намного выше, чем у списка пакетов, поэтому такая точка разделения была мудрым решением.

Точка разделения 3: мы разделяем между ними все имеющиеся у нас высокоуровневые компоненты. Возможно, нам удалось настроить один из компонентов, и вы приступите к работе над следующими. Вы хотите использовать кэшированный слой с тем, что вы сделали с первым компонентом.

Совет 1: упорядочивайте разделы по уменьшенной вероятности их изменения. Например, конфигурации пакетов, скорее всего, изменятся, поэтому они обычно заканчиваются в конце. Или сценарии точки входа, если на то пошло.

Совет 2: особенно если у вас нет кэширующего прокси, когда вы добавляете новый репозиторий, вам также необходимо получать информацию из него (через apt-get update). Если у вас несколько репозиториев, сначала добавьте репозитории, а затем запустите один apt-get update. Конечно, это характерно для Debian / Ubuntu, но другие могут иметь подобное поведение.

Возможность повторного использования

Посмотрите на Ссылку B, имя файла Docker (Dockerfile.template ,, если вы не открывали ссылку). Это не последняя версия Dockerfile. На самом деле это простой файл шаблона, в котором несколько переменных заменены на sed.

Причина в том, что они хотят внести в него несколько изменений. Обычно вы меняете имя и версию базового образа, а также версию вашего приложения.

Хех, здорово, там есть многоразовые, но слишком тонкие. С другой стороны, мы уже получили подсказку: создание шаблона или создание Dockerfile!

Давайте посмотрим на содержимое приведенного выше примера вымышленного раздела Dockerfile и, возможно, даже на Ссылку B.

У нас есть apt-get update команда. Хороший. В какой-то момент я понял, что было бы лучше добавить -qq, чтобы было меньше подробностей. (Отмечая: отредактируйте все мои файлы Docker, чтобы это изменить.)

Установим пакет: apt-get install some_package. Хороший.

Позже вы узнаете, что независимо от того, что этот пакет предлагает вам также установить, лучше быть более точным в том, что вы хотите установить, а не устанавливать. Это делает изображение более тонким и даже лучше с точки зрения безопасности (меньшая поверхность атаки). (Заметка: отредактируйте все мои файлы Docker, чтобы использовать apt-get install — no-install-suggests — no-install-recommends.)

Ах, добавьте еще DEBIAN_FRONTEND=noninteractive и -yqq в эту смесь. Внутри Dockerfile у вас может быть несколько мест, куда вы устанавливаете пакеты. Вы должны заменить их все этой большой линией. Вы чувствуете запах другой проблемы? (Если у вас простуда, это копипаст.)

Мне был нужен сервер NGINX, один код демона Python и один веб-сервер Python. Думаю, я начал с https://hub.docker.com/r/tiangolo/uwsgi-nginx/.

Отметим также ссылку на Dockerfile из него:
Ссылка C: https://raw.githubusercontent.com/tiangolo/uwsgi-nginx-docker /master/python3.7/Dockerfile

В любом случае, я быстро отклонил этот источник, потому что начальный образ был уже большим, с установленным множеством ненужных пакетов. Но на самом деле это было, вероятно, из-за собственного начального слоя.

Итак, я сказал, я построю себе такой.

Я посмотрел на репозиторий NGINX как на отправную точку (например, Ссылка A) - кстати, полезные вещи, которым можно поучиться!

Интересно: я нашел еще один хороший вариант, который можно использовать при установке пакетов через apt: Dpkg::Options::= — force-unsafe-io. (Так что обратите внимание: отредактируйте все мои файлы Docker, чтобы изменить команду пакета установки. Хм, мне начать повторять свои задачи?)

Я вижу еще одну интересную часть установки NGINX - поиск хорошего сервера для импорта ключа GPG. Я знаю, что мне понадобится NGINX в других образах Docker, так что ... (Вы уже знаете, как это сделать, верно? Замечание: измените способ установки NGINX во всех моих файлах Docker.)

Еще… Обратите внимание на то, как выполнять очистку: каталоги кеша, очистку каталогов журналов, кеши пакетов, списки репозиториев, пакеты, которые устанавливаются автоматически и которые могут быть автоматически удалены, и так далее. С каждым файлом Dockerfile, который вы просматриваете, вы узнаете что-то новое о том, что можно очистить, и пополните свой набор вкусностей! (Примечание для себя: обновите мои файлы Dockerfiles, часть очистки, добавив вещи, которые я мог пропустить, по этим ссылкам, которые я вам дал.)

Что мне делать, когда все это записано? Идите и отредактируйте каждый из моих файлов Docker. И каждый раз, когда я обнаруживаю, что могу что-то улучшить, я делаю это снова.
Мой мозг кричит: а где возможность повторного использования? Где хорошие практики разработки?

Начни улучшать ситуацию

Цели:

  • Задача A - снизить когнитивную нагрузку. Имейте небольшие независимые разделы, которые вы можете назвать. Подумайте о функциях и о том, как вы можете понять программу в целом, если она построена из более мелких, хорошо названных функций.
  • Цель B - с той же базой кода разрешить Dockerfiles искать запуск производственной сборки или запуск сборки разработчика.
  • Цель C - Разрешить Dockerfiles выглядеть немного иначе в зависимости от некоторых аргументов (например, версий, начальных точек изображения и т. Д.).
  • Цель D. Разрешить создание файлов Docker из частей многократного использования.

Мысли текут…

  • Не собираюсь напрямую использовать Dockerfile, а сгенерировать его. Шаблонный механизм или что-то в этом роде. Ничего нового, это делают другие, но я не думаю, что это сделано в полную силу.
  • Поскольку большинство проблем возникает из-за команд RUN, которые являются командами оболочки, механизмом создания шаблонов будет сценарий оболочки (Bash). То есть мы будем генерировать код из сценариев оболочки. Это означает отсутствие дополнительных зависимостей. Вы должны знать некоторые сценарии оболочки, чтобы в любом случае иметь возможность писать свой Dockerfile, поэтому вы повторно используете это ноу-хау. И, скорее всего, у вас уже есть сценарий сборки оболочки (build.sh, update.sh, что угодно).
  • Таким образом, сценарий будет кодом Bash, который будет выводить фактическое содержимое файла Dockerfile. Аргументы / переменные могут использоваться для настройки вывода (как источник изображения FROM). Будучи сценарием оболочки, вы должны легко достичь цели C).
  • Упростите понимание: используйте более мелкие, хорошо названные функции Bash. Цель А проверена! Правильное имя - это субъективно, но все же более важный шаг, чем полное отсутствие имени.
  • Я начинаю чувствовать себя лучше. Мы приближаемся к более ориентированному на разработчиков способу решения этой задачи. Берем мороженое.
  • Нам нужно отслеживать, нужно ли создавать Dockerfile в режиме разработчика или в производственном режиме.
  • Чтобы реализовать предыдущую мысль, нам нужно различать команды RUN и другие. RUN будут объединены в производственном режиме. (Этот и предыдущий пункты предназначены для цели B.)
  • Возможность повторного использования: вы создаете свои общие функции в отдельном репозитории SCM, а затем используете его как-то в качестве зависимости SCM (например, как подмодуль Git).
  • Скрипты Bash будут использовать множество «здесь документов» для генерации контента.
    Не в восторге от этого, но все же приемлемо.
  • Весь сгенерированный контент (для команд RUN) должен начинаться с ; и заканчиваться \. Обязательно: перед кодом необходимо указать set -e. Я предпочитаю ;, а не&&. Некоторую конструкцию было бы сложнее реализовать && способом, а ; более естественен - ​​он был создан для разделения операторов. Мне не нравится, что меня все равно заставят чем-то ограничивать утверждения. Тем не менее, это приемлемая боль. Теперь.

Код

Фрагменты из общего (многоразового) кода:

main.sh:

apt.sh:

dockerfile-gen.sh - основной файл реального проекта:

использование

Создайте свой Dockerfile и запустите сборку:

Предполагая, что вы находитесь в режиме разработки:

  • Запустите его один раз. Вы получите файл с множеством записей RUN (по крайней мере, по одной для каждого run_* вызова функции).
  • Теперь измените что-нибудь в вашем скрипте, например, код, связанный с конфигурацией. Запустив сценарий сборки, он должен показать, что Docker повторно использует некоторые из кэшированных слоев.
  • Когда все будет в порядке, запустите сценарий сборки с другими параметрами, который в дальнейшем удалит параметр — dev.
  • Код должен снова создать Dockerfile, но на этот раз с меньшим количеством команд RUN.
  • Сделайте еще один тест этого изображения.

Выполнено!

Помимо этих вызовов функций, которые субъективно имеют приятные названия и позволяют быстро понять, что мы делаем, может возникнуть один вопрос: что с условными вызовами?

Звонок copy_files

Скорее всего, вам понадобятся некоторые файлы, которые нужно вставить в образ (файлы конфигурации, код, сценарий запуска и т. Д.). Во время разработки вероятность их изменения очень высока, поэтому, как мы уже обсуждали, мы хотели бы, чтобы это произошло как можно позже в Dockerfile. Но после этого нам также потребуется запустить некоторый код, чтобы выполнить с ними действительно необходимые действия (chmod +x, mv и т. Д.).

Это причина того, что после нее вызывается функция run_fix_files.
К сожалению, это приведет к созданию нового слоя, но мы ничего не можем с этим поделать. Сведите эту функцию к минимуму. А в производственном режиме нам ничего не нужно между функцией очистки и строкой, которая начала обновление / установку программного обеспечения. Вы хотели бы, чтобы там были только вызовы функций в стиле run_*.

Звонок run_cleanup

Возможно, вам не захочется, чтобы он находился в режиме разработки, потому что это может занять время, и вас не очень интересует его запуск или размер окончательного изображения.
С другой стороны, это делает его еще более необходимым. что вы также тестируете свой образ как созданный рабочим Dockerfile, потому что вы хотите также протестировать побочные эффекты очистки.

Подумайте об этом: что, если ваше приложение зависит от какой-то библиотеки, но функция и все, что она делает, думали, что она была установлена ​​автоматически и в действительности не нужна?

Для цели D, чтобы извлечь выгоду из общего кода, вы должны поместить его в выделенный репозиторий, скажем, в Git, или, например, в этот репозиторий.
В инструкциях по установке вашего приложения вы должны указать, как пользователи должны git clone ваш репозиторий, чтобы они также получали зависимости.

Если вы создаете образ для какого-то продукта, который имеет небольшую вероятность быть интегрированным позже в другой контейнер, вам следует поместить свои функции (из этого dockerfile-gen.sh скрипта) в отдельные файлы, чтобы их можно было повторно использовать. Например, в этом репозитории я поместил некоторые функции в каталог dockerfile /.

Что касается повторного использования, я хотел бы сказать больше, но просто откройте эти три ссылки выше (A, B и C) одновременно и посмотрите сами.

Важно: вы не можете принудительно использовать этот способ написания файлов Docker для других, но в вашей организации, если вы создаете много образов, в которых есть проблемы, обсуждаемые до сих пор, вам может быть полезно изменить способ их написания.

Дальнейшие улучшения и идеи

  • Это может быть полезно тем, у кого много выделенных узлов сборки, широко использующих кеши: у вас может быть код, который автоматически сделает недействительным кешированный слой через некоторое время. Подумайте о том, что вы не хотите использовать кэшированный слой FROM ubuntu:latest RUN apt-get update по прошествии нескольких часов.
  • Имейте хорошую apt библиотеку обработки или любой другой пакет управления, который вы используете. Возможно, определите способ объявления пакетов, необходимых для выполнения, а не для сборки.
  • Самое важное: найдите способ использовать не «здесь документы» Bash, а настоящий шелл-код. Вы получите еще больше преимуществ. Держись поближе, у меня есть на этот счет кое-какие идеи.

Ух ты! Это было давно!

Даже если вы не будете использовать или применять то, что я описал выше, я все же надеюсь, что вам понравилось и вы узнали что-то новое о написании образов Docker.
Не стесняйтесь обращаться ко мне с вопросами, комментариями или предложениями.