Аутентификация в container registry

Перед работой с образами необходимо аутентифицироваться в container registry. Сделать это можно командой werf cr login:

werf cr login <registry url>

Например:

# Login with username and password from command line
werf cr login -u username -p password registry.example.com

# Login with token from command line
werf cr login -u username -p token registry.example.com

# Login into insecure registry (over http)
werf cr login --insecure-registry registry.example.com

В случае использования команды ci-env с поддерживаемыми CI/CD-системами аутентификация во встроенные container registry выполняется в рамках команды, поэтому использование команды werf cr login в этом случае не требуется.

Тегирование образов

Тегирование образов werf выполняется автоматически в рамках сборочного процесса. Используется оптимальная схема тегирования, основанная на содержимом образа, которая предотвращает лишние пересборки и время простоя приложения при выкате.

Тегирование в деталях

По умолчанию тег — это некоторый идентификатор в виде хэша, включающий в себя контрольную сумму инструкций и файлов сборочного контекста. Например:

registry.example.org/group/project  d4bf3e71015d1e757a8481536eeabda98f51f1891d68b539cc50753a-1589714365467  7c834f0ff026  20 hours ago  66.7MB
registry.example.org/group/project  e6073b8f03231e122fa3b7d3294ff69a5060c332c4395e7d0b3231e3-1589714362300  2fc39536332d  20 hours ago  66.7MB

Такой тег будет меняться только при изменении входных данных для сборки образа. Таким образом, один тег может быть переиспользован для разных Git-коммитов. werf берёт на себя:

  • расчёт таких тегов;
  • атомарную публикацию образов по этим тегам в репозиторий или локально;
  • передачу тегов в Helm-чарт.

Образы в репозитории именуются согласно следующей схеме: CONTAINER_REGISTRY_REPO:DIGEST-TIMESTAMP_MILLISEC. Здесь:

  • CONTAINER_REGISTRY_REPO — репозиторий, заданный опцией --repo;
  • DIGEST — контрольная сумма от:
    • сборочных инструкций, описанных в Dockerfile или werf.yaml;
    • файлов сборочного контекста, используемых в тех или иных сборочных инструкциях.
  • TIMESTAMP_MILLISEC — временная отметка, которая проставляется в процессе процедуры сохранения слоя в container registry после того, как стадия была собрана.

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

Тегирование промежуточных слоёв

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

Добавление произвольных тегов

Пользователь может добавить произвольное количество дополнительных тегов с опцией --add-custom-tag:

werf build --repo REPO --add-custom-tag main

# Можно добавить несколько тегов-алиасов.
werf build --repo REPO --add-custom-tag main --add-custom-tag latest --add-custom-tag prerelease

Шаблон тега может включать следующие параметры:

  • %image%, %image_slug% или %image_safe_slug% для использования имени образа из werf.yaml (обязательно при сборке нескольких образов);
  • %image_content_based_tag% для использования content-based тега werf.
werf build --repo REPO --add-custom-tag "%image%-latest"

ЗАМЕЧАНИЕ: При использовании опций создаются дополнительные теги-алиасы, ссылающиеся на автоматические теги-хэши. Полное отключение создания автоматических тегов не предусматривается.

Послойное кэширование образов

Послойное кэширование образов является неотъемлемой частью сборочного процесса werf. werf сохраняет и переиспользует сборочный кэш в container registry, а также синхронизирует работу параллельных сборщиков.

Как работает сборка

Сборка реализована с отступлением от стандартной для Docker парадигмы разделения стадий build и push, и переходом к одной стадии build, совмещающей и сборку, и публикацию слоёв.

Стандартный подход сборки и публикации образов и слоёв через Docker может выглядеть следующим образом:

  1. Скачивание сборочного кеша из container registry (опционально).
  2. Локальная сборка всех промежуточных слоёв образа с использованием локального кеша слоёв.
  3. Публикация собранного образа.
  4. Публикация локального сборочного кеша в container registry (опционально).

Алгоритм сборки образов в werf работает по-другому:

  1. Если очередной собираемый слой уже есть в container registry, то его сборка и скачивание не происходит.
  2. Если очередной собираемый слой отсутствует в container registry, то скачивается предыдущий слой, базовый для сборки текущего.
  3. Новый слой собирается на локальной машине и публикуется в container registry.
  4. В момент публикации автоматически разрешаются конфликты между сборщиками с разных хостов, которые пытаются опубликовать один и тот же слой. При общем backend синхронизации и действующей аренде блокировки сборщик повторно проверяет registry и переиспользует уже опубликованный подходящий слой. (Такое возможно за счёт использования встроенного сервиса синхронизации).
  5. И т.д. до тех пор, пока не будут собраны все слои образа.

Алгоритм выборки стадии в werf можно представить следующим образом:

  1. Рассчитывается дайджест стадии.
  2. Выбираются все стадии, подходящие под дайджест, т.к. с одним дайджестом может быть связанно несколько стадий в репозитории.
  3. Для stapel-сборщика, если текущая стадия связана с Git (стадия Git-архив, пользовательская стадия с Git-патчами или стадия git latest patch), тогда выбираются только те стадии, которые связаны с коммитами, являющимися предками текущего коммита. Таким образом, коммиты соседних веток будут отброшены.
  4. Выбирается старейший по времени TIMESTAMP_MILLISEC.

Если запустить сборку с хранением образов в репозитории, то werf сначала проверит наличие требуемых стадий в локальном хранилище и скопирует оттуда подходящие стадии, чтобы не пересобирать эти стадии заново.

Поиск стадий повторно использует кешированные списки в течение команды, включая поиски, которые ничего не находят. Каждый список загружается при первом обращении. Свежие проверки и успешные локальные публикации обновляют соответствующий кешированный список. Перед публикацией стадии werf заново проверяет основной репозиторий под блокировкой стадии (шаг 4 выше): стадия, опубликованная другим сборщиком после получения кешированного списка, может привести к лишней работе по сборке, но перед публикацией её наличие проверяется повторно.

ЗАМЕЧАНИЕ: Предполагается, что репозиторий образов для проекта не будет удален или очищен сторонними средствами без негативных последствий для пользователей CI/CD, построенного на основе werf (см. очистка образов).

Dockerfile

По умолчанию Dockerfile-образы кешируются одним образом в container registry.

Для включения послойного кеширования Dockerfile-инструкций в container registry необходимо использовать директиву staged в werf.yaml. Директива может быть установлена глобально на корневом уровне werf.yaml для всех образов или локально для конкретных образов, где локальная настройка переопределит глобальную:

# werf.yaml
image: example
dockerfile: ./Dockerfile
staged: true

ВАЖНО: Опция staged: true поддерживается только при использовании сборщика Buildah.

Stapel

Образы stapel кешируются в режиме послойного кеширования в container registry по умолчанию без дополнительной конфигурации.

Версионирование кеша

Вы можете использовать глобальную директиву build.cacheVersion или её локальный аналог <image>.cacheVersion, чтобы явно управлять версией кеша образов через конфигурацию и сохранять воспроизводимость всех сборок. Если обе директивы указаны, локальная имеет приоритет.

Пример использования:

project: test
configVersion: 1
build:
  cacheVersion: global-cache-version
---
image: backend
cacheVersion: user-cache-version
dockerfile: Dockerfile
---
image: frontend
cacheVersion: frontend-cache-version
dockerfile: Dockerfile
staged: true
---
image: user
cacheVersion: user-cache-version
from: alpine:3.14

Проверка того, что образы собраны

werf build --check-built-images (алиасы: --require-built-images, -Z, $WERF_CHECK_BUILT_IMAGES), а также --require-built-images у команд, которые работают с образами, но не собирают их, проверяют, что все необходимые проекту образы уже опубликованы. Отсутствие стадий приводит к ошибке stages required; недоступность выходных образов или произвольных тегов также завершает проверку ошибкой.

Проверка выполняется только на чтение, а поиск стадий ограничен основным репозиторием:

  • стадия, найденная в --secondary-repo, не копируется в основной репозиторий, а сами вторичные репозитории вообще не опрашиваются — поэтому проект, стадии которого есть только во вторичном репозитории, не пройдёт проверку, пока обычная сборка не скопирует их;
  • ничего не публикуется: ни стадии, ни manifest list для мультиплатформенного образа, ни произвольные теги, ни записи managed-образов, ни Git-метаданные. Произвольные теги вместо создания проверяются на существование. werf build --check-built-images и его алиасы также пропускают регистрацию синхронизации и используют локальные блокировки; остальные команды с --require-built-images сохраняют обычную инициализацию синхронизации. Автоматическая очистка хоста тоже не запускается, поэтому проверка никогда не удаляет данные локального кэша и локальные образы бэкенда;
  • настроенный результат сборки по-прежнему проверяется, только на чтение: при --final-repo финальный образ обязан существовать там и не копируется туда, поэтому проверка никогда не сообщает об образе как о доступном по адресу, где его нет.

В отличие от обычной сборки проверка не доверяет отрицательному результату списка на команду: если в списке нет стадии для дайджеста, основной репозиторий перечисляется заново, чтобы стадия, опубликованная во время проверки, считалась собранной, а не отсутствующей. Стадия, уже имеющаяся в списке, используется как есть.

Параллельность и порядок сборки образов

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

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

При использовании Dockerfile-стадий параллельность их сборки также определяется на основе дерева зависимостей. Также, если Dockerfile-стадия используется разными образами, объявленными в werf.yaml, werf обеспечит однократную сборку этой общей стадии без лишних пересборок

Параллельная сборка в werf регулируется двумя параметрами --parallel и --parallel-tasks-limit. По умолчанию параллельная сборка включена и собирается не более 5 образов одновременно. Установка --parallel-tasks-limit в 0 или отрицательное значение запускает по одному воркеру на образ, поэтому все образы, готовые к сборке, собираются одновременно.

Рассмотрим следующий пример:

# backend/Dockerfile
FROM node as backend
WORKDIR /app
COPY package*.json /app/
RUN npm ci
COPY . .
CMD ["node", "server.js"]
# frontend/Dockerfile

FROM ruby as application
WORKDIR /app
COPY Gemfile* /app
RUN bundle install
COPY . .
RUN bundle exec rake assets:precompile
CMD ["rails", "server", "-b", "0.0.0.0"]

FROM nginx as assets
WORKDIR /usr/share/nginx/html
COPY configs/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=application /app/public/assets .
COPY --from=application /app/vendor .
ENTRYPOINT ["nginx", "-g", "daemon off;"]
project: my-project
configVersion: 1
---
image: backend
dockerfile: Dockerfile
context: backend
---
image: frontend
dockerfile: Dockerfile
context: frontend
target: application
---
image: frontend-assets
dockerfile: Dockerfile
context: frontend
target: assets

Имеется 3 образа: backend, frontend и frontend-assets. Между ними нет зависимостей на уровне werf — COPY --from=application разрешается внутри сборки Dockerfile, а staged по умолчанию false, — поэтому все они попадают в Level #0. Зависимости на уровне werf (dependencies:, import:, fromImage: или стадии Dockerfile-образа со staged: true) образуют следующие уровни.

В этом случае werf выведет следующий план сборки:

┌ Concurrent build plan (no more than 5 images at the same time)
│ Level #0:
│ - 🛳️  (1/3) image backend
│ - 🛳️  (2/3) image frontend
│ - 🛳️  (3/3) image frontend-assets
└ Concurrent build plan (no more than 5 images at the same time)

Сетевая изоляция

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

На текущий момент сетевая изоляция поддерживается только при использовании Docker в качестве сборочного бэкенда. Она работает как для Dockerfile, так и для Stapel синтаксисов.

Конфигурация

Вы можете указать сетевой режим в конфигурации werf.yaml для каждого образа с помощью параметра network:

project: my-project
configVersion: 1
---
image: backend
dockerfile: Dockerfile
network: none # сеть отключена во время сборки
---
image: frontend
from: alpine:3.14
network: host # использовать сеть хоста

Опция CLI

Вы также можете установить сетевой режим глобально для процесса сборки с помощью опции CLI --backend-network:

werf build --backend-network none

Опция CLI --backend-network имеет приоритет над параметром network, определенным в конфигурации werf.yaml.

Использование SSH-агента

werf позволяет использовать SSH-агент для аутентификации при доступе к удалённым Git-репозиториям или выполнении команд в сборочных контейнерах.

Замечание: Только пользователь root внутри сборочного контейнера может получить доступ к UNIX-сокету из переменной окружения SSH_AUTH_SOCK.

По умолчанию werf пытается использовать SSH-агент, запущенный в системе, определяя его через переменную окружения SSH_AUTH_SOCK.

Если SSH-агент не запущен, werf может автоматически запустить временный агент и загрузить доступные ключи (~/.ssh/id_rsa|id_dsa). Этот агент работает только в течение выполнения команды и не конфликтует с системным SSH-агентом.

Указание конкретных SSH-ключей

Флаг --ssh-key PRIVATE_KEY_FILE_PATH позволяет ограничить использование SSH-агента конкретными ключами (можно указать несколько раз для добавления нескольких ключей). werf запустит временный SSH-агент только с указанными ключами.

werf build --ssh-key ~/.ssh/private_key_1 --ssh-key ~/.ssh/private_key_2

Ограничения на macOS

При работе на macOS нужно учитывать, что werf выполняет сборку внутри контейнера, запущенного в Linux-VM Docker Desktop. Docker Desktop предоставляет собственный прокси-сокет, который перенаправляет системный SSH-сокет macOS (обычно тот, что запущен через launchd для текущего пользователя). Использовать произвольный агент возможности нет.

Таким образом, на macOS временный SSH-агент werf и опция --ssh-key не работают. Ключи необходимо заранее добавлять в системный SSH-агент macOS.

Мультиплатформенная и кроссплатформенная сборка

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

Мультиплатформенная сборка использует механизмы кроссплатформенного исполнения инструкций, предоставляемые ядром Linux и эмулятором QEMU. Перечень поддерживаемых архитектур. Подготовка хост-системы для мультиплатформенной сборки рассмотрена в разделе установки werf

Поддержка мультиплатформенной сборки для разных вариантов синтаксиса сборки, режимов сборки и используемого бэкенда:

  buildah docker-server
Dockerfile полная поддержка полная поддержка
staged Dockerfile полная поддержка не поддерживается
stapel полная поддержка только linux/amd64 и linux/arm64

Сборка образов под одну целевую платформу

По умолчанию в качестве целевой используется платформа хоста, где запущен werf. Выбор другой целевой платформы для собираемых образов осуществляется с помощью параметра --platform:

werf build --platform linux/arm64

— все конечные образы, указанные в werf.yaml, будут собраны для указанной платформы с использованием эмуляции.

Целевую платформу можно также указать директивой конфигурации build.platform:

# werf.yaml
project: example
configVersion: 1
build:
  platform:
  - linux/arm64
---
image: frontend
dockerfile: frontend/Dockerfile
---
image: backend
dockerfile: backend/Dockerfile

В этом случае запуск werf build без параметров вызовет сборку образов для указанной платформы (при этом явно указанный параметр --platform переопределяет значение из werf.yaml).

Сборка образов под множество целевых платформ

Поддерживается и сборка образов сразу для набора архитектур. В этом случае в container registry публикуется манифест включающий в себя собранные образы под каждую из указанных платформ (во время скачивания такого образа автоматически будет выбираться образ под требуемую архитектуру).

Можно определить общий список платформ для всех образов в werf.yaml с помощью конфигурации:

# werf.yaml
project: example
configVersion: 1
build:
  platform:
  - linux/arm64
  - linux/amd64
  - linux/arm/v7

Можно определить список целевых платформ отдельно для каждого собираемого образа (такая настройка будет иметь приоритет над общим списком определённым в werf.yaml):

# werf.yaml
project: example
configVersion: 1
---
image: mysql
dockerfile: ./Dockerfile.mysql
platform:
- linux/amd64
---
image: backend
dockerfile: ./Dockerfile.backend
platform:
- linux/amd64
- linux/arm64

Общий список можно также переопределить параметром --platform непосредственно в момент вызова сборки:

werf build --platform=linux/amd64,linux/i386

— такой параметр переопределяет список целевых платформ указанных в werf.yaml (как общих, так и для отдельных образов).

Использование зеркал для docker.io

Вы можете использовать зеркала для используемого по умолчанию docker.io container registry.

Docker

Если вы используете Docker для сборки, добавьте registry-mirrors в файл /etc/docker/daemon.json:

{
  "registry-mirrors": ["https://<my-docker-io-mirror-host>"]
}

После этого перезапустите Docker и разлогиньтесь из docker.io с помощью команды:

werf cr logout

Buildah

Если вы используете Buildah для сборки, вместо редактирования daemon.json используйте опцию --container-registry-mirror для werf-команд. Например:

werf build --container-registry-mirror=mirror.gcr.io

Для добавления нескольких зеркал используйте опцию --container-registry-mirror несколько раз.

Помимо опции, поддерживается использование переменных окружения WERF_CONTAINER_REGISTRY_MIRROR_*, например:

export WERF_CONTAINER_REGISTRY_MIRROR_GCR=mirror.gcr.io
export WERF_CONTAINER_REGISTRY_MIRROR_LOCAL=docker.mirror.local

Зеркала, заданные таким способом, по умолчанию считаются secure (https) зеркалами. При необходимости для обращений к registry можно включить глобальный insecure-режим werf через --insecure-registry или --skip-tls-verify-registry.

werf также читает зеркала container registry и standalone insecure registries из registries.conf.

Поддерживаются следующие пути в порядке приоритета:

  1. путь из переменной окружения CONTAINERS_REGISTRIES_CONF;
  2. ~/.config/containers/registries.conf;
  3. /etc/containers/registries.conf.

Если задан CONTAINERS_REGISTRIES_CONF, werf использует только этот файл и соседнюю директорию <path>.d.

Если CONTAINERS_REGISTRIES_CONF не задан, werf использует первый найденный файл из стандартных путей и соседнюю директорию <path>.d для этого файла.

Из этой конфигурации werf использует:

  • зеркала для docker.io;
  • standalone insecure registries из [[registry]] insecure = true.

Insecure-зеркала должны задаваться через registries.conf. Insecure-зеркало для docker.io не делает тот же host standalone insecure registry автоматически. Если один и тот же host должен использоваться и как зеркало docker.io, и как standalone insecure registry, его нужно описать двумя отдельными записями.

Использование container registry

При использовании werf container registry используется не только для хранения конечных образов, но также для сборочного кэша и служебных данных, необходимых для работы werf (например, метаданные для очистки container registry на основе истории Git). Репозиторий container registry задаётся параметром --repo:

werf converge --repo registry.mycompany.org/project

В дополнение к основному репозиторию существует ряд дополнительных:

  • --final-repo для сохранения конечных образов в отдельном репозитории;
  • --meta-repo для хранения служебных метаданных werf (используемых для очистки на основе истории Git) в отдельном репозитории;
  • --secondary-repo для использования репозитория в режиме read-only (например, для использования container registry CI, в который нельзя пушить, но можно переиспользовать сборочный кэш);
  • --cache-repo для поднятия репозитория со сборочным кэшом рядом со сборщиками.

ВАЖНО. Для корректной работы werf container registry должен быть надёжным (persistent), а очистка должна выполняться только с помощью специальной команды werf cleanup

При получении списка тегов werf запрашивает до 1 000 000 тегов на страницу, поэтому список даже большого репозитория обычно получается за несколько запросов вместо сотен. Container registry может вернуть страницу меньшего размера со ссылкой на следующую страницу — werf переходит по таким ссылкам. Чем больше страница, тем больше ответ, который werf держит в памяти: тег занимает не более 128 байт, поэтому полная страница из миллиона тегов может весить более ста мегабайт. Чтобы изменить это, задайте в WERF_DOCKER_REGISTRY_TAGS_PAGE_SIZE другое количество тегов на страницу или 0, чтобы использовать размер страницы клиентской библиотеки container registry (1000 тегов). Отрицательное или нечисловое значение считается ошибкой.

Amazon ECR ограничивает страницу 1000 тегами, поэтому для реализации container registry ecr и для public.ecr.aws werf сразу использует размер страницы клиентской библиотеки, независимо от значения переменной окружения. Если другой container registry отвечает на запрос списка распознаваемым отказом из-за размера страницы, werf повторяет запрос списка с размером страницы клиентской библиотеки и, если запрос удался, продолжает использовать этот размер страницы для данного хоста до завершения команды werf. Любая другая ошибка возвращается как есть.

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

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

werf build --repo registry.mycompany.org/project --final-repo final-registry.mycompany.org/project-final

Финальные репозитории позволяют сократить время загрузки образов и снизить нагрузку на сеть за счёт поднятия container registry ближе к кластеру Kubernetes, на котором происходит развёртывание приложения. Также при необходимости финальные репозитории могут использоваться в том же container registry, что и основной репозиторий (--repo).

Дополнительный репозиторий для служебных метаданных

По умолчанию werf хранит служебные метаданные в основном репозитории (--repo) рядом со стадиями образов. При необходимости эти метаданные — image-metadata, используемые для очистки на основе истории Git, список managed images, метаданные custom-тегов и запись о последней очистке — можно вынести в отдельный репозиторий с помощью --meta-repo:

werf build --repo registry.mycompany.org/project --meta-repo registry.mycompany.org/project-meta

(Маркеры отклонённых стадий и алиасы custom-тегов на образах стадий остаются в --repo, так как привязаны к образам стадий.)

Вынесение метаданных из репозитория стадий полезно, когда в репозитории накапливается большое количество служебных тегов. Записи image-metadata создаются для каждой пары образа, коммита и стадии, поэтому при активной разработке их быстро становится многократно больше, чем самих стадий, а каждая операция werf, которой нужно найти стадию, получает полный список тегов --repo и фильтрует его. Перенос метаданных в --meta-repo оставляет в этом списке только стадии. Все команды werf (build, cleanup, purge и т. д.) должны вызываться с одним и тем же значением --meta-repo.

Защита от несогласованного использования --meta-repo

Чтобы метаданные не оказались разделены между двумя репозиториями (из-за чего очистка может удалить используемые образы), werf при первой записи метаданных в --meta-repo сохраняет в --repo маркер для проекта. В дальнейшем маркер проверяется при каждом запуске:

  • пропуск --meta-repo или указание другого значения — жёсткая ошибка, в том числе если --meta-repo указывает на тот же репозиторий, что и --repo;
  • команды только для чтения лишь проверяют маркер и никогда его не пишут, поэтому учётные данные только на чтение продолжают работать.

Подключение --meta-repo для существующего проекта

Указать --meta-repo проекту, у которого в --repo уже есть метаданные, можно сразу: новые метаданные начинают писаться в отдельный репозиторий. Но метаданные, уже накопленные в --repo, туда не переезжают, и werf, читающий только --meta-repo, их не видит — поэтому перенесите их до первой очистки, иначе cleanup будет принимать решения о судьбе стадий по неполной картине и удалит используемые образы:

werf meta-repo migrate --from registry.mycompany.org/project --to registry.mycompany.org/project-meta

migrate копирует метаданные из --from в --to (сначала копирование с проверкой, идемпотентно — можно запускать повторно), записывает маркер в --from, после чего удаляет каждый оригинал из --from — но только после того, как его копия подтверждена в --to. Чтобы сохранить оригиналы, укажите --remove-source=false.

Чтобы снять защиту, выполните werf meta-repo detach --repo registry.mycompany.org/project. Команда удаляет только маркер; метаданные обратно не переносятся, поэтому последующие запуски без --meta-repo будут читать устаревшие или пустые метаданные из --repo.

werf purge удаляет маркер последним шагом, после удаления стадий и метаданных проекта. Поэтому заново созданный проект можно снова собирать без --meta-repo, выполнять werf meta-repo detach для этого не требуется. Если очистка прервалась на середине, маркер остаётся на месте, чтобы метаданные, которые ещё лежат в --meta-repo, оставались защищены.

Дополнительный репозиторий для быстрого доступа к сборочному кэшу

С помощью параметра --cache-repo можно указать один или несколько т.н. кеширующих репозиториев.

# Дополнительный кэширующий репозиторий в локальной сети.
werf build --repo registry.mycompany.org/project --cache-repo localhost:5000/project

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

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

Очистка кeширующего репозитория может осуществляться путём его полного удаления без каких-либо рисков.

Синхронизация сборщиков

Для согласованной публикации собираемых образов werf синхронизирует параллельных сборщиков. По умолчанию используется публичный сервис синхронизации по адресу https://synchronization.werf.io/ и от пользователя ничего дополнительно не требуется.

Как работает сервис синхронизации

Сервис синхронизации — это компонент werf, который предназначен для координации нескольких процессов werf и выполняет роль менеджера блокировок. Блокировки требуются для корректной публикации новых образов в container registry и реализации алгоритма сборки, описанного в разделе «Послойное кэширование образов».

Сервис синхронизации получает общий client ID, имя проекта и дайджест стадии, которые определяют блокировку. Учётные данные registry и содержимое образов не входят в запросы блокировки.

Все сборщики одного репозитория должны использовать общий backend синхронизации и имя проекта. Ошибка получения блокировки останавливает публикацию. Как и в v2, блокировки с арендой не обеспечивают защиту записи на стороне registry при длительном сетевом разделении или перезапуске сервера с хранением блокировок в памяти.

В качестве сервиса синхронизации может выступать:

  1. HTTP-сервер синхронизации, реализованный в команде werf synchronization.
  2. Ресурс ConfigMap в кластере Kubernetes. В качестве механизма используется библиотека lockgate, реализующая распределённые блокировки через хранение аннотаций в выбранном ресурсе.
  3. Локальные файловые блокировки, предоставляемые операционной системой.

Использование собственного сервиса синхронизации

HTTP-сервер

Сервер синхронизации можно запустить командой werf synchronization, например для использования порта 55581 (по умолчанию):

werf synchronization --host 0.0.0.0 --port 55581

По умолчанию HTTP-сервер хранит блокировки в памяти процесса. Разные процессы сервера не разделяют блокировки, даже если указаны одинаковые каталоги; при перезапуске блокировки теряются. Для хранения блокировок в ConfigMap фиксированного namespace werf-synchronization используйте werf synchronization --kubernetes. Опции --local, --local-lock-manager-base-dir, --local-stages-storage-cache-base-dir, --kubernetes-namespace-prefix и --ttl принимаются для совместимости, но не влияют на работу сервера.

— данный сервер поддерживает только работу в режиме HTTP, для использования HTTPS необходима настройка дополнительной SSL-терминации сторонними средствами (например через Ingress в Kubernetes).

Далее во всех командах werf, которые используют параметр --repo дополнительно указывается параметр --synchronization=http[s]://DOMAIN, например:

werf build --repo registry.mydomain.org/repo --synchronization https://synchronization.domain.org
werf converge --repo registry.mydomain.org/repo --synchronization https://synchronization.domain.org

Синхронизация через Kubernetes

Для блокировок непосредственно через ConfigMap используйте --synchronization=kubernetes://NAMESPACE[:CONTEXT][@CONFIG_PATH]. Поддерживается встроенный kubeconfig: kubernetes://NAMESPACE@base64:BASE64_CONFIG_DATA. Все сборщики должны использовать общий кластер и namespace. Клиенту нужны права на создание namespace, чтение, создание и обновление его ConfigMap.

Локальная синхронизация

Включается опцией --synchronization=:local. Локальный менеджер блокировок использует файловые блокировки, предоставляемые операционной системой.

werf build --repo registry.mydomain.org/repo --synchronization :local
werf converge --repo registry.mydomain.org/repo --synchronization :local

ЗАМЕЧАНИЕ: Данный способ подходит лишь в том случае, если в вашей CI/CD системе все запуски werf происходят с одного и того же раннера.

Отчёт по сборке

Отчёт по сборке содержит результаты сборки: имена образов, теги, дайджесты и другие метаданные. Его можно сохранить в файл, а затем использовать в других командах werf, чтобы пропустить повторную сборку.

Сохранение отчёта по сборке

Используйте --save-build-report для сохранения отчёта в файл:

werf build --save-build-report --repo REPO

По умолчанию отчёт сохраняется в файл .werf-build-report.json в формате JSON. С помощью --build-report-path можно указать произвольный путь — формат определяется автоматически по расширению файла (.json, .env):

werf build --save-build-report --build-report-path .werf-build-report.env --repo REPO

Флаг --save-build-report поддерживается всеми командами, в рамках которых выполняется сборка.

Важно: Получить теги до сборки невозможно — они формируются в процессе. Чтобы использовать теги, сохраните их после выполнения сборки с помощью --save-build-report.

Формат JSON

JSON-отчёт содержит расширенную информацию о сборке:

  • Runtime — информация об окружении сборки:
    • Версия werf, сформировавшая отчёт (WerfVersion)
    • Используемый контейнерный бэкенд (Backend: docker или buildah)
    • Флаг запуска werf внутри контейнера (InContainer).
  • Images — список собранных образов:
    • Имя образа в werf (WerfImageName)
    • Тип конфигурации образа (ConfigType: stapel, dockerfile, staged, unknown)
    • Теги образа (DockerImageName, DockerRepo, DockerTag)
    • Целевая платформа (TargetPlatform), например linux/amd64
    • Был ли образ пересобран (Rebuilt)
    • Является ли образ конечным или промежуточным (Final). Конечные образы доступны в values Helm-чарта, могут быть помечены произвольными тегами, опубликованы в финальный репозиторий и экспортированы. Промежуточные образы (final: false) используются только как зависимости сборки
    • Размер образа в байтах (Size) и время сборки в секундах (BuildTime)
    • Git-коммит, на котором был собран образ (Commit)
    • Стадии сборки (Stages) с деталями:
      • Имя стадии (Name)
      • Теги (DockerImageName, DockerTag, DockerImageID, DockerImageDigest)
      • Время создания образа стадии в Unix time nanoseconds (CreatedAt)
      • Размер (Size) в байтах
      • Источник базового образа (SourceType: local, secondary, cache-repo, registry)
      • Был ли загружен базовый образ (BaseImagePulled)
      • Была ли стадия пересобрана (Rebuilt)
      • Время сборки стадии в секундах (BuildTime)
      • Git-коммит, на котором была собрана стадия (Commit).
  • ImagesByPlatform — разрез по платформам для multiarch-сборок. Поле включается только если установлена переменная окружения WERF_ENABLE_REPORT_BY_PLATFORM=1. Структура записей та же, что и у Images, но данные сгруппированы по имени образа и платформе.

  • Operations — агрегированные тайминги низкоуровневых операций за весь запуск команды (сборка и инспектирование образов бэкендом, запросы к registry, git-операции, взятие блокировок и т.д.). Поле заполняется только при установленном флаге --build-report-operations ($WERF_BUILD_REPORT_OPERATIONS) или при включённом отладочном логировании (--log-debug). Ключи операций именуются как подсистема: операция, например registry: tags list, docker: image build или git: clone; это вызовы бэкендов, а не отдельные HTTP-запросы. Для каждой операции: количество вызовов (Count), суммарная длительность по всем параллельным воркерам (TotalTimeSeconds), реальная длительность с учётом параллельного выполнения — объединение возможно пересекающихся интервалов (WallTimeSeconds), средняя (AvgTimeSeconds) и максимальная (MaxTimeSeconds) длительности. Operations в основном считает вызовы, которые действительно дошли до бэкенда: там, где таймер находится внутри кеширующего слоя, обращение, отвеченное из этого кеша, вызов сюда не добавляет. Это верно не везде: таймеры git: patch и git: archive запускаются до обращения к дисковому кешу, поэтому вызов, отвеченный с диска, всё равно учитывается — значения Operations в общем случае нельзя вывести из исходов кеша в CacheOperations. Некоторые учитываемые вызовы внутри себя делают другой учитываемый вызов — например, buildah-вызов pull инспектирует только что скачанный образ, — поэтому строки могут пересекаться и вкладываться друг в друга; не складывайте их в длительность команды. Консольный блок покрывает весь запуск команды, а сохранённый отчёт — операции с момента предыдущего отчёта той же команды: при --follow в отчёт попадает всё, что произошло после предыдущего — включая ожидание изменений между сборками и неудачные retry-попытки.

  • CacheOperations — счётчики того, как кеширующие слои отвечали на проходящие через них обращения; ключи верхнего уровня те же, что и в Operations, а вложенные — слой кеша, до которого обращение дошло (memory или disk). Каждый вызов через кеширующий слой классифицируется ровно один раз и учитывается в момент его завершения: Hit (пригодный результат из кеша, в том числе пустой), Miss (у кеша не было пригодного результата — само по себе это не означает, что нижележащий вызов был выполнен: присоединившееся или отменённое обращение может до бэкенда и не дойти) или Bypass (вызывающий запросил свежий результат и к кешу не обращался). Lookups — их сумма. Многослойный кеш считает по одному обращению на каждый слой, которого вызов действительно достиг: результат, найденный в памяти, не меняет запись disk той же операции, а промах в памяти, закрытый с диска, — это по одному обращению на каждый из двух слоёв. Поэтому записи одной операции нельзя складывать в число запросов приложения: каждая из них описывает свой слой. Shared считает вызовы, присоединившиеся к уже выполняющемуся запросу вместо запуска своего, и потому входит в Miss или Bypass: локальный список образов, присоединившийся к перечислению, которое начал другой вызывающий, тоже учитывается как Shared — даже если затем он отверг это перечисление как слишком старое и дождался следующего, — с тем исходом, который даёт его собственная опция кеширования (Miss, если он обращался к кешу, и Bypass, если он запрашивал свежий результат). Доля попаданий — Hit / (Hit + Miss) по каждому слою — отдельно не сериализуется. Кеширующий слой, не получивший ни одного обращения, не попадает в отчёт вовсе, а не строкой из нулей. Поле заполняется при тех же условиях, что и Operations, а сохранённый отчёт покрывает только обращения с момента предыдущего отчёта той же команды.

  • StageCache — счётчики того, как стадии были получены при сборке, по источникам (в стадиях): найдены в локальном хранилище или в repo, скопированы из secondary-хранилища, собраны или discarded. Запуск, в котором все образы были получены по content-based быстрому пути, не работает ни с одной стадией, поэтому секция и консольная строка Stages: не выводятся вовсе, а не показывают нули. discarded считает стадии, собранные локально и затем отброшенные, потому что к моменту окончания сборки другой сборщик уже опубликовал подходящую стадию: такая стадия одновременно учитывается как найденная в хранилище стадий, поэтому discarded — подмножество переиспользованных стадий, а не отдельный исход. Это не означает ни того, что опубликованная стадия сломана, ни того, что были удалены старые образы. Поле заполняется только при установленном флаге --build-report-operations ($WERF_BUILD_REPORT_OPERATIONS) или при включённом отладочном логировании (--log-debug).

  • RegistryCache — счётчики запросов списка тегов, использующих кешированный или общий результат: registry tags cache hit (список взят из кеша тегов в памяти) и registry tags shared result (результат разделён между параллельными запросами к тому же репозиторию). Общий результат учитывается для каждого вызывающего, включая инициатора запроса к registry, поэтому это не число предотвращённых сетевых запросов. Эти счётчики вынесены из StageCache, поскольку считают запросы, а не стадии. Они сохранены для совместимости и заменяются секцией CacheOperations, которая классифицирует каждое обращение, а не считает две частные ситуации. Поле заполняется только при установленном флаге --build-report-operations ($WERF_BUILD_REPORT_OPERATIONS) или при включённом отладочном логировании (--log-debug) и отсутствует, если таких запросов не было. Обе секции кеша подчиняются тем же правилам, что и Operations: консольный блок покрывает весь запуск команды, а сохранённый отчёт — только интервал с момента предыдущего отчёта той же команды.

  • Recovery — счётчики восстановления из сломанного или конфликтного состояния хранилища; вынесены из StageCache, поскольку считают сбои, а не способ получения стадии: broken stage detections (чтение, скачивание или мутация стадии, которую repo-хранилище стадий отвергло как сломанный образ; стадия, которой просто нет, которая отклонена или недоступна, сломанной не считается, а каждое независимое обнаружение учитывается заново, включая повторные обращения к той же стадии) и conveyor restarts (попытки конвейера после первой, вызванные неожиданным состоянием хранилища стадий; запланированный backoff или отмена до следующей попытки ничего не добавляют). Секция и консольная строка Recovery: не выводятся, если ничего не было обнаружено. Поле заполняется при тех же условиях, что и Operations.

Пример отчёта в формате JSON (секции Operations, CacheOperations, StageCache, RegistryCache и Recovery присутствуют, потому что отчёт сгенерирован с флагом --build-report-operations). Имена операций и значения ниже приведены для иллюстрации:

{
  "Runtime": {
    "WerfVersion": "v3.6.1",
    "Backend": "docker",
    "InContainer": false
  },
  "Images": {
    "frontend": {
      "WerfImageName": "frontend",
      "ConfigType": "dockerfile",
      "DockerRepo": "localhost:5000/demo-app",
      "DockerTag": "079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353",
      "DockerImageID": "sha256:9b3a32dfe5a4aa46d96547e3f8e678626f96741776d78656ea72cab7117612bf",
      "DockerImageDigest": "sha256:54f564edebb6e0699dc0e43de4165488f86fbc76b0c89d88311d7cc06ae397f5",
      "DockerImageName": "localhost:5000/demo-app:079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353",
      "Rebuilt": true,
      "Final": true,
      "Size": 20960980,
      "BuildTime": "4.08",
      "Commit": "9d1bb68ca2f4e8b0e2b6e5f5a3c7d1e4f2a0b3c9",
      "Stages": [
        {
          "Name": "from",
          "DockerImageName": "localhost:5000/demo-app:6f40fd07cdb62e03d7238e1fccb3341379bcd677ff6d7575317f3783-1752501287209",
          "DockerTag": "6f40fd07cdb62e03d7238e1fccb3341379bcd677ff6d7575317f3783-1752501287209",
          "DockerImageID": "sha256:2ae3fbc31d2b5f0d7d105a74693ed14bec6b106ad43c660b9162dfa00d24d4d0",
          "DockerImageDigest": "sha256:c4449ccfaee03e5b601290909e4d69178c40e39c9d2daf57d1fd74093beb4e10",
          "CreatedAt": 1752501286000000000,
          "Size": 20960798,
          "SourceType": "",
          "BaseImagePulled": false,
          "Rebuilt": true,
          "BuildTime": "3.42",
          "Commit": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
        },
        {
          "Name": "install",
          "DockerImageName": "localhost:5000/demo-app:079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353",
          "DockerTag": "079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353",
          "DockerImageID": "sha256:9b3a32dfe5a4aa46d96547e3f8e678626f96741776d78656ea72cab7117612bf",
          "DockerImageDigest": "sha256:54f564edebb6e0699dc0e43de4165488f86fbc76b0c89d88311d7cc06ae397f5",
          "CreatedAt": 1752501316000000000,
          "Size": 20960980,
          "SourceType": "",
          "BaseImagePulled": false,
          "Rebuilt": true,
          "BuildTime": "0.46",
          "Commit": "9d1bb68ca2f4e8b0e2b6e5f5a3c7d1e4f2a0b3c9"
        }
      ]
    }
  },
  "ImagesByPlatform": {},
  "Operations": {
    "registry: tags list": {
      "Count": 2,
      "TotalTimeSeconds": 2.905423333,
      "WallTimeSeconds": 1.8,
      "AvgTimeSeconds": 1.4527116665,
      "MaxTimeSeconds": 1.611141611
    },
    "docker: image build": {
      "Count": 2,
      "TotalTimeSeconds": 0.831474958,
      "WallTimeSeconds": 0.831474958,
      "AvgTimeSeconds": 0.415737479,
      "MaxTimeSeconds": 0.421835292
    },
    "docker: image list": {
      "Count": 1,
      "TotalTimeSeconds": 0.110243333,
      "WallTimeSeconds": 0.110243333,
      "AvgTimeSeconds": 0.110243333,
      "MaxTimeSeconds": 0.110243333
    },
    "git: clone": {
      "Count": 1,
      "TotalTimeSeconds": 0.213458291,
      "WallTimeSeconds": 0.213458291,
      "AvgTimeSeconds": 0.213458291,
      "MaxTimeSeconds": 0.213458291
    },
    "git: patch": {
      "Count": 1,
      "TotalTimeSeconds": 0.042113250,
      "WallTimeSeconds": 0.042113250,
      "AvgTimeSeconds": 0.042113250,
      "MaxTimeSeconds": 0.042113250
    },
    "sync: lock acquire": {
      "Count": 2,
      "TotalTimeSeconds": 0.001153668,
      "WallTimeSeconds": 0.001153668,
      "AvgTimeSeconds": 0.000576834,
      "MaxTimeSeconds": 0.000661667
    }
  },
  "CacheOperations": {
    "registry: tags list": {
      "memory": {
        "Lookups": 12,
        "Hit": 9,
        "Miss": 3,
        "Bypass": 0,
        "Shared": 1
      }
    },
    "git: patch": {
      "memory": {
        "Lookups": 3,
        "Hit": 2,
        "Miss": 1,
        "Bypass": 0,
        "Shared": 0
      },
      "disk": {
        "Lookups": 1,
        "Hit": 0,
        "Miss": 1,
        "Bypass": 0,
        "Shared": 0
      }
    },
    "docker: image list": {
      "memory": {
        "Lookups": 5,
        "Hit": 4,
        "Miss": 1,
        "Bypass": 0,
        "Shared": 0
      }
    }
  },
  "StageCache": {
    "built": 2,
    "found in repo stages storage": 1,
    "discarded": 1
  },
  "RegistryCache": {
    "registry tags cache hit": 9,
    "registry tags shared result": 2
  },
  "Recovery": {
    "broken stage detections": 1,
    "conveyor restarts": 1
  }
}

Чтобы извлечь список итоговых тегов образов из JSON-отчёта, можно использовать утилиту jq:

jq -r '.Images | to_entries | map({key: .key, value: .value.DockerImageName}) | from_entries' .werf-build-report.json

Результат:

{
  "backend": "localhost:5000/demo-app:caeb9005a06e34f0a20ba51b98d6b99b30f5cf3b8f5af63c8f3ab6c3-1752510176215",
  "frontend": "localhost:5000/demo-app:079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353"
}

Формат envfile

Отчёт в формате envfile содержит часть полей образов в виде переменных окружения. Для каждого образа генерируются следующие переменные:

  • WERF_<IMAGE>_DOCKER_IMAGE_NAME — полное имя образа с тегом
  • WERF_<IMAGE>_DOCKER_IMAGE_ID — ID образа
  • WERF_<IMAGE>_DOCKER_IMAGE_DIGEST — дайджест образа
  • WERF_<IMAGE>_DOCKER_REPO — репозиторий образа
  • WERF_<IMAGE>_DOCKER_TAG — тег образа
  • WERF_<IMAGE>_WERF_IMAGE_NAME — оригинальное имя образа в werf
  • WERF_<IMAGE>_FINAL — является ли образ конечным или промежуточным (true/false). Конечные образы доступны в values Helm-чарта, могут быть помечены произвольными тегами, опубликованы в финальный репозиторий и экспортированы. Промежуточные образы (final: false) используются только как зависимости сборки

Где <IMAGE> — имя образа в верхнем регистре, в котором символы /, -, ., + заменены на _.

Пример отчёта в формате envfile:

WERF_BACKEND_DOCKER_IMAGE_NAME=localhost:5000/demo-app:b94607bcb6e03a6ee07c8dc912739d6ab8ef2efc985227fa82d3de6f-1752510311968
WERF_BACKEND_DOCKER_IMAGE_ID=sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
WERF_BACKEND_DOCKER_IMAGE_DIGEST=sha256:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5
WERF_BACKEND_DOCKER_REPO=localhost:5000/demo-app
WERF_BACKEND_DOCKER_TAG=b94607bcb6e03a6ee07c8dc912739d6ab8ef2efc985227fa82d3de6f-1752510311968
WERF_BACKEND_WERF_IMAGE_NAME=backend
WERF_BACKEND_FINAL=true
WERF_FRONTEND_DOCKER_IMAGE_NAME=localhost:5000/demo-app:079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353
WERF_FRONTEND_DOCKER_IMAGE_ID=sha256:9b3a32dfe5a4aa46d96547e3f8e678626f96741776d78656ea72cab7117612bf
WERF_FRONTEND_DOCKER_IMAGE_DIGEST=sha256:54f564edebb6e0699dc0e43de4165488f86fbc76b0c89d88311d7cc06ae397f5
WERF_FRONTEND_DOCKER_REPO=localhost:5000/demo-app
WERF_FRONTEND_DOCKER_TAG=079dfdd3f51a800c269cdfdd5e4febfcc1676b2c0d533f520255961c-1752501317353
WERF_FRONTEND_WERF_IMAGE_NAME=frontend
WERF_FRONTEND_FINAL=true

Использование отчёта по сборке

Отчёт по сборке выступает контрактом между этапами CI/CD-пайплайна: этап сборки создаёт его, а последующие этапы (деплой, экспорт, рендер) используют. Это позволяет собрать образы один раз и переиспользовать результаты в нескольких заданиях или окружениях без пересборки. Подробный пример CI/CD см. в разделе Развертывание с использованием отчёта по сборке.

Флаг --use-build-report позволяет пропустить сборку и прочитать данные об образах из ранее сохранённого отчёта. Путь и формат отчёта задаются через --build-report-path (формат определяется автоматически по расширению файла). Поддерживаются форматы JSON и envfile.

Флаг --use-build-report поддерживается всеми командами, в рамках которых выполняется сборка.

Пример двухшагового CI-пайплайна — сборка в одном задании, деплой в другом:

# Шаг 1: Сборка и сохранение отчёта
werf build --save-build-report --build-report-path .werf-build-report.env --repo REPO

# Шаг 2: Деплой с использованием сохранённого отчёта (без пересборки)
werf converge --use-build-report --build-report-path .werf-build-report.env --repo REPO