[Перевод] Теперь Dockerfiles поддерживает Multiple Build Contexts

8683589708514923d41ec20506a5a984.jpg

Новые релизы Dockerfile 1.4 и Buildx v0.8+ дают возможность определения нескольких контекстов сборки. Теперь в качестве сборки вы можете использовать файлы из разных локальных директорий. Давайте посмотрим, какая от этого польза и как это использовать в разработке процессах сборки.

Команда docker build принимает один позиционный аргумент, который является путём или URL к контексту сборки. Чаще всего docker build . использует текущую рабочую директорию в качестве контекста сборки.

Внутри Dockerfile вы можете использовать команды COPY и ADD, чтобы скопировать файлы из контекста сборки и сделать их доступными для последующих шагов сборки. В BuildKit мы также добавили mount директории во время сборки с помощью RUN --mount, который позволяет получать доступ к файлам напрямую без копирования, для увеличения производительности.

Содержание:

Покорение сложных сборок

Когда сборки становятся более сложными, возможность доступа к файлам только из одного места начинает сильно ограничивать. Поэтому мы добавили многоэтапные (multi-stage) сборки, так что вы можете копировать файлы из разных частей Dockerfile, добавляя флаг --from и указывая путь к названию другого этапа Dockerfile или удалённого образа.

Новые именованные контексты сборки являются продолжением этого паттерна. Теперь вы можете определять дополнительные контексты сборки при исполнении команды сборки. Дайте им название и сможете обращаться к ним внутри Dockerfile так же, как со стейджами сборки.

Дополнительные контексты можно определить с помощью нового флага --build-context [name]=[value]. Ключевой компонент определяет название контекста сборки. Варианты значений такие:

  • Локальная директория — --build-context project2=../path/to/project2/src

  • Репозиторий Git — --build-context qemu-src=https://github.com/qemu/qemu.git

  • HTTP URL до tarball — --build-context src=https://example.org/releases/src.tar

  • Docker-образ — определяется с помощью docker-image:// префикса, т.е. --build-context alpine=docker-image://alpine:3.15

На стороне Dockerfile вы можете сослаться на тот или иной контекст сборки во всех командах, которые принимают параметр «from». Так это может выглядеть:

# syntax=docker/dockerfile:1.4
FROM [name]
COPY --from=[name] ...
RUN --mount=from=[name] …

Значение [name] сопоставляется со следующим порядком приоритета:

  • Именованный контекст сборки, определённый с помощью --build-context [name]=..;

  • Стейдж сборки, определённый с помощью AS [name] внутри Dockerfile;

  • Удалённый Docker-образ [name] в registry.

Если --from не будет передан, то файлы будут загружены из основного контекста сборки.

Пример №1: пин образа

Начнём с примера, как использовать контексты сборки, чтобы запинить образ конкретной версии, используемый в Dockerfile.

Это может пригодиться во многих случаях. Например, с новой функцией BuildInfo вы можете использовать все источники сборки и запустить сборку с такими же зависимостями, как у предыдущей, даже если теги образа обновились.

docker buildx imagetools inspect --format '{{json .BuildInfo}}' moby/buildkit
"sources": [
      {
        "type": "docker-image",
        "ref": "docker.io/library/alpine:3.15",
        "pin": "sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300"
      },
docker buildx build --build-context alpine:3.15=docker-image://alpine:3.15@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300 .

Если Dockerfile использует alpine:3.15, то даже с обновлённой версией реестра новая сборка всё равно будет использовать тот же самый образ, что и предыдущая.

А вот другой пример. Вы можете просто попробовать использовать другой образ или другую версию для отладки или создания вашего образа. Общей закономерностью может быть то, что вы ещё не зарелизили новый образ, и пока что он только в test или stage registry. Давайте представим, что создали своё приложение и запушили его в staging-репозиторий, но теперь хотите использовать его в других сборках, которые обычно используют release-образ.

docker buildx build --build-context myorg/myapp=docker-image://staging.myorg.com/registry/myapp .

Предыдущие примеры можно также рассматривать как способ создания алиаса для образа.

Пример №2: несколько проектов

Наверное, самый востребованный юзкейс применения именованных контекстов — возможность использования нескольких локальных директорий.

Когда в проекте несколько компонентов, которые нужно собрать вместе, то может быть сложно загрузить их с помощью одного контекста сборки, где всё должно содержаться в одной директории. У вас сразу несколько проблем: к каждому компоненту будет получен доступ по его полному пути, и у вас может быть лишь один .dockerignore файл —, а вы, возможно, хотите, чтобы у каждого компонента был свой личный Dockerfile.

Если у вашего проекта такая структура…

project
├── app1
│   ├── .dockerignore
│   ├── src
├── app2
│   ├── .dockerignore
│   ├── src
├── Dockerfile

…с таким Dockerfile…

#syntax=docker/dockerfile:1.4
FROM … AS build1
COPY –from=app1 . /src
 
FROM … AS build2
COPY –from=app2 . /src
 
FROM …
COPY –from=build1 /out/app1 /bin/
COPY –from=build2 /out/app2 /bin/

…то вы можете вызвать свою сборку с помощью docker buildx build –build-context app1=app1/src –build-context app2=app2/src .. Обе исходные директории предоставлены для Dockerfile по отдельности, и к ним можно получить доступ, используя соответствующие названия.

Еще это позволяет вам получить доступ к файлам, которые находятся вне исходного кода вашего основного проекта. Обычно по соображениям безопасности внутри Dockerfile вам нельзя иметь доступ к файлам вне вашего контекста сборки, используя родительский селектор ../. Теперь, пока все контексты сборки передаются напрямую от клиента, вы можете использовать --build-context othersource=../../path/to/other/project, чтобы обойти это ограничение.

Пример №3: замените удалённую зависимость локальной

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

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

Что-то вроде этого:

FROM golang AS helper
RUN apk add git
WORKDIR /src
ARG HELPERAPP_VERSION=1.0
RUN git clone https://github.com/someorg/helperapp.git && cd helperapp && git checkout $HELPERAPP_VERSION
WORKDIR /src/helperapp
RUN go build -o /out/helperapp .
 
FROM alpine
COPY –link –from=helper /out/helperapp /bin
COPY –link –from=build /out/myapp /bin

И это неплохо работает. Когда вы создаёте сборку, helperapp собирается напрямую из его исходного репозитория и копируется рядом с бинарным файлом вашего приложения. Когда вам понадобилось использовать другую версию, вы обращаетесь к аргументу сборки HELPERAPP_VERSION и указываете другое значение.

Но что, если вы разрабатываете приложение и нашли баг? Вы не уверены, является ли источником бага исходный код или приложение-помощник. Можно внести несколько локальных изменений в код helperapp, чтобы проанализировать ситуацию. Но с текущим кодом сначала придётся пушить изменения в Github, чтобы они могли быть перенесены в Dockerfile. Повторять этот процесс для каждого изменения кода очень больно и неэффективно.

Вместо этого заменим предыдущий код на:

FROM alpine AS helper-clone
RUN apk add git
WORKDIR /src
ARG HELPERAPP_VERSION=1.0
RUN git clone https://github.com/someorg/helperapp.git && cd helperapp && git checkout $HELPERAPP_VERSION
 
FROM scratch AS helper-src
COPY –from=helper-clone /src/helperapp /
 
FROM golang:alpine AS helper
WORKDIR helperapp
RUN –mount=target=.,from=helper-src go build -o /out/helperapp .
 
FROM alpine
COPY –link –from=helper /out/helperapp /bin
COPY –link –from=build /out/myapp /bin

По умолчанию этот Dockerfile ведёт себя так же, как предыдущий, клонируя репозиторий с GitHub. Но теперь мы добавили отдельный этап helper-src, который содержит исходный код для helperapp. И мы можем использовать новые фичи контекстов, чтобы при необходимости сделать замену на нашу локальную исходную директорию.

docker buildx build --build-context helper-src=../path/to/my/local/helper/checkout .

006de908b5b2ff7abd968516cd8bb6f5.png

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

Именованные контексты в buildx bake

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

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

Мы также добавили поддержку именованных контекстов в bake. Это полезно, потому что, если Dockerfile зависит от нескольких контекстов сборки, вы можете забыть, что нужно передавать эти значения с помощью флага --build-context каждый раз, когда вызываете команду сборки.

С помощью bake вы можете задать определение вашей цели. Например:

hcl
target "binary” {
  contexts = {
    app1 = "app1/src”
    app2 = "app2/src”
  }
}

Теперь не нужно каждый раз вспоминать об использовании флага --build-context с правильными путями. Просто вызовите docker buildx bake binary, и ваша сборка запустится с правильной конфигурацией. Конечно, использовать переменные Bake и остальное в этих полях можно и для более сложных случаев.

Вы можете также использовать этот паттерн для создания специальных bake-целей для отладки или тестирования образов в этапных репозиториях.

hcl
target "myapp” {
 …
}
 
target "myapp-stage” {
  inherits = ["myapp”]
  contexts = {
    helperapp = "docker-image://staging.myorg.com/registry/myapp”
  }
}

С Bake-файлом, как здесь, можно вызвать docker buildx bake myapp-stage, чтобы собрать приложение с такой же конфигурацией, определённой для вашей цели myapp. За исключением случаев, когда ваша сборка использует образ helperapp, которое она загрузит из staging-репозитория, вместо релиза того, что записано в Dockerfile.

Создавайте пайплайны сборки путём ссылок на bake-цели

В дополнение к образу, Git, URL и локальным репозиториям, Bake-файлы также поддерживают ещё одно определение, которое можно использовать как именованный контекст. 

Вы можете установить в качестве источника для именованного контекста другую цель сборки внутри Bake-файла. Так вы можете объединять сборки из нескольких Dockerfiles, которые зависят друг от друга, и собрать их с помощью единственного вызова команды.

Давайте представим, что мы имеем два Dockerfile:

# base.Dockerfile
FROM alpine
…
# Dockerfile
FROM baseapp
...

Обычно сначала собирают base.Dockerfile, затем пушат это в реестр или оставляют в хранилище образов Docker. Затем собирают второй Dockerfile, который загружает образ по имени.

Проблема в том, что если вы используете хранилище образов Docker, то в данный момент он не поддерживает мультиплатформенные локальные образы. Использовать внешний registry не всегда удобно. И в обоих случаях некоторые внешние изменения могут обновить base-образ между двух сборок и сделать так, что вторая сборка будет использовать не тот образ. Вам нужно запустить команды сборки дважды и синхронизировать их вручную.

Вместо этого вы можете определить Bake-файл с помощью контекста сборки с префиксом target:

target "base” {
  dockerfile = "base.Dockerfile”
  platforms = ["linux/amd64”, "linux/arm64”]
}
 
target "myapp” {
  contexts = {
    baseapp = "target:base”
  }
  platforms = ["linux/amd64”, "linux/arm64”]
}

Теперь вы можете собрать ваше приложение, просто запустив docker buildx bake myapp, чтобы собрать оба Dockerfiles и связать их так, как нужно. Если вы хотите собрать и базовый образ и ваше приложение вместе, вы можете использовать docker buildx bake myapp base. Обе этих цели определены как мультиплатформенные, и Buildx позаботится о линковке соответствующих одноплатформенных образов друг к другу.

3005fdc25c3d98e6b2b1061c4a2326d9.png

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

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

Посмотрите новую функцию контекста сборки в DockerBuildx v0.8 release, входящем в состав последнего Docker Desktop.

© Habrahabr.ru