company_banner

Content-based tagging в сборщике werf: зачем и как это работает?



    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.


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

    Флант
    Специалисты по DevOps и Kubernetes

    Комментарии 13

      +1

      Непонятно про влияние истории гитв на тег. Как он влияет и, главное, зачем?

        0

        Цель: изоляция кеша стадий и итоговых образов собранных для разных веток в git. Кеш, собранный для какой-либо ветки от master не будет доступен в master до тех пор, пока эта ветка не будет смержена в master.

          +2

          Итоговый образ точно также как и кеш стадий будет изолирован за счет создания разных тегов. Вот в этом и заключается влияние истории гита на тег.


          Другими словами: вот есть у нас 2 образа одинаковых по контенту, но собранных для разных гит-веток. Верф изолирует эти 2 образа: создаст для каждого свой идентификатор.


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

            +1

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

              0

              Не так. При мерже собранный кеш становится доступен для всех остальных точно также как и изменения которые были сделаны в ветке становятся доступны всем только после merge. Если мерж был не fast-forward, то возможно произойдет пересборка связанная со слиянием изменений мастера и ветки, но опять же уже собранный кеш будет учавствовать в сборке.


              Если для ветки сделать rebase, то кеш, собранный для старого коммита потеряется и более не будет использоваться, также как теряется родительская связь между коммитами при rebase — произойдет пересборка. При дальнейшем merge кеш не теряется.


              Такая изоляция — это отчасти переложение логики гита на собираемые образы.

                0

                Можно сказать по-другому: мы выстраиваем content-based файловую систему на основе docker-образов, новое состояние в этой фс создается на основе кеша stages путем сборки новых образов, идентификаторы и пересборка тесно связаны с историей правок в git.

          +1

          А как по этому тегу потом понять, какая версия кода выполняется (какой коммит конкретно был собран)?

            +3

            werf deploy добавляет аннотации:


            apiVersion: apps/v1
            kind: Deployment
            metadata:
              annotations:
                ci.werf.io/commit: bedee28e358378b2e20c406628f751499f15d2e3
                gitlab.ci.werf.io/job-url: https://gitlab.com/group/project/-/jobs/54301
                gitlab.ci.werf.io/pipeline-url: https://gitlab.com/group/project/pipelines/25574
                project.werf.io/env: env
                project.werf.io/git: https://gitlab.com/group/project
                project.werf.io/name: app
                service.werf.io/owner-release: app-env
                werf.io/version: v1.1.8.4
            spec:
              template:
                spec:
                  containers:
                  - name: pod
                    image: registry.gitlab.com/group/project/image:781a2fa1c6d299fe1f6d5cfa44dfcf0c7e2c6de3eec4d10fbe314523
              +3

              Как вариант — запоминать версию кода на этапе сборки образа. Но опять же commit-id — это неидеальный идентификатор кода, потому что его изменение не означает что код в образе изменился. При этом мог быть сделан какой-то нерелевантный коммит и сборщик не стал лишний раз пересобирать образ без надобности — в этом случае образ остался связан со старым коммитом.


              Другой вариант — не встраивать информацию о коммите в образы вообще. Зачем добавлять в образ изменчивую инфу, тем более, что commit-id не отражает его содержимое. А добавлять инфу о коммите куда-то в рантайм — Kubernetes — в процессе выката приложения. При выкате werf, например, через values может передать commit-id в шаблоны. Дальше его можно положить в какой-нибудь ConfigMap, приложение же может доставать commit-id в рантайме — читать его из ConfigMap. Тут получается и овцы сыты, и волки целы: лишних пересборок нету, приложение может получить инфу о гите.

                +1

                Да, отличная идея. Спасибо за совет, пошёл пилить))

                  +1

                  Я тут подумал, что можно по аналогии с helm3 в oci images хранить информацию о релизе, т.е. прямо в том же реестре где и контейнеры с сервисами.

                +1

                Парни, круто! Тоже сражался с этой проблемой и пришёл к выводу использовать дайджесты образов в виде тэгов. НО, как потом быстро соотнести такой тэг с ревизией vcs?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое