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

в 9:55, , рубрики: continuous delivery, devops, docker, dockerfile, werf, Блог компании Флант, системное администрирование, системы сборки

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

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

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

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


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

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

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

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

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

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

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

Собирать Docker-образы в werf теперь можно и по обычному Dockerfile - 2
Старый конфиг для dapp на Ruby

Собирать Docker-образы в werf теперь можно и по обычному Dockerfile - 3
Актуальный конфиг для 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:

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

Кроме того, можно объявить следующий 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 (обзор и видео доклада)».

Автор: Timofey Kirillov

Источник


* - обязательные к заполнению поля