Функция многоэтапной сборки в файлах 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
.
FROM ubuntu AS base
RUN apt-get update && apt-get install git
FROM base AS src1
RUN git clone …
FROM base as src2
RUN git clone …
В этом примере второй и третий этап в BuildKit строятся одновременно.
Непосредственное использование образов
Вместо того, чтобы использовать имена этапов сборки в командах FROM
, которые ранее поддерживали только ссылки на образы, можно напрямую использовать образы — с помощью метки --from
. Получается копировать файлы непосредственно из этих образов. Например, linuxkit/ca-certificatesimage
в следующем коде напрямую копирует корни TLS CA в текущий этап.
FROM alpine
COPY --from=linuxkit/ca-certificates / /
Псевдоним общего образа
Этап сборки необязательно включает какие-либо команды; он может состоять из единственной строки FROM
. Если используется образ в нескольких местах, это упростит чтение и сделает так, что, если надо обновить общий образ, потребуется изменить только одну строку.
FROM alpine:3.6 AS alpine
FROM alpine
RUN …
FROM alpine
RUN …
В данном примере каждое место, использующее образ alpine, фактически фиксируется на alpine:3.6
, а не alpine:latest
. Когда придет время обновиться до alpine:3.7
, изменить потребуется единственную строку, и можно не сомневаться: теперь во всех элементах сборки используется обновленная версия.
Это тем более важно, когда в псевдониме используется аргумент сборки. Следующий пример аналогичен предыдущему, но позволяет пользователю переопределить все экземпляры сборки, в которых задействован образ alpine, путем настройки опции --build-arg ALPINE_VERSION=value
. Помните: любые аргументы, используемые в командах FROM
, нужно определить до первого этапа сборки.
ARG ALPINE_VERSION=3.6
FROM alpine:${ALPINE_VERSION} AS alpine
FROM alpine
RUN …
Использование аргументов сборки в "—?from"
Значение, указанное в метке --from
команды COPY
, не должно содержать аргументы сборки. Например, следующий пример недействителен.
// THIS EXAMPLE IS INTENTIONALLY INVALID
FROM alpine AS build-stage0
RUN …
FROM alpine
ARG src=stage0
COPY --from=build-${src} . .
Это связано с тем, что зависимости между этапами нужно определить еще до начала сборки. Тогда постоянная оценка всех команд не потребуется. Например, переменная среды, определенная в образе alpine
, может влиять на оценку значения --from
. Причина, по которой мы можем оценивать аргументы команды FROM
, — в том, что эти аргументы определяются глобально до начала любого этапа. К счастью, как мы выяснили ранее, достаточно определить этап псевдонима с помощью одной команды FROM
и ссылаться на него.
ARG src=stage0
FROM alpine AS build-stage0
RUN …
FROM build-${src} AS copy-src
FROM alpine
COPY --from=copy-src . .
Теперь, если переопределить аргумент сборки src
, исходный этап для окончательного элемента COPY
переключится. Обратите внимание: если какие-то этапы теперь не используются, то эффективно пропускать их смогут только компоновщики на основе BuildKit.
Условия, использующие аргументы сборки
Нас просили добавить в Dockerfile поддержку условий стиля IF/ELSE
. Мы пока еще не знаем, будем ли добавлять нечто подобное, но в будущем попробуем — с помощью поддержки клиентской части в BuildKit. Между тем, чтобы добиться аналогичного поведения, можно использовать текущие многоэтапные концепции (при некотором планировании).
// THIS EXAMPLE IS INTENTIONALLY INVALID
FROM alpine
RUN …
ARG BUILD_VERSION=1
IF $BUILD_VERSION==1
RUN touch version1
ELSE IF $BUILD_VERSION==2
RUN touch version2
DONE
RUN …
В предыдущем примере показан псевдокод для записи условий с помощью IF/ELSE
. Чтобы добиться аналогичного поведения с помощью текущих многоэтапных сборок, может потребоваться определить различные условия ветвей в качестве отдельных этапов и использовать аргумент для выбора правильного пути зависимости.
ARG BUILD_VERSION=1
FROM alpine AS base
RUN …
FROM base AS branch-version-1
RUN touch version1
FROM base AS branch-version-2
RUN touch version2
FROM branch-version-${BUILD_VERSION} AS after-condition
FROM after-condition
RUN …
Последний этап в Dockerfile основан на этапе after-condition
, который представляет собой псевдоним образа (распознается на основе аргумента сборки BUILD_VERSION
). В зависимости от значения BUILD_VERSION
выбирается тот или иной этап средней секции.
Обратите внимание: пропускать неиспользуемые ветви могут только компоновщики на базе BuildKit. В предыдущих вариантах компоновщиков строились бы все этапы, но перед созданием конечного образа их результаты были бы отброшены.
Помощник по разработке/тестированию для минимального производственного этапа
В завершение давайте рассмотрим пример комбинирования предыдущих шаблонов для демонстрации того, как создать Dockerfile, создающий минимальный производственный образ и способный затем использовать его содержимое для тестирования и создания образа разработки. Начнем с базового примера Dockerfile:
FROM golang:alpine AS stage0
…
FROM golang:alpine AS stage1
…
FROM scratch
COPY --from=stage0 /binary0 /bin
COPY --from=stage1 /binary1 /bin
Когда создается минимальный производственный образ, это — довольно распространенный вариант. Но что, если надо также получить альтернативный образ разработчика или запустить тесты с этими двоичными файлами на конечном этапе? Первое, что приходит на ум, — это просто скопировать аналогичные двоичные файлы на этапах тестирования и разработки. Проблема же такова: нет гарантий того, что Вы будете тестировать все производственные двоичные файлы в той же комбинации. На конечном этапе что-то может измениться, а Вы забудете внести аналогичные изменения на других этапах или допустите ошибку в пути копирования двоичных файлов. В конце концов, мы не отдельный двоичный файл тестируем, а конечный образ.
Альтернативный вариант — определить этап разработки и тестирования после этапа производства и копировать все содержимое этапа производства. Затем использовать для этапа производства одну команду FROM
, чтобы снова сделать этап производства по умолчанию последним шагом.
FROM golang:alpine AS stage0
…
FROM scratch AS release
COPY --from=stage0 /binary0 /bin
COPY --from=stage1 /binary1 /bin
FROM golang:alpine AS dev-env
COPY --from=release / /
ENTRYPOINT ["ash"]
FROM golang:alpine AS test
COPY --from=release / /
RUN go test …
FROM release
По умолчанию этот Dockerfile продолжит построение минимального образа по умолчанию, в то время как, например, сборка с опцией --target=dev-env
будет создавать образ с оболочкой, содержащей все двоичные файлы окончательной версии.
Надеюсь, это было полезно и подсказало, как создать более эффективные многоэтапные файлы Dockerfile. Если участвуете в DockerCon2018 и хотите узнать больше о многоэтапных сборках, Dockerfiles, BuildKit или о каких-то смежных темах, подписывайтесь на компоновщик Hallway track или отслеживайте внутренние совещания платформы Docker на треках Contribute and Collaborate или Black Belt.