werf — наша GitOps CLI-утилита с открытым кодом для сборки и доставки приложений в Kubernetes. В релизе v1.1 была представлена новая возможность в сборщике образов: тегирование образов по содержимому или content-based tagging. До сих пор типичная схема тегирования в werf предполагала тегирование Docker-образов по Git-тегу, Git-ветке или Git-коммиту. Но у всех этих схем есть недостатки, которые полностью решаются новой стратегией тегирования. Подробности о ней и чем она так хороша — под катом.

Выкат набора микросервисов из одного Git-репозитория


Часто встречается ситуация, когда приложение разбито на множество более-менее независимых сервисов. Релизы этих сервисов могут происходить независимо: за один раз может релизиться один или несколько сервисов, остальные при этом должны продолжать работать без каких-либо изменений. Но с точки зрения хранения кода и управления проектом удобнее держать такие сервисы приложения в едином репозитории.

Бывают ситуации, когда сервисы действительно независимы и не связаны с одним приложением. В таком случае они будут расположены в отдельных проектах и их релиз будет осуществляться через отдельные процессы CI/CD в каждом из проектов.

Однако в реальности разработчики зачастую разбивают единое приложение на несколько микросервисов, но заводить для каждого отдельный репозиторий и проект… — явный overkill. Именно про эту ситуацию и пойдет далее речь: несколько таких микросервисов лежат в едином репозитории проекта и релизы происходят через единый процесс в CI/CD.

Тегирование по Git-ветке и Git-тегу


Допустим, используется самая распространенная стратегия тегирования — tag-or-branch. Для Git-веток образы тегируются названием ветки, для одной ветки в один момент времени существует только один опубликованный образ по имени этой ветки. Для Git-тегов образы тегируются соответственно именем тега.

При создании нового Git-тега — например, при выходе новой версии — для всех образов проекта в Docker Registry будет создан новый Docker-тег:

  • myregistry.org/myproject/frontend:v1.1.10
  • myregistry.org/myproject/myservice1:v1.1.10
  • myregistry.org/myproject/myservice2:v1.1.10
  • myregistry.org/myproject/myservice3:v1.1.10
  • myregistry.org/myproject/myservice4:v1.1.10
  • myregistry.org/myproject/myservice5:v1.1.10
  • myregistry.org/myproject/database:v1.1.10

Эти новые имена образов попадают через Helm-шаблоны в конфигурацию Kubernetes. При запуске деплоя командой werf deploy происходит обновление поля image в манифестах ресурсов Kubernetes и перезапуск соответствующих ресурсов из-за изменившегося имени образа.

Проблема: в случае, когда реально с предыдущего выката (Git-тега) не изменилось содержимое образа, а лишь его Docker-тег, происходит лишний перезапуск этого приложения и, соответственно, возможен некоторый простой. Хотя не было никаких реальных причин производить этот перезапуск.

Как следствие, при текущей схеме тегирования приходится городить несколько отдельных Git-репозиториев и встает проблема организации выката этих нескольких репозиториев. В общем и целом такая схема получается перегруженной и сложной. Лучше объединять много сервисов в единый репозиторий и создавать такие Docker-теги, чтобы лишних перезапусков не было.

Тегирование по Git-коммиту


В werf также присутствует стратегия тегирования, связанная с Git-коммитами.

Git-commit является идентификатором содержимого Git-репозитория и зависит от истории правок файлов в Git-репозитории, поэтому кажется логичным использовать его для тегирования образов в Docker Registry.

Однако тегирование по Git-коммиту имеет те же недостатки, что и по Git-веткам или Git-тегам:

  • Мог быть создан пустой коммит, который не меняет файлов, а Docker-тег образа будет изменен.
  • Мог быть создан merge-коммит, который не меняет файлов, а Docker-тег образа будет изменен.
  • Мог быть создан коммит, который меняет те файлы в Git, которые не импортируются в образ, а Docker-тег образа снова будет изменен.

Тегирование по имени Git-ветки не отражает версию образа


Есть и еще одна проблема, связанная со стратегией тегирования по Git-веткам.

Тегирование по имени ветки работает до тех пор, пока коммиты этой ветки собирают последовательно в хронологическом порядке.

Если в текущей схеме пользователь запустит пересборку старого коммита, связанного с некоторой веткой, то werf перетрет образ по соответствующему Docker-тегу вновь собранной версией образа для старого коммита. Использующие этот тег Deployment'ы с этого момента рискуют во время перезапуска pod'ов сделать pull другой версии образа, в результате чего наше приложение потеряет связь с CI-системой, рассинхронизируется.

Кроме того, при последовательных push’ах в одну ветку с малым промежутком времени между ними старый коммит может собраться позже, чем более новый: старая версия образа перетрет новую по тегу Git-ветки. Такие проблемы может решать CI/CD-система (например, в GitLab CI для серии коммитов запускается pipeline последнего). Однако это поддерживают не все системы и должен быть более надежный способ предотвращения столь фундаментальной проблемы.

Что такое content-based tagging?


Итак, что же такое content-based tagging — тегирование образов по содержимому.

Для создания Docker-тегов используются не примитивы Git'а (Git-ветка, Git-тег…), а контрольная сумма, связанная с:

  • содержимым образа. Идентификатор-тег образа отражает его содержимое. При сборке новой версии этот идентификатор не поменяется, если в образе не изменились файлы;
  • историей создания этого образа в Git. Образы, связанные с разными Git-ветками и разной историей сборки через werf, будут иметь разные теги-идентификаторы.

В качестве такого тега-идентификатора выступает так называемая сигнатура стадий образа.

Каждый образ состоит из набора стадий: from, before-install, git-archive, install, imports-after-install, before-setup,… git-latest-patch и т.д. У каждой стадии есть идентификатор, отражающий ее содержимое, — сигнатура стадии (stage signature).

Финальный же образ, состоящий из этих стадий, тегируется так называемой сигнатурой набора этих стадий — stages signature, — которая является обобщающей для всех стадий образа.

У каждого образа из конфигурации werf.yaml в общем случае будет своя такая сигнатура и, соответственно, Docker-тег.

Сигнатура стадий решает все указанные проблемы:

  • Устойчива к пустым Git-коммитам.
  • Устойчива к Git-коммитам, которые меняют файлы, не являющиеся релевантными для образа.
  • Не приводит к проблеме с перетиранием актуальной версии образа при перезапуске сборок для старых Git-коммитов ветки.

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

Как включить и использовать в werf


Соответствующая опция появилась у команды werf publish: --tag-by-stages-signature=true|false

В CI-системе стратегия тегирования задается командой werf ci-env. Ранее для нее определялся параметр werf ci-env --tagging-strategy=tag-or-branch. Теперь, если указать werf ci-env --tagging-strategy=stages-signature или не указывать эту опцию, werf по умолчанию будет использовать стратегию тегирования stages-signature. Команда werf ci-env автоматически выставит нужные флаги для команды werf build-and-publish (или werf publish), поэтому никаких дополнительных опций для этих команд указывать не нужно.

Например, команда:

werf publish --stages-storage :local --images-repo registry.hello.com/web/core/system --tag-by-stages-signature

… может создать следующие образы:

  • registry.hello.com/web/core/system/backend:4ef339f84ca22247f01fb335bb19f46c4434014d8daa3d5d6f0e386d
  • registry.hello.com/web/core/system/frontend:f44206457e0a4c8a54655543f749799d10a9fe945896dab1c16996c6

Здесь 4ef339f84ca22247f01fb335bb19f46c4434014d8daa3d5d6f0e386d — это сигнатура стадий образа backend, а f44206457e0a4c8a54655543f749799d10a9fe945896dab1c16996c6 — сигнатура стадий образа frontend.

При использовании специальных функций werf_container_image и werf_container_env в шаблонах Helm ничего менять не требуется: эти функции будут автоматически генерировать верные имена образов.

Пример конфигурации в CI-системе:

type multiwerf && source <(multiwerf use 1.1 beta)
type werf && source <(werf ci-env gitlab)
werf build-and-publish|deploy

Больше информации по настройке доступно в документации:


Итого


  • Новая опция werf publish --tag-by-stages-signature=true|false.
  • Новое значение опции werf ci-env --tagging-strategy=stages-signature|tag-or-branch (если не указать, то по умолчанию будет stages-signature).
  • Если до этого использовались опции тегирования по Git-коммитам (WERF_TAG_GIT_COMMIT или опция werf publish --tag-git-commit COMMIT), то обязательно переключать на стратегию тегирования stages-signature.
  • Новые проекты лучше сразу переключать на новую схему тегирования.
  • Старые проекты при переводе на werf 1.1 желательно переключать на новую схему тегирования, однако старая tag-or-branch по-прежнему поддерживается.

Content-based tagging решает все освещенные в статье проблемы:

  • Устойчивость имени Docker-тега к пустым Git-коммитам.
  • Устойчивость имени Docker-тега к Git-коммитам, которые меняют нерелевантные для образа файлы.
  • Не приводит к проблеме с перетиранием актуальной версии образа при перезапуске сборок для старых Git-коммитов для Git-веток.

Пользуйтесь! И не забывайте заглядывать к нам на GitHub, чтобы создать issue или найти уже существующий, поставить плюс, создать PR или просто понаблюдать за развитием проекта.

P.S.


Читайте также в нашем блоге: