Собираем docker образы с помощью buildx

5
(1)

У докера есть отличная вещь, называется buildx, она расширяет стандартные возможности «docker build», позволяет описывать сборку в виде конфигурационных файлов с расширением .hcl и в целом более гибко управлять сборкой.

Давайте рассмотрим сборку фронтового приложения на vite. Приложение будет без конкретики с измененными «sensitive» моментами.

Начнем с главного, наш Dockerfile

Dockerfile
FROM node as node_modules
COPY package.json yarn.lock /app
RUN --mount=type=secret,id=npm_token,target=/run/secrets/npm_token,uid=10064,gid=10064 \
    --mount=type=cache,sharing=locked,target=/home/node/.cache/yarn,id=node_cache,uid=10064,gid=10064 \
    --mount=type=tmpfs,target=/tmp/ \
    yarn install --non-interactive --no-progress

FROM node_modules as build
ARG BACKEND_HOST
COPY --chown=node:node ./ /app
RUN --mount=type=secret,id=npm_token,target=/run/secrets/npm_token,uid=10064,gid=10064 \
    --mount=type=cache,sharing=locked,target=/home/node/.cache/yarn,id=node_cache,uid=10064,gid=10064 \
    --mount=type=tmpfs,target=/tmp/ \
    yarn build && \
    rm -rf /app/.dockerignore \
           /app/package.json 

FROM nginx as nginx
WORKDIR /var/www/app/public
COPY --from=build /app/dist ./

Как мы видим, перед нами мультистейдж, где сначала собираются зависимости (node_modules), и тут же мы используем функционал docker для безопасного «монтирования» сикрета с токеном для нашего внутреннего registry npm пакетов в сборку. Тут есть несколько способ его использовать.

Мы можем добавить в нашу RUN инструкцию такую команду

Bash
echo "//registry.yarnpkg.com/:_authToken="$(cat /run/secrets/npm_token)" >> ~/.npmrc

Либо мы можем подготовить базовый образ так, чтобы внутри него уже был наш кастомный .npmrc файл с примерно такой записью:

Bash
//registry.yarnpkg.com/:_authToken="${NPM_TOKEN}"

И далее во время сборки сделать:

Bash
export NPM_TOKEN="$(cat /run/secrets/npm_token)"

Второе, что мы тут используем это опцию mount=type=cache, об этом я уже упоминал в рамках статьи про buildkit cache тут. Напомню, что это создаст некий тайник, которого не будет после сборки. Т.е. он будет существовать только в рамках сборки и только там, где мы его подключаем.

Из интересного тут мы используем опцию sharing=locked она может быть полезна в контексте сборки с помощью yarn, так как возможна ситуация, при которой будет две одновременные сборки на одном раннере, тогда возникнет ошибка lock файлов. Эта опция поможет ее избежать, по сути запретив параллельность и ставя в «очередь» другую сборку.

Вторым стейджом у нас идет build.

Тут все тоже самое, только выполняется команда для билда.

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

Давайте опишем общий buildx файл buildx_general.hcl

HCL
variable "CI_REGISTRY_IMAGE"        { default = "registry" }
variable "CI_COMMIT_REF_SLUG"       { default = "dev" }
variable "CI_COMMIT_SHORT_SHA"      { default = "00000000" }
variable "CI_PIPELINE_IID"          { default = "00000" }
variable "BACKEND_HOST"             { default = "" }

variable "NPM_TOKEN"                { default = "token" }

IMAGE_TAG = "${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_IID}"
IMAGE_TAG_LATEST = "${CI_COMMIT_REF_SLUG}-latest"

group "node_modules" {
    targets = ["node_modules"]
}

group "app" {
    targets = ["app_dev", "app_prod"]
}

В самом начале мы описываем переменные, которые будут использованы, часть из них совпадает с реальными переменными gitlab. Если переменные не объявить внутри buildx и попробовать собрать с их использованием, получим ошибку не определенной переменной.

Основное это group. Тут мы формируем группы по которым в дальнейшем будем запускать сборку и указываем таргеты этим группам. Этот target не имеет отношения к target из Dockerfile, дальше вы это увидите.

Создадим файл buildx_app.hcl

HCL
target "default" {
    context="."
    dockerfile="Dockerfile"
    pull = true
    secret = [
        "id=npm_token,src=${NPM_TOKEN}"
    ]
    args = {
        BUILDKIT_INLINE_CACHE = "1"
    }
    output = ["type=registry"]
}

target "node_modules" {
    inherits = ["default"]
    target = "node_modules"
    tags = [
        "${CI_REGISTRY_IMAGE}/node_modules:${IMAGE_TAG}",
        "${CI_REGISTRY_IMAGE}/node_modules:${IMAGE_TAG_LATEST}"
    ]
    cache-from = [
        "${CI_REGISTRY_IMAGE}/node_modules:${IMAGE_TAG_LATEST}",
        "${CI_REGISTRY_IMAGE}/node_modules:${IMAGE_TAG}"
        "node",
        "nginx"
    ]
}

target "app_dev" {
    inherits = ["default"]
    target = "nginx"
    tags = [
        "${CI_REGISTRY_IMAGE}/nginx_dev:${IMAGE_TAG}",
        "${CI_REGISTRY_IMAGE}/nginx_dev:${IMAGE_TAG_LATEST}"
    ]
    args = {
        BACKEND_HOST = "dev.example.com"
    }
    cache-from = [
        "${CI_REGISTRY_IMAGE}/nginx_dev:${IMAGE_TAG}",
        "${CI_REGISTRY_IMAGE}/nginx_dev:${IMAGE_TAG_LATEST}",
        "nginx"
    ]
}

target "app_prod" {
    inherits = ["default"]
    target = "nginx"
    tags = [
        "${CI_REGISTRY_IMAGE}/nginx_prod:${IMAGE_TAG}",
        "${CI_REGISTRY_IMAGE}/nginx_prod:${IMAGE_TAG_LATEST}"
    ]
    args = {
        BACKEND_HOST = "prod.example.com"
    }
    cache-from = [
        "${CI_REGISTRY_IMAGE}/nginx_prod:${IMAGE_TAG}",
        "${CI_REGISTRY_IMAGE}/nginx_prod:${IMAGE_TAG_LATEST}",
        "nginx"
    ]
}

Таргет default — тут описываем разные параметры, например путь до Dockefile, указываем какой секрет использовать. Помните мы ранее описали вот такую строку «variable «NPM_TOKEN» { default = «token» }» — по сути в переменной NPM_TOKEN у нас лежит путь до файла, внутри которого токен для registry. Создать такой файл мы можем например там, где в gitlab вызываем наш build.

В args прописываем «общие» аргументы сборки, которые будут справедливы для любых таргетов внутри. Их можно также переопределить или дополнить на уровне отдельных таргетов, buildx сделает именно мердж аргументов.

Помните в buildx_general мы описывали таргеты в группах, вот те самые таргеты мы теперь описывает тут.

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

Также с помощью аргументов мы разделили нашу сборку на два образа, dev и prod. Они отличаются урлом, который будет при билде статики «прошит» внутрь нее.

Указывать все таргеты из нашего мультистейджа нет смысла, достаточно указать «финальные» или те, что мы хотим видеть в качестве образов внутри регистри. Если Dockerfile написан правильно и имеет внутри себя верные зависимости между стейджами, то buildx сам поймет, что при начале сборки например «app_dev» таргета nginx в нашем Dockerfile, нужно собрать и «build» стейдж, так как он зависимый.

Как запустить сборку всего этого?

Bash
docker buildx bake --file buildx_general.hcl --file buildx_app.hcl <имя группы>

Соответственно где-то тут можно как раз записать наш токен в файл, например так

Bash
echo $NPM_TOKEN > token

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

Также buildx поддерживает мульти платформенную сборку. Предположим в компании часть людей работают на macOS а ARM архитектурой, другая часть сидит на AMD. Мы можем собрать один образ сразу под несколько платформ.

Для начала надо создать builder, который будет использоваться для сборки

Bash
docker context create builder
docker buildx create --use builder --name builder

Далее в наших таргетах мы можем просто указать какие под какие платформы мы хотим собрать образ:

HCL
target "default" {
  platforms = ["linux/amd64", "linux/arm64"]
}

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

Насколько статья полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 5 / 5. Количество оценок: 1

Оценок пока нет. Поставьте оценку первым.

Оставить комментарий