Собирать Docker-образы в werf теперь можно и по обычному Dockerfile

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

onutju4gqnldsccyq0bbeywfnng.png

Речь пойдёт про werf — GitOps-утилиту, которая интегрируется с любой CI/CD-системой и обеспечивает управление всем жизненным циклом приложения, позволяя:

  • собирать и публиковать образы,
  • разворачивать приложения в Kubernetes,
  • удалять неиспользуемые образы с помощью специальных политик.


Философия проекта — собрать низкоуровневые инструменты в единую унифицированную систему, дающую DevOps-инженерам контроль над приложениями. По возможности должны быть задействованы уже существующие утилиты (вроде Helm и Docker). Если же решения какой-то задачи нет — мы можем создать и поддерживать всё необходимое для этого.

Предыстория: свой сборщик образов


Так и случилось со сборщиком образов в werf: привычного Dockerfile нам не хватало. Если бегло окунуться в историю проекта, то эта проблема проявилась уже в первых версиях werf (тогда еще известного как dapp).

Создавая инструмент для сборки приложений в Docker-образы, мы быстро поняли, что Dockerfile нам не подходит для некоторых вполне конкретных задач:

  1. Необходимость собирать типичные небольшие веб-приложения по следующей стандартной схеме:
    • установить общесистемные зависимости приложения,
    • установить bundle библиотек зависимостей приложения,
    • собрать ассеты,
    • и самое важное — обновлять код в образе быстро и эффективно.
  2. При изменениях в файлах проекта сборщик должен быстро создавать новый слой путем наложения патча на измененные файлы.
  3. Если поменялись определенные файлы, то необходимо пересобирать соответствующую зависимую стадию.


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

В общем, недолго думая, мы вооружились используемым языком программирования (см. ниже) и отправились в путь — реализовывать собственный DSL! Соответствуя поставленным задачам, он был предназначен для описания процесса сборки по стадиям и определения зависимостей этих стадий от файлов. А дополнял его собственный сборщик, который превращал DSL в конечную цель — собранный образ. Сначала DSL был на Ruby, а по мере перехода на Golang — конфиг нашего сборщика стал описываться в YAML-файле.

z5jisrzzlmuxu5r3gct98dovmxq.png
Старый конфиг для dapp на Ruby

wch1jjegx2n6zxtxh3-ky9ouedu.png
Актуальный конфиг для werf на YAML

Механизм работы сборщика тоже менялся со временем. Сначала мы просто генерировали на лету некий временный Dockerfile из нашей конфигурации, а потом стали запускать сборочные инструкции во временных контейнерах и делать commit.

NB: На данный момент наш сборщик, который работает со своим конфигом (в YAML) и называется Stapel-сборщиком, уже развился в достаточно мощный инструмент. Его развернутое описание заслуживает отдельных статей, а основные подробности можно узнать из документации.

Осознание проблемы


Но мы поняли, причем не сразу, что совершили одну ошибку: не добавили возможность собирать образы через стандартный Dockerfile и интегрировать их в ту же инфраструктуру комплексного управления приложением (т.е. собирать образы, деплоить и чистить их). Как можно было сделать инструмент для деплоя в Kubernetes и не реализовать поддержку Dockerfile, т.е. стандартного способа описания образов для большинства проектов?…

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

NB: К слову, с чего бы вам вообще захотеть использовать werf? Основные фичи сводятся к следующим:

  • полный цикл управления приложением включая очистку образов;
  • возможность управлять сборкой сразу нескольких образов из единого конфига;
  • улучшенный процесс деплоя совместимых с Helm чартов.


С более полным их списком можно ознакомиться на странице проекта.

Итак, если раньше мы бы предложили переписать Dockerfile на наш конфиг, то теперь с радостью скажем: «Позвольте werf собрать ваши Dockerfile«ы!»

Как использовать?


Полная реализация этой возможности появилась в релизе werf v1.0.3-beta.1. Общий принцип прост: пользователь указывает путь до существующего Dockerfile в конфиге werf, после чего запускает команду werf build… и всё — werf соберёт образ. Рассмотрим на абстрактном примере.

Объявим следующий Dockerfile в корне проекта:

FROM ubuntu:18.04
RUN echo Building ...


И объявим werf.yaml, который использует этот Dockerfile:

configVersion: 1
project: dockerfile-example
---
image: ~
dockerfile: ./Dockerfile


Всё! Осталось запустить werf build:

ukjm7j_4aryw5zgbugqusuxvdue.png

Кроме того, можно объявить следующий werf.yaml для сборки сразу нескольких образов из разных Dockerfile«ов:

configVersion: 1
project: dockerfile-example
---
image: backend
dockerfile: ./dockerfiles/Dockerfile-backend
---
image: frontend
dockerfile: ./dockerfiles/Dockerfile-frontend


Наконец, поддерживается и передача дополнительных параметров сборки — таких как --build-arg и --add-host — через конфиг werf. Полное описание конфигурации Dockerfile image доступно на странице документации.

Как это работает?


В процессе сборки функционирует стандартный кэш локальных слоёв в Docker. Однако, что важно, werf также интегрирует конфигурацию Dockerfile в свою инфраструктуру. Что это означает?

  1. Каждый образ, собранный из Dockerfile, состоит из одного stage под названием dockerfile (подробнее про то, что такое stages в werf, можно почитать здесь).
  2. Для stage«а dockerfile werf рассчитывает сигнатуру, которая зависит от содержимого конфигурации Dockerfile. При изменении конфигурации Dockerfile происходит смена сигнатуры стадии dockerfile и werf инициирует пересборку этой стадии с новым конфигом Dockerfile. Если же сигнатура не меняется, то werf берет образ из кэша (подробнее об использовании сигнатур в werf рассказывалось в этом докладе).
  3. Далее собранные образы можно опубликовать командой werf publish (или werf build-and-publish) и использовать для деплоя в Kubernetes. Опубликованные образы в Docker Registry будут чиститься стандартными средствами очистки werf, т.е. произойдет автоматическая очистка старых образов (старше N дней), образов, связанных с несуществующими Git-ветками, и по другим политикам.


Подробнее об описанных здесь моментах можно узнать из документации:

Примечания и предосторожности


1. Внешний URL в ADD не поддерживается


На данный момент не поддерживается использование внешнего URL в директиве ADD. Werf не будет инициировать пересборку при изменении ресурса по указанному URL. В скором времени планируется добавление данной возможности.

2. Нельзя добавлять .git в образ


Вообще говоря, добавление директории .git в образ — порочная плохая практика и вот почему:

  1. Если .git остается в финальном образе, это нарушает принципы 12 factor app: поскольку итоговый образ должен быть связан с одним коммитом, не должно быть возможности сделать git checkout произвольного коммита.
  2. .git увеличивает размер образа (репозиторий может быть большим из-за того, что в него когда-то добавили большие файлы, а потом удалили). Размер же work-tree, связанного только с определенным коммитом, не будет зависеть от истории операций в Git. При этом добавление и последующее удаление .git из финального образа не сработает: образ все равно приобретет лишний слой — так работает Docker.
  3. Docker может инициировать лишнюю пересборку, даже если идет сборка одного и того же коммита, но из разных work-tree. Например, GitLab создает отдельные склонированные директории в /home/gitlab-runner/builds/HASH/[0-N]/yourproject при включенной параллельной сборке. Лишняя пересборка будет связана с тем, что директория .git отличается в разных склонированных версиях одного и того же репозитория, даже если собирается один и тот же коммит.


Последний пункт имеет последствие и при использовании werf. Werf требует, чтобы собранный кэш присутствовал при запуске некоторых команд (например, werf deploy). Во время работы таких команд werf рассчитывает сигнатуры стадий для образов, указанных в werf.yaml, и они должны быть в сборочном кэше — иначе команда не сможет продолжить работу. Если же сигнатура стадий будет зависеть от содержимого .git, то мы получаем неустойчивый к изменениям в нерелевантных файлах кэш, и werf не сможет простить такую оплошность (подробнее см. в документации).

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

Итог


Наш изначальный путь с написанием своего сборщика для определенных потребностей был тяжелым, честным и прямолинейным: вместо использования костылей поверх стандартного Dockerfile мы написали своё решение с кастомным синтаксисом. И это дало свои плюсы: Stapel-сборщик отлично справляется со своей задачей.

Однако в процессе написания собственного сборщика мы упустили из фокуса кейс использования уже существующих Dockerfile«ов. Сейчас этот недостаток исправлен, а в дальнейшем мы планируем развивать поддержку Dockerfile наряду с нашим кастомным сборщиком Stapel для распределенной сборки и для сборки с использованием Kubernetes (т.е. сборки на runner«ах внутри Kubernetes, как это сделано в kaniko).

Так что, если у вас вдруг завалялось пара Dockerfile«ов… попробуйте werf!

P.S. Список документации по теме


Читайте также в нашем блоге: «werf — наш инструмент для CI/CD в Kubernetes (обзор и видео доклада)».

© Habrahabr.ru