Как стать автором
Обновить
45
0
Timofey Kirillov @tkir

System utils development

Отправить сообщение

Добрый день!

Обычно мы в werf стремимся получать обратную связь от инженеров, которые могут принести кейс использования, и высокоуровневый сценарий для которого нужна некоторая фича. Мы ожидаем что инженеры приносят свои кейсы, без обратной связи по разным каналам, было бы невозможно сделать инструмент лучше и универсальнее.

Возможно в вашем кейсе такая обратная связь была и мы её упустили где-нибудь в телеграме или issue — в любом случае стоит продублировать и мы обязательно ответим предметно.

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

Почему две сборки подряд из одного коммита будут не идентичными?

Например потому что мы никак не контролируем что происходит в сборочных инструкциях типа:

 - apt-get update
 - apt-get install some-solution

— и версия 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 делает push каждого собранного слоя (стадии) в registry сразу после успешной сборки, что позволяет сразу переиспользовать его в других процессах сборки на произвольных машинах. Это возможно благодаря новому алгоритму публикации и выборки стадий с оптимистичными блокировками.
  • На момент публикации финального образа по определённому имени все необходимые слои, из которых состоит этот образ, уже есть в registry. Werf по факту делает push лишь для небольшого слоя с некоторой мета-информацией и ставит на этот слой тег. Таким образом этот push происходит моментально, т.к. registry переиспользует существующие слои.

К тому же werf по умолчанию предлагает content-based-тегирование, что позволяет ещё лучше переиспользовать уже существующие слои и избавится от лишних ребилдов и редеплоев приложения.

Так-то оно так, но ни kaniko, ни buildah не умеют собирать по-настоящему распределённый кеш. Чтобы уже собранные слои были доступны во всех сборках.


В werf теперь возможно такое: werf собирает коммит номер 1 на одном билдере. Пользователь делает новый коммит в git, werf запускается на другом билдере, накладывает патч на уже существующий кеш из предыдущей сборки на другом раннере и публикует новый образ.


Это похоже на то, что предоставляет --cache-from в docker builder, но быстрее и эффективнее и с поддержкой ansible и инкрементальных пересборок по истории git и т.д.

Да, сборщик werf требует доступа к локальному docker-server на хостах, где запускается.

https://habr.com/ru/company/flant/blog/504390/
Промахнулся.

Да, сборщик werf требует доступа к локальному docker-server на хостах, где запускается.

Можно сказать по-другому: мы выстраиваем 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 образа: создаст для каждого свой идентификатор.


При этом в рамках одной ветки образы с общим контентом будут иметь одинаковый идентификатор.

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Работает в
Дата рождения
Зарегистрирован
Активность