
Продолжаем серию публикаций «werf vs...», которая вдохновлена часто задаваемыми вопросами. В первой статье мы объяснили, чем werf отличается от Helm. Теперь черед сравнения с еще более базовой утилитой — Docker.
Нас нередко спрашивают: зачем собирать образы с werf, если уже есть Docker с Dockerfile? Обычно мы отвечаем, что werf — не только про сборку. Утилита участвует в полном цикле CI/CD для доставки приложения в Kubernetes, а Docker при этом тоже используется, но как вспомогательный инструмент. Понятно, что такого объяснения недостаточно, нужны подробности.
Этот материал главным образом для тех, кто мало или совсем не знаком с werf, но знает Docker и хотя бы немного работал с ним. Для начала, как и в случае с Helm, попробуем разобраться, есть ли смысл в противопоставлении двух решений.
Роль в CI/CD
По традиции, начнем с общего плана — сравнения Docker и werf в контексте CI/CD-конвейера и выката приложения в Kubernetes. Здесь всё просто:
Функции | Docker | werf |
Сборка приложения в Docker-образ | + | + |
Публикация образов в container registry | + | + |
Очистка container registry от неактуальных образов на основе преднастроенных политик | − | + |
Деплой в Kubernetes | − | + |
Docker — базовое решение. Он предоставляет достаточно возможностей для непосредственной работы с образами на низком уровне, включая сборку, тегирование, запуск контейнеров, pull и push в container registry.
werf — более высокоуровневый инструмент. Пользователь werf оперирует не образами и контейнерами, а приложением (при необходимости — его компонентами); вся низкоуровневая механика остаётся «под капотом». Если у Docker, условно, сотни рычагов для работы с образами и контейнерами, то у werf не больше десятка — но лишь потому, что управление остальными рычагами утилита берет на себя.
Главное предназначение werf — доставка приложений в K8s. При этом утилита использует Docker как один из компонентов, «склеивая» его с Git, Helm и Kubernetes. werf упрощает работу с CI/CD-конвейерами, которые создаются на основе этих стандартных инструментов и CI-системы, выбранной пользователем.
Что умеет werf (и не умеет Docker)
Несмотря на то, что werf использует Docker для сборки и работы с образами, в сам процесс сборки привносятся новые функции и с пользователя снимается часть задач. Например, пользователю не нужно думать о тегировании образа и о том, что хранится в container registry. Вот как выглядит сравнение решений в более узком контексте:
Возможности | Docker | werf |
Сборка образов с Dockerfile | + | + |
Сборка образов со Stapel (собственный синтаксис werf) | − | + |
Параллельная сборка образов | + | + |
Распределенная сборка образов | − | + |
Отладка собираемых образов | − | + |
Тегирование образов произвольным тегом | + | − |
Автоматическое тегирование образов | − | + |
Публикация образов | + | − |
Автоматическая публикация образов | − | + |
Запуск образов | + | + |
Очистка хоста от артефактов сборки | + | + |
Автоматическая очистка хоста от артефактов сборки | − | + |
Очистка container registry | − | + |
Полный набор инструментов для управления образами и контейнерами | + | − |
Поддержка Giterminism (нашей версии GitOps) | − | + |
Как werf использует Docker
Без контейнеризации разработка приложений уже немыслима. Эту технологию виртуализации изобрели, конечно, задолго до появления Docker. Заслуга Docker в другом:
он определил стандарт контейнеризации: как нужно упаковывать приложения в контейнеры, в какой последовательности, с помощью ��аких инструментов;
Docker максимально упростил UX, то есть работу пользователя с контейнерами.
В Docker применяется клиент-серверная архитектура. Движок Docker включает клиента (ПО для непосредственной работы с Docker) и хост (сервер). Демон, запущенный на хосте, используется для управления объектами Docker, в том числе образами и контейнерами.
werf выступает в роли альтернативного Docker-клиента. Он использует Docker Engine SDK для взаимодействия с Docker-демоном по API.

Docker-демон запускается на локальном или удаленном хосте. При сборке Dockerfile никаких ограничений нет: werf может работать с Docker-демоном и локально, и удалённо по TCP-сокету.
В случае со Stapel-сборщиком для werf требуется монтирование служебных директорий с хоста в сборочные контейнеры, поэтому удалённый режим пока не поддерживается.
Детальное сравнение werf и Docker
Dockerfile и альтернативный синтаксис Stapel
В Docker поддерживается единственный формат, который уже стал стандартом для всех инструментов сборки, — Dockerfile.
werf тоже умеет использовать Dockerfile для сборки. Если у вас уже есть готовый Dockerfile для собственного проекта — это самый простой путь начать использовать werf. Чтобы образ собирался из Dockerfile, достаточно указать на это в файле конфигурации werf.yaml. Пример:
project: my-project
configVersion: 1
---
image: example
dockerfile: DockerfileСинтаксис Dockerfile при использовании werf ничем не отличается от стандартного. Пример Dockerfile для сборки простого приложения на Node.js:
FROM node:14-stretch
WORKDIR /app
RUN apt update
RUN apt install -y tzdata locales
COPY package*.json .
RUN npm ci
COPY . .
CMD ["node","/app/app.js"]Кроме Dockerfile в werf поддерживается собственный Stapel-синтаксис.
Сборочные инструкции Stapel описываются не в отдельном файле, а в werf.yaml. Вот тот же самый пример, но при использовании Stapel-cинтаксиса:
image: example
from: node:14-stretch
git:
- add: /
to: /app
stageDependencies:
setup:
- package*.json
shell:
beforeInstall:
- apt update
- apt install -y tzdata locales
setup:
- npm ci
docker:
WORKDIR: /app
CMD: "['node','/app/app.js']"Главная особенность и ценность Stapel-сборщика — это интеграция с Git и механизм работы с исходным кодом, которые значительно сокращают время инкрементальной сборки.
При добавлении файлов в Dockerfile используются директивы COPY и ADD. Пересборка происходит каждый раз при изменении добавляемых файлов. Таким образом, для эффективной сборки необходимо осознанно использовать эти директивы и комбинировать добавление определённых файлов со сборочными инструкциями. К примеру:
COPY package*.json .
RUN npm ciНо что делать, если сборочным инструкциям требуются все файлы, а пересборка должна выполняться при изменении конкретных?
В случае с Dockerfile решить такую задачу невозможно. Приходится выполнять сборочные инструкции при любых изменениях.
В Stapel-сборщике шаг добавления исходников нефиксированный. Пользователь может определить зависимости, при которых пересборка сборочных инструкций выполнится с актуальными файлами — своего рода триггер.
Основные преимущества Stapel:
Сборочные инструкции Stapel аналогичны инструкциям Dockerfile, но более гибкие и расширенные.
Если Dockerfile поддерживает только инструкцию RUN c shell-командами, то в Stapel могут использоваться как shell-команды, так и Ansible-задания.
Более удобная работа с конфигурациями сборки за счет использования YAML-формата и шаблонизации.
Сборка Stapel-образа может базироваться на другом образе, описанном в
werf.yaml: Dockerfile- или Stapel-образе.Пользователь может использовать монтирование для ускорения сборки и уменьшения размера конечного образа.
В Stapel-cинтаксисе есть директива
import, которая работает аналогично многоэтапной сборке Docker multi-stage. Директива применяется для импорта файлов из других Dockerfile- или Stapel-образов, описанных вwerf.yaml. Это еще один способ уменьшить размер конечного образа и время сборки за счет переиспользования существующих.При сборке на основе Stapel-синтаксиса работает более эффективная механика кеширования слоёв, а также доступны дополнительные полезные функции.
Вывод сборки
Вывод стандартного сборщика Docker — сухой. В логе пользователь видит сборочные инструкции и их вывод, но не знает, сколько времени ушло на выполнение, как долго выполнялась сборка в целом и каков размер промежуточных слоев. При работе с большим количеством образов и объемным выводом сборочных инструкций пользователю сложно сориентироваться: он смотрит лог и не понимает, к какой сборочной инструкции и образу этот лог относится.

Вывод становится содержательнее, если в Docker задействован сборщик Build Kit (подробнее о Build Kit см. ниже в разделе «Параллельная сборка»). Появляется дополнительная информация: время выполнения сборки в целом и отдельно по каждому шагу.
С выводом сборочных инструкций удобно работать в интерактивном режиме в процессе сборки: выводится определенное количество строк; после выполнения сборочные инструкции полностью стираются. Минус в том, что с выводом не получится работать после сборки. В большинстве CI/CD-систем терминал неинтерактивный, и вывод BuildKit теряет свои плюсы.

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

На скриншоте выше — часть вывода сборки образа dev. Собираются две стадии: beforeSetup и setup. Вывод каждой инструкции пользователя сопровождается префиксом с именем образа и стадией. Показывается время выполнения сборки образа, время выполнения всех его стадий, а также самих инструкций. После сборки каждой стадии выводится информация о том, насколько увеличился размер образа на этом шаге.
Сборка, тегирование и публикация образов
При использовании Docker нужно выполнить как минимум две команды:
Чтобы собрать образ, нужно выполнить
docker build. Чтобы протегировать при сборке —docker build --tag <tag>Чтобы опубликовать в container registry —
docker push <tag>
В werf всё это делается в рамках одной команды — werf build.
Главные отличия процесса werf build от docker build
werf buildсобирает, тегирует и публикует каждый слой по очереди. Аdocker buildтегирует и публикует только финальный образ.В
werf buildкаждая стадия, из которой состоит финальный образ, именуется явно и специальным именем (content-based tagging);docker buildявно именует лишь финальный образ. Поэтому в werf все промежуточные слои показываются вdocker images, а в Docker — нет.werf buildможет работать в двух режимах: локальном и распределенном. В локальном режиме результатwerf build— это набор специально именованных стадий собираемых образов, в распределенном — тот же набор, но еще и опубликованный в container registry.Как следствие, в werf нет понятий tag и push — эти процедуры встроены в
werf build.
Почему процесс werf build так устроен
Главная идея — упростить сборку для пользователя и сделать ее более эффективной.
В werf пользователь не оперирует именами образов. Он собирает Git-коммит проекта и заполняет container registry недостающими слоями. После сборки коммита гарантируется, что в container registry есть — то есть собраны и опубликованы — все нужные слои для образов, описанных в werf.yaml.
werf автоматически тегирует Docker-образы по содержимому образа (content-based tagging). Такая схема тегирования устойчива к пустым коммитам и к коммитам, которые не меняют файлы, задействованные Docker-образом. При перезапуске сборки на основе старого Git-коммита ветки актуальная версия образа не переписывается, и приложение не перезапускается. То есть, никаких необязательных перевыкатов и простоев.
Результирующие образы и промежуточные слои werf всегда неизменны (immutable). Это гарантирует, что ранее опубликованный слой или образ не будет перетёрт другим слоем в дальнейшем. В Docker же пользователь сам выбирает имена финальных образов, поэтому такой гарантии нет.
werf экономит время на сборку, собирая только те образы, или слои образа, которые требуются для текущего коммита и которых еще нет в container registry. Вдобавок, экономится место, поскольку нет необходимости повторно сохранять образ в реестре.
Сборочный контекст и гитерминизм
При запуске команды сборки docker build текущий рабочий каталог используется в качестве сборочного контекста, и все файлы в нём могут добавляться в собираемый образ Dockerfile-инструкциями COPY и ADD.
В случае с werf файлы сборочного контекста всегда берутся из текущего коммита репозитория проекта — так werf работает с контекстом в режиме Giterminism (от Git + determinism), более продвинутой версии GitOps. (Напомним, подход GitOps подразумевает, что текущее состояние инфраструктуры отражено в Git, а сборка и деплой приложения воспроизводимы.) Утилита форсирует версионирование всего, что связано со сборкой и деплоем приложения. werf ожидает, что вся конфигурация, которая нужна для сборки и деплоя, — в Git.
werf при сборке Dockerfile подготавливает архив со сборочным контекстом. Для Stapel-образа в сборочный контейнер монтируются архивы или патчи в зависимости от стадии сборки и пользовательской конфигурации.
werf стремится к легко воспроизводимым конфигурациям и позволяет разработчикам в Windows, macOS и Linux использовать образы, только что собранные в CI-системе. Для этого нужно просто переключиться на желаемый коммит — результат везде будет один и тот же.
Автоматическое тегирование
Пользователю werf вообще не нужно думать о тегах — он никогда с ними не сталкивается. Для работы с компонентами приложения пользователю достаточно имени образа из werf.yaml. Используя имя образа, можно выполнить команду для определённого компонента или сослаться на него в Helm-шаблонах.
werf build <IMAGE_NAME_FROM_WERF_YAML>
werf run <IMAGE_NAME_FROM_WERF_YAML>В Helm-шаблонах для пользователя доступен набор сервисных значений, которые werf проставляет при чтении. Среди этих значений имена Docker-образов. Пример:
{{ .Values.werf.image.<IMAGE_NAME_FROM_WERF_YAML> }}Запуск образов
Для запуска образов в Docker предусмотрены команды docker run и docker compose. Первая команда подходит для запуска определенного образа, вторая — для развертывания приложения целиком в Docker.
Чтобы запустить компонент приложения в werf, достаточно указать его имя из werf.yaml: werf run <IMAGE_NAME_FROM_WERF_YAML>.
Собираемые образы werf также могут использоваться в Docker Compose-конфигурациях. Для этого необходимо использовать зарезервированные переменные окружения для образов и запускать команды werf: werf compose config|down|up|run.
version: '3.8'
services:
web:
image: ${WERF_APP_DOCKER_IMAGE_NAME}
ports:
- published: 5000
target: 5000Docker Compose — мощный инструмент, который может использоваться не только для локальной разработки и тестов, но и для остальных окружений, если это укладывается в ваши ожидания и требования.
Здесь могут возникнуть логичные вопросы: что выбрать, Docker Compose или Kubernetes? Какие преимущества и недостатки у этих окружений? И почему основная ценность и фокус werf именно на Kubernetes?.. Ответы на них выходят за рамки статьи и заслуживают отдельного разбора.
Параллельная сборка
Параллельная сборка образов нужна для ускорения сборки. Независимые друг от друга образы собираются одновременно; так же одновременно выполняются разные этапы сборки.
Docker поддерживает параллельность при использовании сборщика BuildKit. Сам по себе Docker может собирать только один образ за раз в рамках одного вызова docker build. BuildKit обеспечивает параллельную сборку связанных образов (multi-stage).
В werf пользователь может явно включить BuildKit (DOCKER_BUILDKIT=1). Однако в отличие от Docker, werf параллельно собирает сразу все образы приложения. Пользователь может собирать:
произвольное количество таргетов из одного или нескольких Dockerfile;
произвольное количество Stapel-образов.
Распределённая сборка
Под распределенностью подразумевается способность нескольких сборщиков работать совместно. За счет механизмов синхронизации и блокировок они эффективно переиспользуют общие слои и не нарушают воспроизводимость всех сборок.
werf собирает слой, проверяет, не был ли этот слой собран ранее другим сборщиком, и публикует его в container registry. Если в процессе сборки стадии werf видит, что собранная стадия уже есть локально или в container registry, она не выполняет сборку повторно, а берет готовый образ. Алгоритм отчасти схож с кэшированием Docker, но более сложный и продуманный. В werf реализован механизм MVCC (multiversion concurrency control) с оптимистичными блокировками.
Отладка собираемых образов
В werf есть инструмент отладки в сборочном контейнере — интроспекция стадий. Интроспекция помогает анализировать конкретные стадии процесса сборки, выявлять возможные ошибки в инструкциях сборки или, например, находить причину неожиданного результата сборки.
Благодаря опциям интроспекции можно получить доступ к конкретной стадии непосредственно в процессе сборки. Во время интроспекции пользователь получает такое же состояние контейнера, как и во время сборки, с теми же переменными окружения, с доступом к тем же служебным инструментам, используемым werf во время сборки. По сути, интроспекция, — это запуск сборочного контейнера в интерактивном режиме для работы в нем.
Интроспекция — вид тестовой сборки: сначала вы получаете результат в сборочном контейнере, после чего оптимизируете конфигурацию сборки в werf.yaml. Это удобно в процессе разработки, когда нужно экспериментировать с некоторыми шагами сборки.
Очистка хоста
В Docker предусмотрены команды для выборочного удаления образов, контейнеров и других типов ресурсов: docker rm CONTAINER_ID [CONTAINER_ID ...], docker rmi IMAGE_ID [IMAGE_ID ...]. Также есть универсальная команда для очистки неиспользуемых данных: docker system prune.
В werf по умолчанию включена функция автоматической очистки хоста, которая выполняется в рамках основных команд (werf converge и werf build). Вычищаются временные данные, данные в кэше werf и локальные образы Docker. Алгоритм учитывает давность неактуальных файлов, а также место, которое они занимают на диске. По достижении порога werf автоматически удаляет неактуальные файлы, начиная с самого старого.
Для очистки хоста вручную используется команда werf host cleanup. Для полной очистки следов werf на хосте — команда werf host purge.
Очистка container registry
В container registry обычно лежат образы, которые используются регулярно. Однако со временем реестр переполняется неактуальными образами, которые давно не используются, но при этом занимают от деся��ков мегабайт до терабайтов. Если реестр размещен в AWS или другом платном сервисе, рост объема хранилища может стать проблемой.
У Docker, в отличие от werf, нет функции очистки container registry. Нужно вручную удалять каждый неактуальный образ, либо использовать для очистки средства самого container registry — если, конечно, эти средства имеются.
В werf предусмотрена команда werf cleanup, рассчитанная на периодический запуск по расписанию. Удаление производится в соответствии с принятыми политиками очистки; процедура безопасна. При очистке учитываются задействованные в Kubernetes образы, свежесобранные образы, а также пользовательские политики, которые определяют особенности рабочих процессов в команде и связаны с Git (подробнее — в статье на Habr и в документации).
Полная очистка container registry проводится только вручную, по команде werf purge.
Планы по оптимизации сборки в werf v1.3
Все описанное выше справедливо для актуальной версии werf — 1.2. У нас также есть планы по дальнейшим улучшениям в следующем релизе:
1. Docker не поддерживает сборку в userspace, как, например, Kaniko и Buildah. Текущая версия werf, соответственно, тоже. Поскольку эта опция становится всё более востребованной, в werf v1.3 мы планируем отвязаться от Docker-демона и добавить возможность сборки в userspace.
2. Также хотим сделать кастомные конвейеры werf-стадий:
каждую Dockerfile-инструкцию рассматривать как отдельную werf-стадию вместо единой стадии для всех инструкций;
дать пользователю полную свободу в выборе стадий для Stapel-сборки (сейчас Stapel-сборщик использует только фиксированный набор стадий).
3. Расширить возможности Dockerfile, не нарушая совместимости с Docker. В первую очередь нас интересует интеграция с Git по аналогии со Stapel-сборщиком.
4. Добавить в сборку Dockerfile-образа функции, которые уже реализованы в Stapel-сборщике:
сборка Dockerifle-образа на базе другого Dockerfile- или Stapel-образа, описанного в
werf.yaml;импорт артефактов из других Dockerfile- или Stapel-образов, описанных в
werf.yaml— для поддержки multi-stage между несколькими Dockerfile файлами и интеграции со Stapel-образами.
Подытожим
Прямое сравнение Docker и werf, как и в случае сравнения с Helm, некорректно. werf поддерживает полный цикл доставки приложений в Kubernetes. Сборка образов интегрирована в этот процесс и, насколько возможно, автоматизирована для пользователя.
Docker — стандарт сборки с широким набором инструментов для работы с образами и контейнерами. werf выстраивает сборочный процесс на его основе, привнося свои улучшения и обеспечивая интеграцию результата сборки с другими шагами CI/CD-пайплайна: запуском образов, выкатом приложения и очисткой container registry. Утилита значительно экономит время, избавляя от низкоуровневых задач.
P.S.
Читайте также в нашем блоге:
