24 авг. 2024 г.·7 мин чтения

Подписание Docker-образов в GitLab, которым команды продолжают пользоваться

Подписание Docker-образов в GitLab даёт командам понятный способ доказать, что именно было выпущено, добавить проверки provenance и при этом сохранить короткий и удобный путь релиза.

Подписание Docker-образов в GitLab, которым команды продолжают пользоваться

Почему команды теряют след того, что на самом деле попало в production

Большинство проблем с релизами начинается с мелких упрощений, а не с громких провалов. Команда быстро выкатывает исправление, снова запускает pipeline, переносит image на другой tag и идёт дальше. Через две недели уже никто не может уверенно сказать, что именно работает в production.

Одна из частых проблем — дрейф при пересборке. Команды собирают один и тот же commit дважды и ожидают одинаковый image, но получают разный результат. Base image мог измениться, зеркало пакетов могло отдать более новую версию, или шаг сборки мог подтянуть свежие зависимости. Код не менялся, а digest image — да.

Ещё хуже становится, когда люди чинят всё вручную. Инженер собирает image локально, потому что pipeline заблокирован. Кто-то вручную правит значение конфигурации, чтобы уложиться в дедлайн. Кто-то другой отправляет image с тем же tag, чтобы deploy не остановился. Сервис, возможно, и восстановится, но в истории релиза уже появится дыра.

Tags добавляют ещё больше путаницы, потому что выглядят стабильными, хотя это не так. Команды доверяют названиям вроде staging, prod или даже version tag, но tags могут смещаться. Если никто внимательно не следит за digest, tag меняется, а команда продолжает говорить так, будто он всё ещё указывает на тот же image. Откаты быстро превращаются в хаос, если история tag неясна.

Во время разбора инцидента это превращает техническую проблему в проблему учёта. Люди перестают спрашивать, почему возникла ошибка, и начинают задавать базовые вопросы:

  • Какой commit создал этот image?
  • Какая pipeline его собрала?
  • Кто-то пересобирал его вне GitLab?
  • Работает ли production на том же digest, который прошёл тесты?

Если на поиск ответов уходит час, доверие к pipeline падает. Инженеры перестают верить записям о релизах и начинают делать обходные пути. А это делает следующий релиз ещё сложнее для отслеживания.

Подписание Docker-образов помогает, но только если путь к релизу остаётся коротким и предсказуемым. Когда команда может проследить один image digest от commit до pipeline и далее до deploy, она перестаёт спорить о том, что, вероятно, было отправлено, и начинает опираться на факты.

Что на самом деле доказывают подпись и provenance

Подписание Docker-образов решает базовую проблему доверия. Когда в registry лежит несколько тегов, которые выглядят валидными, подпись показывает вашему deploy job, какой image пришёл от одобренной identity сборки, а какой — нет.

Один tag сам по себе мало что доказывает. Человек может переиспользовать tag, отправить image с неправильной машины или опубликовать image вне обычного GitLab release pipeline. В registry image может выглядеть нормально, но вы уже не знаете, собрала ли его команда привычным способом.

Provenance — это квитанция. Она фиксирует, откуда пришёл image: commit, pipeline run, builder и входные данные сборки. Когда вы проверяете provenance, можно отследить один работающий container до конкретного CI job, а не полагаться на правила именования или память.

Это даёт понятный ответ на вопрос, который команды задают во время релизов и инцидентов: «Откуда взялся этот image?» При наличии подписи и provenance ответ находится за секунды, а не после долгого поиска по чатам и старым pipeline run.

Проверка должна происходить до deploy, а не после неожиданности. Простая политика может блокировать images, которые не соответствуют identity, которой вы доверяете, или provenance, который вы ожидаете.

Например, проверка может отклонить images, которые были:

  • собраны вне CI
  • пересобраны под тем же tag
  • скопированы из другого проекта
  • созданы не из той ветки или не из того источника pipeline

Вот где становится полезной проверка cosign. Инженерам не нужно каждый раз вручную смотреть metadata. Pipeline проверяет подпись, проверяет provenance и останавливает неизвестные artefacts до того, как они попадут в staging или production.

Для небольшой команды это особенно важно. Если версия 1.8.2 сломалась после релиза, команда может сразу подтвердить точный commit и pipeline, стоящие за image. Они тратят меньше времени на споры о том, что было отправлено, и больше — на исправление проблемы.

Подпись и provenance в простых словах доказывают три вещи: кто собрал image, откуда он пришёл и можно ли вашей системе развёртывания ему доверять.

Выберите такой путь релиза, которым люди действительно будут пользоваться

Команды обходят контрольные проверки, если безопасный путь кажется длиннее старого. Если для релиза нужны локальные скрипты, ручные tags и отдельная команда для signing, люди в первый же напряжённый момент найдут обходной путь.

Для подписание Docker-образов самое безопасное правило одновременно и самое простое в использовании: только GitLab CI должен создавать release image. Локальный ноутбук разработчика может собирать image для тестов, но никогда не должен создавать тот image, который попадёт в registry. CI уже знает commit SHA, identity job и логи, так что последующие проверки имеют смысл.

Путь релиза должен оставаться почти одинаковым для каждой сборки. Обычные сборки ветки и релизные сборки должны использовать один и тот же Dockerfile, одну и ту же job сборки и один и тот же шаг signing. Release tag должен менять metadata, а не создавать второй workflow. Два пути — это два набора ошибок, и обычно один из них просто игнорируют.

Хорошо работает простая отправная схема:

  • Соберите image в GitLab CI.
  • Отправьте его в один registry.
  • Подпишите его в той же pipeline.
  • Разверните его в одно защищённое окружение.
  • Завалите pipeline, если не хватает хотя бы одной проверки.

Этого достаточно для небольшой команды. Потом можно добавить больше, но многие команды делают это слишком рано. Два registry, три окружения и особые правила для hotfix могут казаться разумными на бумаге. На практике они только мешают понять, что именно было выпущено и почему.

Понятные сообщения об ошибках важнее, чем многие думают. «Job failed» никому не подскажет, что делать. «Signature missing for image sha256:...» или «provenance does not match commit 8f3c...» дают инженеру понятную задачу, которую можно решить за минуты. Хороший GitLab release pipeline быстро останавливается и точно говорит, что сломалось.

Вот простой пример, почему это работает. Команда сливает изменения в main, GitLab собирает image, подписывает его, отправляет в единственный registry, который используется для релизов, и разворачивает его в staging. Когда они ставят release tag, GitLab использует тот же путь и продвигает тот же image. Никому не нужно помнить вторую команду, и никому не нужно верить, что ноутбук всё сделал правильно.

Добавляйте подписание image в GitLab шаг за шагом

Если pipeline пересобирает один и тот же релиз дважды, signing вас не спасёт. Пусть GitLab соберёт один image, отправит именно этот artefact в registry и подпишет digest, который вернулся после push.

Обычно хороший путь релиза выглядит так:

  1. Соберите один image для release commit.
  2. Пометьте его human tag, например 1.8.2, и commit tag, например short SHA.
  3. Отправьте оба tag в GitLab container registry.
  4. Считайте неизменяемый digest из отправленного image.
  5. Подпишите этот digest с помощью cosign и сохраните его вместе с записью о релизе.

Digest важен, потому что tags смещаются. Tag вроде latest через десять минут может указывать уже на что-то другое. Digest — нет. Если ваш deploy job проверяет registry.example.com/app@sha256:..., вы точно знаете, какой код запущен.

Небольшой job в GitLab может сделать эту работу, не превращая релиз в мучение:

release_image:
  stage: release
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG")
    - cosign sign --yes "$DIGEST"
    - echo "$DIGEST" > image-digest.txt
  artifacts:
    paths:
      - image-digest.txt

Оставьте identity signing простой. Многие команды начинают с cosign private key, хранящегося в protected GitLab CI variables. Это работает, но OIDC лучше, если вы хотите меньше работать с секретами. С OIDC GitLab подтверждает identity job в момент signing, и вам не нужно хранить в CI long-lived private key.

Какой бы способ вы ни выбрали, закройте его как следует. Только protected branches и tags должны запускать signing job. Если подпись может ставить любая ветка, она перестаёт что-то значить.

Сохраняйте подписанный digest там, где люди смогут быстро найти его во время review релиза. Положите его в release notes GitLab, прикрепите image-digest.txt как artefact или запишите в небольшой metadata-файл рядом с номером версии. Когда инженеру позже понадобится ответ, он должен за минуту понять один вопрос: какой именно image мы отправили?

Подписание Docker-образов становится полезным только тогда, когда подписанный digest появляется в том же коротком пути, которым команда уже пользуется для tagging, review и deploy.

Проверяйте provenance до deploy

Укрепите staging и production
Используйте одно и то же правило проверки перед каждым деплоем, чтобы команда тестировала то, что выпускает.

Перед тем как любой deploy job заберёт image, он должен ответить на два вопроса: собрала ли этот digest наша GitLab pipeline и совпадает ли metadata с этим релизом? Одной подписи недостаточно. Она доказывает, что image кто-то подписал. Provenance показывает, какой commit, pipeline, runner и base image его создали.

Генерируйте provenance в той же pipeline, которая собирает image. Если позже metadata добавляет другой job или кто-то подписывает с ноутбука, доверие быстро падает. Держите сборку, signing и attestation рядом, чтобы digest, commit SHA и pipeline ID приходили из одного пути.

Записывайте небольшой набор фактов каждый раз:

  • точный commit SHA
  • GitLab pipeline ID
  • runner, который собрал image
  • имя и digest base image
  • финальный image digest

Этого достаточно для большинства команд. На первом этапе не нужен огромный policy file.

Ваш deploy в staging должен проверять и подпись, и provenance ещё до старта. Если вы используете cosign verification, убедитесь, что attestation указывает на тот же digest, который вы собираетесь развернуть, а затем сравните записанные metadata с входными данными релиза. Если tag говорит, что image пришёл из commit a1b2c3, attestation должна говорить то же самое. Если релизы собирают только доверенные runners, runner должен входить в этот разрешённый набор. Если digest base image изменился, считайте это другой сборкой.

Запускайте тот же verify step ещё раз перед production. Не думайте, что staging уже всё доказал. Tags смещаются, jobs запускаются повторно, а ручные правки просачиваются, когда команды спешат. Проверка точного digest на каждом deploy делает правило простым и трудным для обхода.

Если проверка не прошла, возвращайте одно понятное сообщение об ошибке. Не пишите стену логов. Сообщение вроде «Release blocked: image provenance check failed. Rebuild and sign this image in the GitLab release pipeline.» даёт инженеру ясный следующий шаг.

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

Небольшой пример релиза для маленькой команды

Трёхчленная команда запускает один web API через GitLab. Они специально держат путь релиза коротким, потому что лишние шаги обычно пропускают после долгой недели. Подписание Docker-образов остаётся в процессе только тогда, когда выпускать всё ещё просто.

Sara сливает изменение после двух ревью и зелёного прогона тестов. В тот день за релизы отвечает Dan, поэтому он создаёт release tag из уже просмотренного commit. GitLab собирает image, отправляет его в registry, подписывает image с помощью identity pipeline и сохраняет digest вместе с записью job.

Их путь простой:

  • просмотренный merge попадает в main branch
  • один инженер создаёт release tag
  • tagged pipeline собирает и подписывает один image digest
  • staging разворачивает image только если подпись совпадает с pipeline этого проекта

Это правило для staging важнее, чем кажется многим командам. Оно блокирует images, собранные на ноутбуке, images, переназначенные вручную, и images, скопированные из старой job с тем же label версии. Если подпись не совпадает с GitLab project и ожидаемым workflow, staging отказывает.

После быстрого smoke test команда продвигает тот же digest из staging в production. Они не пересобирают image. Они не берут latest. Production принимает только тот digest, который уже прошёл staging, так что все знают: запущенный container — тот же самый, что и на тестах.

Через неделю клиент сообщает о плохом ответе одного endpoint. Команда проверяет digest, который работает в production, а затем ищет данные подписи и запись pipeline, привязанные к этому digest. Через несколько минут они видят commit, merge request, результаты тестов и того, кто одобрил изменение.

Такая скорость меняет настроение во время инцидента. Никто не спорит, какой image действительно был выпущен. Никто не тратит полчаса на сравнение tags между окружениями. Один digest указывает на одну pipeline, а одна pipeline — на точный код, который ушёл наружу.

Для маленькой команды этого достаточно. Им не нужна длинная цепочка согласований или стопка policy file на старте. Им нужен один надёжный путь от просмотренного merge к подписанному image и дальше к проверенному deploy, и этот путь должен работать каждый раз.

Ошибки, которые подрывают доверие к pipeline

Нужна помощь Fractional CTO?
Получите практическую помощь с GitLab, инфраструктурой и безопасной поставкой ПО.

Доверие быстро падает, когда pipeline говорит одно, а кластер запускает другое. Обычно это начинается с маленького упрощения, а не с громкого сбоя.

Частая ошибка при подписание Docker-образов — подписывать tag вроде :latest или :release-1.4, а не image digest. Tags смещаются. Digest — нет. Если команда подписывает tag, а потом пересобирает или переназначает его позже, подпись больше не отвечает на единственный вопрос, который всех волнует: «Какой именно image мы отправили?»

Срочные исправления ломают следующий шаг. Кто-то вручную отправляет image, потому что production горит, и теперь в GitLab release pipeline появляется пробел в истории. Исправление, возможно, сработает, но никто не сможет доказать, откуда взялся этот image, какой commit его создал и были ли на нём запущены тесты в CI. Быстрый путь — нормально. Обходной — нет.

Long-lived signing keys создают скрытый риск. Команды часто держат один private key в CI variable, дают широкий доступ maintainer-ам и забывают об этом на месяцы. Этот key расползается по слишком многим местам и job. Если он утечёт через один runner, ноутбук или проект, каждая прошлая и будущая подпись начинает вызывать сомнения. Identity signing с коротким сроком жизни обычно чище.

Ручные approvals тоже могут навредить, если их слишком много. Если релиз требует пяти кликов в трёх инструментах, люди перестают уважать процесс. Они начинают просить исключения, а потом и вовсе перестают ждать approvals. Лучший путь короткий: собрать один раз, подписать digest, добавить provenance, проверить, развернуть.

Ещё одна ошибка проявляется позже. Команды проверяют только в production, после того как staging уже ушёл в сторону. Тогда staging тестирует один image, а production запускает другой, и cosign verification превращается в последний барьер перед релизом, а не в обычную проверку. Container provenance checks должны происходить перед каждым шагом deploy, а не только в конце.

Если вы хотите, чтобы инженеры продолжали пользоваться процессом, держите перед глазами одно правило: один и тот же digest должен идти из CI в staging и дальше в production, с той же подписью и тем же attached provenance. Как только это правило ломается, обычно ломается и доверие.

Быстрые проверки перед тем, как вы поставите tag на релиз

Упростите путь релиза
Oleg помогает стартапам убрать ручные шаги и сохранить доверие к каждому изменению в production.

Release tag должен указывать на один точный image, собранный один раз и привязанный к одному commit. Чтобы подписание Docker-образов вообще что-то значило, команде нужно, чтобы эта цепочка оставалась короткой и очевидной.

Если кто-то спрашивает: «Что мы отправили?», ответ должен занимать секунды, а не переписку в Slack и три вкладки GitLab.

  • Сборка и signing должны происходить в одной и той же GitLab pipeline и относиться к одному digest. Не пересобирайте image в более позднем job только ради подписи.
  • Deploy jobs должны читать image digest, а не mutable tag вроде latest. Tags помогают людям быстро смотреть релизы. Digests точно говорят серверам, что запускать.
  • Staging и production должны применять одно и то же правило проверки. Если staging пропускает cosign verification, а production требует его, ваш test path уже не совпадает с release path.
  • Signing credentials должны легко ротироваться. Если кто-то уходит или key меняется, команда должна обновить signer, не переписывая половину pipeline.
  • В логах релиза должны быть commit SHA, image digest и identity signer в одном месте. Эта запись экономит время, когда нужно подтвердить, что именно ушло наружу.

Одна маленькая нестыковка может быстро разрушить доверие. Разработчик ставит tag v1.8.3, staging проходит успешно, а потом кто-то отправляет свежий image в latest до того, как production начнёт deploy. Если production читает tag вместо digest, у вас уже есть release note для одной сборки и живой system, работающая на другой.

Ротацию credential стоит проверить заранее, до того как она вам понадобится. Команды часто игнорируют её, пока key не истечёт вечером в пятницу. Оставьте signing setup скучным: понятный владелец, понятный процесс, одно место для обновления доверенного signer и один тестовый релиз, который доказывает, что новый signer работает.

Это не требует тяжёлого процесса. Одна сборка, одна подпись, один digest и одно правило проверки в обоих окружениях обычно достаточно. Когда в логах рядом видны commit, digest и signer, release tag снова начинают что-то значить.

Следующие шаги для лёгкой схемы

Начните с одного service, который часто меняется. Выберите API или worker, с которым команда работает каждую неделю, а не тихий service, который никто не хочет трогать. Частые релизы быстро показывают трение, а именно оно заставляет людей пропускать подписание Docker-образов.

Если новый путь добавляет слишком много работы, инженеры вернутся к старым привычкам. Обычно это случается, когда кто-то должен подписывать локально, вручную копировать digest или помнить отдельную команду перед deploy. Под давлением времени эти шаги исчезают первыми.

Опишите весь путь релиза на одной странице. Сделайте его достаточно коротким, чтобы новый коллега мог пройти его без помощи. Для многих команд на странице достаточно четырёх действий:

  • собрать image в GitLab
  • подписать его в той же pipeline
  • проверить digest и provenance перед deploy
  • поставить tag релиза из этого запуска pipeline

Потом проверьте эту страницу вместе с командой, а не только с человеком, который её написал. Хорошая проверка простая: попросите одного инженера выпустить небольшое изменение, пока другой смотрит на задержки, заминки или отсутствие доступа. Если они останавливаются и спрашивают: «Какой image мы подписали?» или «Откуда взялся этот tag?», процесс всё ещё слишком размытый.

Уберите то, что люди избегают

Всё, что люди пропускают в загруженный день, нужно убрать, объединить или автоматизировать. Ручное approval может остаться, если оно защищает production, но сделайте его в один клик и с понятным контекстом. Проверки provenance должны запускаться в deploy job, а не жить в wiki, которую никто не открывает.

Лёгкая схема часто выглядит лучше после сокращения, а не после добавления. Один способ signing, один источник tag, одно место проверки, одно понятное сообщение об ошибке. Обычно этого достаточно для маленькой команды.

Реалистичная первая победа скромная: один service, одна protected branch, одна release job и cosign verification перед deployment. Поработайте так несколько недель. Если люди продолжают пользоваться этим без напоминаний, перенесите шаблон на следующий service.

Если вашей команде нужна помощь с настройкой CI, инфраструктурой или сокращением пути релиза без потери доверия, Oleg Sotnikov на oleg.is работает со стартапами и небольшими компаниями как Fractional CTO. В его опыте есть production-инфраструктура на базе GitLab, lean CI/CD и практичные AI-driven workflows разработки, поэтому цель остаётся простой: сделать процесс удобнее, а не тяжелее.

Часто задаваемые вопросы

Почему лучше деплоить по digest, а не по tag?

Используйте image digest, а не tag. Tag вроде latest или v1.8.2 может сместиться, а digest указывает на один и тот же точный image навсегда.

Если ваш deploy job читает image@sha256:..., вы всегда знаете, какой код отправили в production и к какому состоянию можно откатиться.

Что на самом деле доказывает подпись image?

Подпись показывает, кто подписал image, и можно ли вашему deploy job ему доверять. Она помогает блокировать образы, которые пришли не оттуда, например после сборки на ноутбуке или после случайного retag.

Но сама по себе подпись не исправит запутанный путь релиза. Вам всё равно нужен один путь сборки в GitLab и один digest, который проходит через staging и production.

Что такое provenance и зачем он нужен?

Provenance показывает, откуда пришёл image. Она связывает digest с commit, pipeline, runner и входными данными сборки.

Так вы быстро получаете ответ во время релиза или инцидента. Можно проверить один работающий image и отследить его до конкретного CI job вместо того, чтобы гадать по тегам или истории в чате.

Где лучше делать signing в GitLab pipeline?

Сначала соберите image один раз в GitLab CI, отправьте его в один registry, получите digest и подпишите именно этот digest в той же pipeline. Затем заставьте deploy job проверять подпись и provenance перед тем, как он заберёт image.

Держите путь коротким. Когда инженеры используют один и тот же поток и для обычных релизов, и для tagged релизов, у них меньше причин искать обходные пути.

Стоит ли использовать cosign с OIDC или с private key?

OIDC обычно даёт более аккуратную схему, потому что GitLab подтверждает identity job в момент signing. Вам не нужно хранить long-lived private key в CI, а значит меньше возни с секретами.

Если вы начинаете с private key, держите его в protected CI variables и ограничьте круг тех, кто может запускать signing job. Потом, когда сможете, переходите на OIDC.

Нужно ли проверять и в staging, и в production?

Да. Проверяйте перед staging и ещё раз перед production. Tags могут смещаться, jobs могут запускаться повторно, а люди могут добавить ручной фикс, когда спешат.

Когда оба окружения проверяют один и тот же digest, вы знаете, что production работает на том же image, который проверил staging.

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

Потому что две сборки могут дать два разных image, даже если commit один и тот же. Base image может измениться, версия пакета может сдвинуться, или шаг сборки может подтянуть свежие зависимости.

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

Что делать, если проверки подписи или provenance не прошли?

Останавливайте deploy и возвращайте одно понятное сообщение об ошибке, которое говорит инженеру, что именно нужно исправить. Что-то вроде Signature missing for digest ... или Provenance does not match commit ... работает лучше, чем стена логов.

Ясные ошибки удерживают людей внутри процесса. Неясные сбои толкают их к ручным обходным путям.

Как обрабатывать срочные hotfix?

Не позволяйте сборке на ноутбуке сразу попадать в production. Если нужен быстрый фикс, внесите изменение в commit, запустите GitLab release pipeline и подпишите новый digest там же.

Скорость важна, но отслеживаемость важна не меньше. Быстрый путь внутри CI всегда лучше обхода.

Какая минимальная схема подойдёт небольшой команде?

Начните с малого. Выберите один service, используйте один registry, пусть GitLab соберёт и подпишет один image, а перед каждым deploy проверяйте digest.

Эта схема даёт небольшой команде достаточно контроля, но не перегружает лишними tool-ами или кучей правил. Если люди продолжают ею пользоваться, перенесите тот же подход на следующий service.