Продолжаем серию публикаций «werf vs...», которая вдохновлена часто задаваемыми вопросами. В первой статье мы объяснили, чем werf отличается от Helm. Теперь черед сравнения с еще более базовой утилитой — Docker. Нас нередко спрашивают: зачем собирать образы с werf, если уже есть Docker с Dockerfile? Обычно мы отвечаем, что werf — не только про сборку. Утилита участвует в полном цикле CI/CD для доставки приложения в Kubernetes, а Docker при этом тоже используется, но как вспомогательный инструмент. Понятно, что такого объяснения недостаточно, нужны подробности. Этот материал главным образом для тех, кто мало или совсем не знаком с werf, но знает Docker и хотя бы немного работал с ним. Для начала, как и в случае с Helm, попробуем разобраться, есть ли смысл в противопоставлении двух решений. По традиции, начнем с общего плана — сравнения 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 для сборки и работы с образами, в сам процесс сборки привносятся новые функции и с пользователя снимается часть задач. Например, пользователю не нужно думать о тегировании образа и о том, что хранится в container registry. Вот как выглядит сравнение решений в более узком контексте: Возможности Docker werf Сборка образов с Dockerfile + + Сборка образов со Stapel (собственный синтаксис werf) − + Параллельная сборка образов + + Распределенная сборка образов − + Отладка собираемых образов − + Тегирование образов произвольным тегом + − Автоматическое тегирование образов − + Публикация образов + − Автоматическая публикация образов −  + Запуск образов + + Очистка хоста от артефактов сборки + + Автоматическая очистка хоста от артефактов сборки − + Очистка container registry − + Полный набор инструментов для управления образами и контейнерами + − Поддержка Giterminism (нашей версии GitOps) − + Без контейнеризации разработка приложений уже немыслима. Эту технологию виртуализации изобрели, конечно, задолго до появления Docker. Заслуга Docker в другом: он определил стандарт контейнеризации: как нужно упаковывать приложения в контейнеры, в какой последовательности, с помощью каких инструментов; Docker максимально упростил UX, то есть работу пользователя с контейнерами. В Docker применяется клиент-серверная архитектура. Движок Docker включает клиента (ПО для непосредственной работы с Docker) и хост (сервер). Демон, запущенный на хосте, используется для управления объектами Docker, в том числе образами и контейнерами. werf выступает в роли альтернативного Docker-клиента. Он использует Docker Engine SDK для взаимодействия с Docker-демоном по API. Docker-демон запускается на локальном или удаленном хосте. При сборке Dockerfile никаких ограничений нет: werf может работать с Docker-демоном и локально, и удалённо по TCP-сокету. В случае со Stapel-сборщиком для werf требуется монтирование служебных директорий с хоста в сборочные контейнеры, поэтому удалённый режим пока не поддерживается. В Docker поддерживается единственный формат, который уже стал стандартом для всех инструментов сборки, — Dockerfile.  werf тоже умеет использовать Dockerfile для сборки. Если у вас уже есть готовый Dockerfile для собственного проекта — это самый простой путь начать использовать werf. Чтобы образ собирался из Dockerfile, достаточно указать на это в файле конфигурации werf.yaml. Пример: Синтаксис Dockerfile при использовании werf ничем не отличается от стандартного. Пример Dockerfile для сборки простого приложения на Node.js: Кроме Dockerfile в werf поддерживается собственный Stapel-синтаксис.  Сборочные инструкции Stapel описываются не в отдельном файле, а в werf.yaml. Вот тот же самый пример, но при использовании Stapel-cинтаксиса: Главная особенность и ценность Stapel-сборщика — это интеграция с Git и механизм работы с исходным кодом, которые значительно сокращают время инкрементальной сборки. При добавлении файлов в Dockerfile используются директивы COPY и ADD. Пересборка происходит каждый раз при изменении добавляемых файлов. Таким образом, для эффективной сборки необходимо осознанно использовать эти директивы и комбинировать добавление определённых файлов со сборочными инструкциями. К примеру: Но что делать, если сборочным инструкциям требуются все файлы, а пересборка должна выполняться при изменении конкретных? В случае с 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 каждая стадия, из которой состоит финальный образ, именуется явно и специальным именем (content-based tagging); docker build явно именует лишь финальный образ. Поэтому в werf все промежуточные слои показываются в docker images, а в Docker — нет. werf build может работать в двух режимах: локальном и распределенном. В локальном режиме результат werf build — это набор специально именованных стадий собираемых образов, в распределенном — тот же набор, но еще и опубликованный в container registry. Как следствие, в werf нет понятий tag и push — эти процедуры встроены в 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-шаблонах. В Helm-шаблонах для пользователя доступен набор сервисных значений, которые werf проставляет при чтении. Среди этих значений имена Docker-образов. Пример: Для запуска образов в 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. Docker 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 обычно лежат образы, которые используются регулярно. Однако со временем реестр переполняется неактуальными образами, которые давно не используются, но при этом занимают от десятков мегабайт до терабайтов. Если реестр размещен в AWS или другом платном сервисе, рост объема хранилища может стать проблемой. У Docker, в отличие от werf, нет функции очистки container registry. Нужно вручную удалять каждый неактуальный образ, либо использовать для очистки средства самого container registry — если, конечно, эти средства имеются. В werf предусмотрена команда werf cleanup, рассчитанная на периодический запуск по расписанию. Удаление производится в соответствии с принятыми политиками очистки; процедура безопасна. При очистке учитываются задействованные в Kubernetes образы, свежесобранные образы, а также пользовательские политики, которые определяют особенности рабочих процессов в команде и связаны с Git (подробнее — в статье на Habr и в документации). Полная очистка container registry проводится только вручную, по команде werf purge. Все описанное выше справедливо для актуальной версии 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. Утилита значительно экономит время, избавляя от низкоуровневых задач. Читайте также в нашем блоге: «werf vs. Helm: корректно ли их вообще сравнивать?» Инженер-программист