Функция многоэтапной сборки в файлах Dockerfile позволяет создавать небольшие образы контейнеров с более высоким уровнем кэширования и меньшим объемом защиты. В этой статье я покажу несколько расширенных шаблонов — нечто большее, чем копирование файлов между этапами сборки и выполнения. Они позволяют добиться максимальной эффективности функции. Однако, если Вы — новичок в сфере многоэтапной сборки, то сперва, наверное, не лишним будет прочесть руководство по использованию. Поддержка многоэтапной сборки была добавлена в Docker в версии v17.05. Все шаблоны работают с любой последующей версией, но некоторые — значительно эффективнее, благодаря использующим серверную часть BuildKit компоновщикам. Скажем, BuildKit эффективно пропускает неиспользуемые этапы и, по возможности, создает этапы одновременно (эти примеры я выделил отдельно). В настоящее время BuildKit добавляется в Moby в качестве экспериментальной серверной части сборки и должен быть доступен в версии Docker CE v18.06. Также его можно применять автономно или в рамках проекта img. Многоэтапная сборка добавляет несколько новых концепций синтаксиса. Прежде всего, можно присвоить этапу, начинающемуся с команды FROM, имя AS stagename и использоваться опцию --from=stagename в команде COPY для копирования файлов из этого этапа. На самом деле команда FROM и метка --from имеют гораздо больше общего, не зря же у них одинаковое название. Обе принимают одинаковый аргумент, распознают его и либо запускают новый этап с этой точки, либо используют ее в качестве источника для копирования файла. То есть чтобы использовать предыдущий этап в исходного качестве образа для этапа текущего, брать можно не только --from=stagename, но и имя этапа FROM stagename. Полезно, если пользуетесь одинаковыми общими частями в нескольких командах в Dockerfile: уменьшает общий код и упрощает его обслуживание, сохраняя дочерние этапы раздельно. Таким образом, перестройка одного этапа не влияет на кэш сборки для других. Соответственно, каждый этап можно собрать индивидуально с помощью метки --target при вызове docker build. В этом примере второй и третий этап в BuildKit строятся одновременно. Вместо того, чтобы использовать имена этапов сборки в командах FROM, которые ранее поддерживали только ссылки на образы, можно напрямую использовать образы — с помощью метки --from. Получается копировать файлы непосредственно из этих образов. Например, linuxkit/ca-certificatesimage в следующем коде напрямую копирует корни TLS CA в текущий этап. Этап сборки необязательно включает какие-либо команды; он может состоять из единственной строки FROM. Если используется образ в нескольких местах, это упростит чтение и сделает так, что, если надо обновить общий образ, потребуется изменить только одну строку. В данном примере каждое место, использующее образ alpine, фактически фиксируется на alpine:3.6, а не alpine:latest. Когда придет время обновиться до alpine:3.7, изменить потребуется единственную строку, и можно не сомневаться: теперь во всех элементах сборки используется обновленная версия. Это тем более важно, когда в псевдониме используется аргумент сборки. Следующий пример аналогичен предыдущему, но позволяет пользователю переопределить все экземпляры сборки, в которых задействован образ alpine, путем настройки опции --build-arg ALPINE_VERSION=value. Помните: любые аргументы, используемые в командах FROM, нужно определить до первого этапа сборки. Значение, указанное в метке --from команды COPY, не должно содержать аргументы сборки. Например, следующий пример недействителен. Это связано с тем, что зависимости между этапами нужно определить еще до начала сборки. Тогда постоянная оценка всех команд не потребуется. Например, переменная среды, определенная в образе alpine, может влиять на оценку значения --from. Причина, по которой мы можем оценивать аргументы команды FROM, — в том, что эти аргументы определяются глобально до начала любого этапа. К счастью, как мы выяснили ранее, достаточно определить этап псевдонима с помощью одной команды FROM и ссылаться на него. Теперь, если переопределить аргумент сборки src, исходный этап для окончательного элемента COPY переключится. Обратите внимание: если какие-то этапы теперь не используются, то эффективно пропускать их смогут только компоновщики на основе BuildKit. Нас просили добавить в Dockerfile поддержку условий стиля IF/ELSE. Мы пока еще не знаем, будем ли добавлять нечто подобное, но в будущем попробуем — с помощью поддержки клиентской части в BuildKit. Между тем, чтобы добиться аналогичного поведения, можно использовать текущие многоэтапные концепции (при некотором планировании). В предыдущем примере показан псевдокод для записи условий с помощью IF/ELSE. Чтобы добиться аналогичного поведения с помощью текущих многоэтапных сборок, может потребоваться определить различные условия ветвей в качестве отдельных этапов и использовать аргумент для выбора правильного пути зависимости. Последний этап в Dockerfile основан на этапе after-condition, который представляет собой псевдоним образа (распознается на основе аргумента сборки BUILD_VERSION). В зависимости от значения BUILD_VERSION выбирается тот или иной этап средней секции. Обратите внимание: пропускать неиспользуемые ветви могут только компоновщики на базе BuildKit. В предыдущих вариантах компоновщиков строились бы все этапы, но перед созданием конечного образа их результаты были бы отброшены. В завершение давайте рассмотрим пример комбинирования предыдущих шаблонов для демонстрации того, как создать Dockerfile, создающий минимальный производственный образ и способный затем использовать его содержимое для тестирования и создания образа разработки. Начнем с базового примера Dockerfile: Когда создается минимальный производственный образ, это — довольно распространенный вариант. Но что, если надо также получить альтернативный образ разработчика или запустить тесты с этими двоичными файлами на конечном этапе? Первое, что приходит на ум, — это просто скопировать аналогичные двоичные файлы на этапах тестирования и разработки. Проблема же такова: нет гарантий того, что Вы будете тестировать все производственные двоичные файлы в той же комбинации. На конечном этапе что-то может измениться, а Вы забудете внести аналогичные изменения на других этапах или допустите ошибку в пути копирования двоичных файлов. В конце концов, мы не отдельный двоичный файл тестируем, а конечный образ. Альтернативный вариант — определить этап разработки и тестирования после этапа производства и копировать все содержимое этапа производства. Затем использовать для этапа производства одну команду FROM, чтобы снова сделать этап производства по умолчанию последним шагом. По умолчанию этот Dockerfile продолжит построение минимального образа по умолчанию, в то время как, например, сборка с опцией --target=dev-env будет создавать образ с оболочкой, содержащей все двоичные файлы окончательной версии. Надеюсь, это было полезно и подсказало, как создать более эффективные многоэтапные файлы Dockerfile. Если участвуете в DockerCon2018 и хотите узнать больше о многоэтапных сборках, Dockerfiles, BuildKit или о каких-то смежных темах, подписывайтесь на компоновщик Hallway track или отслеживайте внутренние совещания платформы Docker на треках Contribute and Collaborate или Black Belt.