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
Больше информации по настройке доступно в документации:
- Справочник > Публикация (publish);
- Работа с CI/CD > Общие сведения > stages-signature;
- Интеграция с GitLab CI/CD > .gitlab-ci.yml.
Итого
- Новая опция
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.
Читайте также в нашем блоге:
VolCh
Непонятно про влияние истории гитв на тег. Как он влияет и, главное, зачем?
tkir Автор
Цель: изоляция кеша стадий и итоговых образов собранных для разных веток в git. Кеш, собранный для какой-либо ветки от master не будет доступен в master до тех пор, пока эта ветка не будет смержена в master.
tkir Автор
Итоговый образ точно также как и кеш стадий будет изолирован за счет создания разных тегов. Вот в этом и заключается влияние истории гита на тег.
Другими словами: вот есть у нас 2 образа одинаковых по контенту, но собранных для разных гит-веток. Верф изолирует эти 2 образа: создаст для каждого свой идентификатор.
При этом в рамках одной ветки образы с общим контентом будут иметь одинаковый идентификатор.
VolCh
А зачем такая изоляция? Вот сделал я ветку от мастера, что-то в ней делал, подмержил мастер в итоге, сделал билд, получио образ, прогнал его по тестам. Вмерживаю в мастер, делаю билд, но кэш недоступен, всё опять с нуля.
tkir Автор
Не так. При мерже собранный кеш становится доступен для всех остальных точно также как и изменения которые были сделаны в ветке становятся доступны всем только после merge. Если мерж был не fast-forward, то возможно произойдет пересборка связанная со слиянием изменений мастера и ветки, но опять же уже собранный кеш будет учавствовать в сборке.
Если для ветки сделать rebase, то кеш, собранный для старого коммита потеряется и более не будет использоваться, также как теряется родительская связь между коммитами при rebase — произойдет пересборка. При дальнейшем merge кеш не теряется.
Такая изоляция — это отчасти переложение логики гита на собираемые образы.
tkir Автор
Можно сказать по-другому: мы выстраиваем content-based файловую систему на основе docker-образов, новое состояние в этой фс создается на основе кеша stages путем сборки новых образов, идентификаторы и пересборка тесно связаны с историей правок в git.