image


Функция многоэтапной сборки в файлах 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.

Комментарии (0)