Обычно мы в werf стремимся получать обратную связь от инженеров, которые могут принести кейс использования, и высокоуровневый сценарий для которого нужна некоторая фича. Мы ожидаем что инженеры приносят свои кейсы, без обратной связи по разным каналам, было бы невозможно сделать инструмент лучше и универсальнее.
Возможно в вашем кейсе такая обратная связь была и мы её упустили где-нибудь в телеграме или issue — в любом случае стоит продублировать и мы обязательно ответим предметно.
В целом по подходу werf: там где другие инструменты предоставляют комбайны и множество вариантов сделать одно и то же, в нашем случае предоставляется ровно один проверенный способ. Иногда возникает запрос на поддержку какого-то высокоуровневого сценария использования, который сейчас никак не покрывается — это нормальная ситуация, в этом случае функционал допиливается по запросу.
Почему две сборки подряд из одного коммита будут не идентичными?
Например потому что мы никак не контролируем что происходит в сборочных инструкциях типа:
- apt-get update
- apt-get install some-solution
— и версия some-solution может быть разной.
И далее мы и не пытаемся контролировать эти инструкции, потому что это слишком глобальная и в реальности невыполнимая задача. Вместо этого мы идём с другой стороны: даём гарантию иммутабельности образов, собранных из коммита, гарантию атомарности при публикации этих образов, гарантию того, что из одного коммита будет выводится одно и то же имя опубликованного образа.
Именно таким путём идёт werf со своей распределённой сборкой и специальным тегированием.
docker принципиально работает по другому алгоритму. Сначала он попытается спуллить все стадии из container registry. Затем соберёт все стадии локально. Затем запушит результирующие стадии и результирующий образ.
werf делает этот процесс _по одной стадии_ (собрали стадию, опубликовали стадию) и связывает кеш с коммитами, что позволяет сразу после сборки переиспользовать общий кеш.
Про вторую историю. Совсем не обязательно что зависимости строго определены. Т.к. кеш иммутабельный и не удаляется из container registry, то те версии зависимостей которые были когда-то собраны для коммита остаются закешированы навсегда. Обеспечивается воспроизводимость однажды собранного кеша для коммита. Если же зависимости необходимо апдейтнуть, то мы создаём коммит, в котором инициируем сброс соотв. стадии (для этого мы либо меняет в гите файл, на изменения которого завязана сборка стадии, либо меняем в werf.yaml спец. поле "версия кеша").
Каждая стадия и конечный образ имеют content-based tag. Он состоит из двух частей: content checksum и timestamp. Два сборщика начинают собирают стадию с одним и тем же content checksum на разных билдерах. Какой-то сборщик завершает сборку быстрее, чем первый. Этот сборщик сразу после сборки стадии пытается опубликовать стадию в container registry. Для этого он проверяет, что стадия ещё не опубликована по content checksum, если уже опубликована, то сборщик выбрасывает свой собранный стейж и использует далее то что нашёл в container registry. Если стадия не опубликована, то werf берёт короткоживущий distributed lock по content checksum и опять для корректности проверяет, что стадия не появилась в container registry. Если стадии нет, то мы генерируем полный content-based-tag подставляя content checksum и текущий timestamp и публикуем стадию. После отпускаем distributed lock и стадия становится моментально доступна для всех сборщиков.
Далее там есть моменты, связанные с тем, что werf обеспечивает изоляцию сборочного кеша на основе git-истории. А это значит, что по одному и тому же content checksum в разных ветках может быть опубликована своя стадия (во-первых будет разный timestamp, во-вторых, даже если произойдёт коллизия timestamp, то werf заметит уже существующий тег и просто сгенерит timestamp ещё раз). Алгоритм подбора существующих стадий обеспечит изоляцию кеша до тех пор, пока ветка не будет смержена в текущую ветку. Как только такой мерж происходит, то и собранный кеш для смерженных коммитов начинает быть доступен в основной ветке (по факту среди нескольких образов с одним и тем же content checksum будет выбран тот, который собран раньше по timestamp).
Это всё можно назвать mvcc с оптимистичными блокировками, это реализует werf под капотом с использованием сервера синхронизации.
Docker не обеспечит иммутабельность публикуемых образов. werf обеспечивает гарантию, что если какой-то образ или отдельный stage связанный с коммитом однажды опубликован в regsitry, то он не будет перетёрт другим образом по тому же тегу. Это нужно для обеспечения воспроизводимости образов по коммиту.
Клиент, который взял блокировку, обязан её периодически продлять. Другой клиент, который ждёт блокировки может заметить, что она уже давно не продлевалась (есть некоторый небольшой timeout) и перехватить её. Если первый клиент не смог продлить блокировку из-за сетевого глюка, то он в какой-то момент придёт продлевать блокировку, которую уже перехватили — он это заметит и включит "особый обработчик" данной ситуации, который в случае werf как можно быстрее crash-ит текущий процесс.
Такой подход в lockgate не даёт 100% гарантию корректности для любого проекта, однако предоставляет возможность определить в своём проекте этот обработчик ситуации с перехватом блокировки. В случае werf аварийного завершения процесса в этой ситуации пока хватает, да и на практике кейс возникает очень редко.
Проблему запуска деплоя новой версии, когда не дождались окончания деплоя старой версии мы в werf решаем с помощью блокировки релиза по имени. В один момент времени выкатывается только один релиз с определённым именем, другой выкат будет дожидаться предыдущего (реализовано это с помощью распределённых блокировок через Kubernetes, деплой может быть запущен с любого хоста).
Небольшое дополнение к статье. В werf сейчас реализован принципиально новый подход к сборке и публикации образов в docker registry, отличный от того, что используется при docker build:
Werf делает push каждого собранного слоя (стадии) в registry сразу после успешной сборки, что позволяет сразу переиспользовать его в других процессах сборки на произвольных машинах. Это возможно благодаря новому алгоритму публикации и выборки стадий с оптимистичными блокировками.
На момент публикации финального образа по определённому имени все необходимые слои, из которых состоит этот образ, уже есть в registry. Werf по факту делает push лишь для небольшого слоя с некоторой мета-информацией и ставит на этот слой тег. Таким образом этот push происходит моментально, т.к. registry переиспользует существующие слои.
К тому же werf по умолчанию предлагает content-based-тегирование, что позволяет ещё лучше переиспользовать уже существующие слои и избавится от лишних ребилдов и редеплоев приложения.
Так-то оно так, но ни kaniko, ни buildah не умеют собирать по-настоящему распределённый кеш. Чтобы уже собранные слои были доступны во всех сборках.
В werf теперь возможно такое: werf собирает коммит номер 1 на одном билдере. Пользователь делает новый коммит в git, werf запускается на другом билдере, накладывает патч на уже существующий кеш из предыдущей сборки на другом раннере и публикует новый образ.
Это похоже на то, что предоставляет --cache-from в docker builder, но быстрее и эффективнее и с поддержкой ansible и инкрементальных пересборок по истории git и т.д.
Можно сказать по-другому: мы выстраиваем content-based файловую систему на основе docker-образов, новое состояние в этой фс создается на основе кеша stages путем сборки новых образов, идентификаторы и пересборка тесно связаны с историей правок в git.
Не так. При мерже собранный кеш становится доступен для всех остальных точно также как и изменения которые были сделаны в ветке становятся доступны всем только после merge. Если мерж был не fast-forward, то возможно произойдет пересборка связанная со слиянием изменений мастера и ветки, но опять же уже собранный кеш будет учавствовать в сборке.
Если для ветки сделать rebase, то кеш, собранный для старого коммита потеряется и более не будет использоваться, также как теряется родительская связь между коммитами при rebase — произойдет пересборка. При дальнейшем merge кеш не теряется.
Такая изоляция — это отчасти переложение логики гита на собираемые образы.
Как вариант — запоминать версию кода на этапе сборки образа. Но опять же commit-id — это неидеальный идентификатор кода, потому что его изменение не означает что код в образе изменился. При этом мог быть сделан какой-то нерелевантный коммит и сборщик не стал лишний раз пересобирать образ без надобности — в этом случае образ остался связан со старым коммитом.
Другой вариант — не встраивать информацию о коммите в образы вообще. Зачем добавлять в образ изменчивую инфу, тем более, что commit-id не отражает его содержимое. А добавлять инфу о коммите куда-то в рантайм — Kubernetes — в процессе выката приложения. При выкате werf, например, через values может передать commit-id в шаблоны. Дальше его можно положить в какой-нибудь ConfigMap, приложение же может доставать commit-id в рантайме — читать его из ConfigMap. Тут получается и овцы сыты, и волки целы: лишних пересборок нету, приложение может получить инфу о гите.
Итоговый образ точно также как и кеш стадий будет изолирован за счет создания разных тегов. Вот в этом и заключается влияние истории гита на тег.
Другими словами: вот есть у нас 2 образа одинаковых по контенту, но собранных для разных гит-веток. Верф изолирует эти 2 образа: создаст для каждого свой идентификатор.
При этом в рамках одной ветки образы с общим контентом будут иметь одинаковый идентификатор.
Добрый день!
Обычно мы в werf стремимся получать обратную связь от инженеров, которые могут принести кейс использования, и высокоуровневый сценарий для которого нужна некоторая фича. Мы ожидаем что инженеры приносят свои кейсы, без обратной связи по разным каналам, было бы невозможно сделать инструмент лучше и универсальнее.
Возможно в вашем кейсе такая обратная связь была и мы её упустили где-нибудь в телеграме или issue — в любом случае стоит продублировать и мы обязательно ответим предметно.
В целом по подходу werf: там где другие инструменты предоставляют комбайны и множество вариантов сделать одно и то же, в нашем случае предоставляется ровно один проверенный способ. Иногда возникает запрос на поддержку какого-то высокоуровневого сценария использования, который сейчас никак не покрывается — это нормальная ситуация, в этом случае функционал допиливается по запросу.
Например потому что мы никак не контролируем что происходит в сборочных инструкциях типа:
— и версия some-solution может быть разной.
И далее мы и не пытаемся контролировать эти инструкции, потому что это слишком глобальная и в реальности невыполнимая задача. Вместо этого мы идём с другой стороны: даём гарантию иммутабельности образов, собранных из коммита, гарантию атомарности при публикации этих образов, гарантию того, что из одного коммита будет выводится одно и то же имя опубликованного образа.
Именно таким путём идёт werf со своей распределённой сборкой и специальным тегированием.
GitOps-тулзы не покрывают весь CI/CD, они обычно отвечают только за доставку релиза в контур кубернетес определённым способом.
Если есть желание/потребность построить полный флоу CI/CD с использованием GitOps на основе pull-модели, то есть вариант совместной работы werf+argocd как на следующей схеме: https://werf.io/documentation/v1.2/advanced/ci_cd/werf_with_argocd/ci_cd_flow_overview.html#reference-cicd-flow.
docker принципиально работает по другому алгоритму. Сначала он попытается спуллить все стадии из container registry. Затем соберёт все стадии локально. Затем запушит результирующие стадии и результирующий образ.
werf делает этот процесс _по одной стадии_ (собрали стадию, опубликовали стадию) и связывает кеш с коммитами, что позволяет сразу после сборки переиспользовать общий кеш.
Про вторую историю. Совсем не обязательно что зависимости строго определены. Т.к. кеш иммутабельный и не удаляется из container registry, то те версии зависимостей которые были когда-то собраны для коммита остаются закешированы навсегда. Обеспечивается воспроизводимость однажды собранного кеша для коммита. Если же зависимости необходимо апдейтнуть, то мы создаём коммит, в котором инициируем сброс соотв. стадии (для этого мы либо меняет в гите файл, на изменения которого завязана сборка стадии, либо меняем в werf.yaml спец. поле "версия кеша").
Попробую рассказать на примере.
Каждая стадия и конечный образ имеют content-based tag. Он состоит из двух частей: content checksum и timestamp. Два сборщика начинают собирают стадию с одним и тем же content checksum на разных билдерах. Какой-то сборщик завершает сборку быстрее, чем первый. Этот сборщик сразу после сборки стадии пытается опубликовать стадию в container registry. Для этого он проверяет, что стадия ещё не опубликована по content checksum, если уже опубликована, то сборщик выбрасывает свой собранный стейж и использует далее то что нашёл в container registry. Если стадия не опубликована, то werf берёт короткоживущий distributed lock по content checksum и опять для корректности проверяет, что стадия не появилась в container registry. Если стадии нет, то мы генерируем полный content-based-tag подставляя content checksum и текущий timestamp и публикуем стадию. После отпускаем distributed lock и стадия становится моментально доступна для всех сборщиков.
Далее там есть моменты, связанные с тем, что werf обеспечивает изоляцию сборочного кеша на основе git-истории. А это значит, что по одному и тому же content checksum в разных ветках может быть опубликована своя стадия (во-первых будет разный timestamp, во-вторых, даже если произойдёт коллизия timestamp, то werf заметит уже существующий тег и просто сгенерит timestamp ещё раз). Алгоритм подбора существующих стадий обеспечит изоляцию кеша до тех пор, пока ветка не будет смержена в текущую ветку. Как только такой мерж происходит, то и собранный кеш для смерженных коммитов начинает быть доступен в основной ветке (по факту среди нескольких образов с одним и тем же content checksum будет выбран тот, который собран раньше по timestamp).
Это всё можно назвать mvcc с оптимистичными блокировками, это реализует werf под капотом с использованием сервера синхронизации.
Docker не обеспечит иммутабельность публикуемых образов. werf обеспечивает гарантию, что если какой-то образ или отдельный stage связанный с коммитом однажды опубликован в regsitry, то он не будет перетёрт другим образом по тому же тегу. Это нужно для обеспечения воспроизводимости образов по коммиту.
Werf поддерживает данное разделение публикации артефактов и релиза с помощью т.н. бандлов: https://werf.io/documentation/v1.2/advanced/bundles.html
werf bundle publish
готовит и публикует в container registry бандлы — артефакты, состоящие из собранных образов и инструкций для их развертывания;werf bundle apply
выкатывает бандл из container registry (доступ к Git-репозиторию уже не требуется, только к container registry).Клиент, который взял блокировку, обязан её периодически продлять. Другой клиент, который ждёт блокировки может заметить, что она уже давно не продлевалась (есть некоторый небольшой timeout) и перехватить её. Если первый клиент не смог продлить блокировку из-за сетевого глюка, то он в какой-то момент придёт продлевать блокировку, которую уже перехватили — он это заметит и включит "особый обработчик" данной ситуации, который в случае werf как можно быстрее crash-ит текущий процесс.
Сами блокировки оформлены в виде отдельного проекта: https://github.com/werf/lockgate.
Такой подход в lockgate не даёт 100% гарантию корректности для любого проекта, однако предоставляет возможность определить в своём проекте этот обработчик ситуации с перехватом блокировки. В случае werf аварийного завершения процесса в этой ситуации пока хватает, да и на практике кейс возникает очень редко.
Проблему запуска деплоя новой версии, когда не дождались окончания деплоя старой версии мы в werf решаем с помощью блокировки релиза по имени. В один момент времени выкатывается только один релиз с определённым именем, другой выкат будет дожидаться предыдущего (реализовано это с помощью распределённых блокировок через Kubernetes, деплой может быть запущен с любого хоста).
В начале видео, где идёт уточнение терминологии, как раз говорится про различие между интуитивным обобщённым пониманием GitOps и конкретной имплементацией с pull-моделью и обязательным промежуточным репом. Данная имплементация использует докер-образы из registry и не отвечает за их корректную сборку и тегирование. Именно о такой имплементации идёт речь в видео. Вот ссылки на почитать, чтобы понять о чём идёт речь: https://blog.argoproj.io/introducing-argo-cd-declarative-continuous-delivery-for-kubernetes-da2a73a780cd, https://www.weave.works/technologies/gitops/.
Небольшое дополнение к статье. В werf сейчас реализован принципиально новый подход к сборке и публикации образов в docker registry, отличный от того, что используется при docker build:
К тому же werf по умолчанию предлагает content-based-тегирование, что позволяет ещё лучше переиспользовать уже существующие слои и избавится от лишних ребилдов и редеплоев приложения.
Так-то оно так, но ни kaniko, ни buildah не умеют собирать по-настоящему распределённый кеш. Чтобы уже собранные слои были доступны во всех сборках.
В werf теперь возможно такое: werf собирает коммит номер 1 на одном билдере. Пользователь делает новый коммит в git, werf запускается на другом билдере, накладывает патч на уже существующий кеш из предыдущей сборки на другом раннере и публикует новый образ.
Это похоже на то, что предоставляет
--cache-from
в docker builder, но быстрее и эффективнее и с поддержкой ansible и инкрементальных пересборок по истории git и т.д.https://habr.com/ru/company/flant/blog/504390/
Промахнулся.
Да, сборщик werf требует доступа к локальному docker-server на хостах, где запускается.
Можно сказать по-другому: мы выстраиваем content-based файловую систему на основе docker-образов, новое состояние в этой фс создается на основе кеша stages путем сборки новых образов, идентификаторы и пересборка тесно связаны с историей правок в git.
Не так. При мерже собранный кеш становится доступен для всех остальных точно также как и изменения которые были сделаны в ветке становятся доступны всем только после merge. Если мерж был не fast-forward, то возможно произойдет пересборка связанная со слиянием изменений мастера и ветки, но опять же уже собранный кеш будет учавствовать в сборке.
Если для ветки сделать rebase, то кеш, собранный для старого коммита потеряется и более не будет использоваться, также как теряется родительская связь между коммитами при rebase — произойдет пересборка. При дальнейшем merge кеш не теряется.
Такая изоляция — это отчасти переложение логики гита на собираемые образы.
Спасибо! Ответил тут: https://habr.com/ru/company/flant/blog/495112/#comment_21457234
Как вариант — запоминать версию кода на этапе сборки образа. Но опять же commit-id — это неидеальный идентификатор кода, потому что его изменение не означает что код в образе изменился. При этом мог быть сделан какой-то нерелевантный коммит и сборщик не стал лишний раз пересобирать образ без надобности — в этом случае образ остался связан со старым коммитом.
Другой вариант — не встраивать информацию о коммите в образы вообще. Зачем добавлять в образ изменчивую инфу, тем более, что commit-id не отражает его содержимое. А добавлять инфу о коммите куда-то в рантайм — Kubernetes — в процессе выката приложения. При выкате werf, например, через values может передать commit-id в шаблоны. Дальше его можно положить в какой-нибудь ConfigMap, приложение же может доставать commit-id в рантайме — читать его из ConfigMap. Тут получается и овцы сыты, и волки целы: лишних пересборок нету, приложение может получить инфу о гите.
Итоговый образ точно также как и кеш стадий будет изолирован за счет создания разных тегов. Вот в этом и заключается влияние истории гита на тег.
Другими словами: вот есть у нас 2 образа одинаковых по контенту, но собранных для разных гит-веток. Верф изолирует эти 2 образа: создаст для каждого свой идентификатор.
При этом в рамках одной ветки образы с общим контентом будут иметь одинаковый идентификатор.