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



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

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

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

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


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

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

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

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

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


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


Актуальный конфиг для 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:



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

Комментарии (18)


  1. Bezk
    21.08.2019 19:45
    +1

    Упустили еще одно, пожалуй, главное преимущество докерфайлов – разработчики обычно работают с теми же образами через docker-compose.
    Держать две конфигурации смысла нет.


    1. tkir Автор
      21.08.2019 20:29

      Планируем плотно развивать тему локальной разработки с использованием werf. Это возможно включает и поддержку docker-compose. Сейчас работаем над стабилизацией версии 1.0. Где-то в конце этого года можно ждать подвижек.


  1. Bezk
    21.08.2019 19:49
    +1

    Тимофей, у меня к вам еще вопрос:
    Я видел драму в issue helm'a c интеграцией поддержки kubedog в нем.
    Можно расчитывать, что вы не оставите надежду интегрировать этот функционал в Helm 3?


    1. tkir Автор
      21.08.2019 20:25

      Не оставим, но в любом случае, когда выйдет стабильный helm 3 и будет способ мигрировать старые инсталляции на новый хельм — мы перейдем на кодовую базу helm 3 и будем использовать и развивать kubedog для слежения за ресурсами в werf.


      Короче советую переходить на werf ;)


      1. Bezk
        21.08.2019 20:36

        Использование werf'a в GitLab где и так есть CI это overhead, теряется гибкость и информативность.
        Вы рассматривали возможность использования helper image в раннере вместо использования тулзы as is в одном джобе?

        В принципе ваша стратегия мне понятна – одно решение для любой системы хранения кода (или без неё вовсе), но всё-же это не очень удобно.


        1. tkir Автор
          22.08.2019 01:12
          +1

          Пока нет, не смотрели, выглядит как немного не то.


          Но интеграция с самим gitlab у нас есть и достаточно плотная:



          Например werf автоматом использует токены, которые выдает gitlab для логина в docker-registry, который также задает gitlab. Werf автоматом использует gitlab-environment если он определен.


          Так-то по возможности стараемся лишнего не делать, если это можно переложить на внешнюю CI-систему.


          Какой в gitlab есть ci конкретно для деплоя в кубы, где верф будет избыточен, можно подробнее на примере? Везде где используется kubectl apply или helm upgrade можно использовать и werf deploy — тут никаких ограничений не вносится, даже наоборот werf deploy проще использовать из-за наличия интеграции с гитлабом.


  1. asleep
    21.08.2019 21:59
    +3

    Чего хотелось бы еще от werf, по небольшому опыту применения
    * Сборка образов на машине разработчика (для тестирования и отладки инструкций) без лишних приседаний, включая работу под Win, как, собственно, можно делать с Dockerfile.
    * Параллельная сборка независимых stages. Это сделало бы werf реально мощнее того, что может сейчас предложить многоконтейнерный Dockerfile.


  1. mrigi
    21.08.2019 22:18
    -4

    В двух словах зачем эти велосипеды нужны, когда есть встроенный docker swarm?


    1. mrigi
      21.08.2019 23:49
      -2

      Что за биомусор минусует в карму на ровном месте?


    1. tkir Автор
      22.08.2019 00:38

      Werf ориентирован на kubernetes. И чтобы деплоить приложения туда. И в дальнейшем чтобы собирать образы используя runner-ы работающие в самом kubernetes.


      1. alex005
        22.08.2019 20:21

        А какие преимущества по сравнению с использованием нативных манифестов Kubernetes и kubectl в pipeline? Ведь очистить слои в docker можно и другими способами? Всегда возникает вопрос можно ли положиться на новую технологию, если это разработка одного человека, где гарантии что она будет поддерживаться и развиваться?


        1. shurup
          23.08.2019 02:23

          «Всегда возникает вопрос можно ли положиться на новую технологию, если это разработка одного человека, где гарантии что она будет поддерживаться и развиваться?» — технология не так уж нова, проекту не один год. Разрабатывается он не одним человеком, а хотя бы «одной компанией» (насколько эта формулировка корректно для Open Source-проекта, конечно же, принимающего и сторонние коммиты/контрибьюторов). Посмотрите за историей развития, например, по коммитам, чтобы понять, сколько мы в нее вкладываем — это по-настоящему огромная и решающая многое инвестиция для нас как компании, помогающей организовать другим DevOps и сопутствующее обслуживание.

          Риск, конечно, всегда есть, но это ещё и Open Source, что при достаточном сообществе + использовании стандартных для экосистемы технологий (речь и про сам язык, и про применяемые внутри технологии вроде Helm) по меньшей мере даёт куда большие шансы на жизнь в будущем, чем в иных ситуациях.

          Этот пост (и сама фича, про которую он) — хорошая иллюстрация того, как мы стремимся развивать сообщество вокруг проекта. Заходите ещё в tg-канал (werf_ru), чтобы увидеть, как активно там люди («сторонние», т.е. вне «Фланта») пользуются и им помогают, вплоть до оперативных патчей, решающих их проблемы.

          О преимуществах werf в сравнении с использованием родных средств Kubernetes (и не только) хорошо рассказано в недавнем докладе (по ссылке краткий его обзор и там же полное видео). Там как раз о проблемах, которые в принципе возникают при построении Continuous Delivery на базе K8s, и как они решены конкретно в werf.


          1. alex005
            24.08.2019 01:41

            Спасибо за содержательный ответ. Нисколько не критикую, а наоборот поддерживаю вашу разработку, пока морально. Просто иногда для начинающих не понятны мотивы использовать сторонних средств для деплоя. Например, мне показалось, что вы делаете альтернативный буилдер, который решает ваши конкретные сложности в инфраструктуре.
            В большинстве случаев сто-двести микросервисов, которые работают с БД легко разруливаются одним человеком, который в теме разработки.


  1. VolCh
    21.08.2019 23:36
    +1

    target из Докер файла можно указать?


    Экспериментальный синтаксис с buildkit поддерживается? Все эти RUN --mount=…


    И только для сборки можно использовать? А то сегодня полдня писал build.sh — грустно как то…


    1. tkir Автор
      22.08.2019 00:49

      Target можно:


      configVersion: 1
      project: myproj
      ---
      image: backend
      dockerfile: ./Dockerfile
      target: backend
      ---
      image: frontend
      dockerfile: ./Dockerfile
      target: frontend

      Поддерживается любой синтаксис стандартного Dockerfile, т.к. для билда используется docker server (который может использовать buildkit).


      После того как образы описаны в werf.yaml, собраны и опубликованы (через werf build-and-publish), их можно использовать для деплоя в кубы. Для этого надо описать chart. В описанном чарте можно ссылаться на имена образов из werf.yaml, в нашем случае например так:


      apiVersion: apps/v1beta1
      kind: Deployment
      metadata:
        name: backend
      spec:
        template:
          metadata:
            labels:
              service: backend
          spec:
            containers:
            - name: main
              command: [ ... ]
      {{ tuple "backend" . | include "werf_container_image" | indent 8 }}
              env:
      {{ tuple "backend" . | include "werf_container_env" | indent 8 }}


      1. VolCh
        22.08.2019 09:03

        Имелось в виду, можно ли использовать исключительно для сборки, возможностями деплоя не пользуясь. Только закончил описывать собственно деплой на helm 3, перешёл к написанию build.sh "универсального", намучался с "конфигом" на баш массивах, а тут ваш пост. :)


        1. tkir Автор
          22.08.2019 10:02
          +1

          Понял, так тоже можно. Использовать только werf build-and-publish и werf cleanup. Вот по этому гайду https://werf.io/documentation/guides/gitlab_ci_cd_integration.html#pipeline просто пропустить стадию деплоя.


  1. gecube
    22.08.2019 17:32

    Зачем вообще упоминать команду ADD? Мое мнение, что ее нужно выжигать калёным железом в пользу COPY. Докеровцы реально "молодцы", что сделали такой комбайн как ADD — который и ссылки скачает, и архивы распакует, что делает прогноз того, что произойдет весьма нетривиальным. Поэтому мой выбор — вполне ясная директива COPY для копирования файлов внутрь образа.