Продолжаем серию публикаций «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
Вывод Docker

Вывод становится содержательнее, если в Docker задействован сборщик Build Kit (подробнее о Build Kit см. ниже в разделе «Параллельная сборка»). Появляется дополнительная информация: время выполнения сборки в целом и отдельно по каждому шагу. 

С выводом сборочных инструкций удобно работать в интерактивном режиме в процессе сборки: выводится определенное количество строк; после выполнения сборочные инструкции полностью стираются. Минус в том, что с выводом не получится работать после сборки. В большинстве CI/CD-систем терминал неинтерактивный, и вывод BuildKit теряет свои плюсы.

Вывод Docker + BuildKit
Вывод Docker + BuildKit

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

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

Вывод werf
Вывод werf

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

Сборка, тегирование и публикация образов

При использовании Docker нужно выполнить как минимум две команды:

  1. Чтобы собрать образ, нужно выполнить docker build. Чтобы протегировать при сборке — docker build --tag <tag>

  2. Чтобы опубликовать в 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: 5000

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

В 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.

Читайте также в нашем блоге: