{ "index": 125, "slug": "editorial-2024-07-mechanism-release-engineering", "title": "Инженерия релиза: как доказать связь артефакта, миграции и отката", "excerpt": "Одинаковая версия в CI не доказывает, что будет доставлен нужный код. Разбираем цепочку commit → artifact → migration → rollout и ставим проверяемый stop до deploy.", "contentHtml": "

Симптом после выкладки: сервис отвечает старым поведением, хотя в CI и карточке релиза написано 2024.07.0. Команда повторяет deploy, увеличивает таймаут и смотрит на зелёный job. Если перед этим миграция изменила данные, откат контейнера может вернуть старый код, но не вернуть прежнее состояние базы. В итоге спорят не о причине сбоя, а о том, какой именно артефакт вообще работает.

\n

Номер версии удобен человеку, но слаб как единственное доказательство. Надёжнее проверять цепочку: заявленный commit связан с provenance сборки, rollout указывает на неизменяемый digest образа, миграция называет целевую границу совместимости, а для возврата записана конкретная ревизия. Это не разрешение на выкладку и не обещание успешного production deploy. Это stop-проверка, которая не даёт продолжить при расхождении записей.

\n

Что должна доказать запись о релизе

\n

Сначала разделим четыре разных вопроса. Источник. Из какого точного commit получены входы сборки? Артефакт. Какой digest соответствует результату сборки? Изменение данных. Какую схему или форму записи ожидает новая версия? Исполнение. Какой digest запросил rollout и чем подтверждено его завершение?

\n

Эти вопросы связаны, но не взаимозаменяемы. Provenance — это подписанная или иным образом проверяемая информация о том, как получен артефакт; она не заменяет политику потребителя. Digest идентифицирует содержимое образа, но не подтверждает, что образ запущен в нужном окружении. Успешный статус rollout говорит о состоянии Deployment, а не об обратимости миграции. Поэтому release record должен хранить отдельные поля, а проверка — отдельные результаты.

\n
Границы доказательств в цепочке релиза
СвязьПроверяемое утверждениеЧто не следует из успеха проверки
commit → provenanceВ provenance зафиксирован ожидаемый источник или dependency с точным идентификатором.Сборка безопасна от всех атак и полностью воспроизводима.
provenance → artifactSubject attestation относится к конкретному артефакту, а signer и builder входят в доверенную политику.Артефакт без уязвимостей и подходит каждому окружению.
artifact → rolloutВ намерении выкладки указан тот же immutable digest.Контейнер уже запущен и прошёл проверки доступности.
migration → releaseМиграция называет совместимую целевую версию и известный data effect.Миграцию можно безопасно отменить одной командой.
return point → rollbackНазваны точные version, digest и действие для данных.Старый код поддерживает новую схему после закрытия окна совместимости.
\n

Маленький gate на синтетических данных

\n

Ниже — самодостаточная проверка для командной строки. Записи вымышлены: значение примера в том, что одна связь намеренно сломана. Файл не вызывает registry, cluster или deploy runner; он только вычисляет логический результат из локального release record.

\n
cat > release-record.json <<'JSON'\n{\n  \"releaseVersion\": \"2024.07.0\",\n  \"commit\": { \"id\": \"commit-91\" },\n  \"provenance\": {\n    \"sourceCommitId\": \"commit-91\",\n    \"artifactDigest\": \"sha256:artifact-42\",\n    \"builderId\": \"ci.example/build\"\n  },\n  \"rollout\": { \"requestedDigest\": \"sha256:artifact-other\" },\n  \"migration\": {\n    \"targetVersion\": \"2024.07.0\",\n    \"compatibleWith\": [\"2024.06.x\"],\n    \"dataEffect\": \"adds nullable profile.locale\"\n  },\n  \"returnPoint\": {\n    \"version\": \"2024.06.4\",\n    \"digest\": \"sha256:previous-17\",\n    \"dataAction\": \"keep forward-compatible column\"\n  }\n}\nJSON\n\njq -e '\n  .provenance.sourceCommitId == .commit.id\n  and .provenance.artifactDigest == .rollout.requestedDigest\n  and .migration.targetVersion == .releaseVersion\n  and (.returnPoint.version != \"\" and .returnPoint.digest != \"\")\n' release-record.json\n# jq: false, exit code 1 — rollout digest не совпал\n
\n

Команда завершится с кодом 1, потому что sha256:artifact-other не равен digest, записанному в provenance. Три прочие части примера согласованы, но этого недостаточно: gate должен остановить действие при одном false. После исправления записи полезно повторить ту же команду и отдельно проверить, что исполнение rollout действительно завершилось. Логический true не подменяет runtime evidence.

\n
\"Схема
Схема разделяет проверку кода и проверку данных: возврат Deployment касается шаблона Pod, но не обещает отменить уже выполненную миграцию.
\n

Как читать расхождение

\n

Если в карточке и в CI одна версия, а поведение разное, первым делом не запускайте новый deploy. Получите фактический digest запущенного образа и сравните его с digest в намерении выкладки. Затем сопоставьте provenance с ожидаемым commit и доверенным builder. Здесь важно не восстановить «примерно ту же сборку», а найти конкретную точку, где цепочка перестала быть доказуемой.

\n

Если rollout указывает на правильный digest, но сервис всё ещё ведёт себя иначе, это уже другой класс проверки: конфигурация окружения, feature flag, кеш, трафик, версия зависимого сервиса и само состояние приложения. Цепочка артефакта не доказывает идентичность всех этих входов. Она лишь не позволяет списать любой эффект на слово «релиз».

\n

При расхождении миграции с release version нужно остановить и повторно согласовать план данных. Подмена значения в карточке задним числом стирает след ошибки. Зафиксируйте, какая миграция уже запущена, какие записи она изменила и с какой версией остаётся совместимой. Если этих сведений нет, статус должен быть «данные не классифицированы», а не «rollback готов».

\n

Почему откат образа не откатывает данные

\n

У приложения есть минимум два состояния: кодовый артефакт и состояние данных. Kubernetes Deployment хранит историю ревизий и позволяет вернуть Pod template к предыдущей ревизии, если она ещё доступна. Это полезно при проблеме с образом или параметрами запуска. Но команда kubectl rollout undo не отменяет SQL-миграцию, изменение документа или уже отправленное внешнему сервису событие.

\n

Поэтому миграцию стоит проектировать с окном совместимости, когда новый код умеет читать старую и новую форму данных. Сначала выкладывается код, способный работать в этом окне, затем выполняется изменение данных, после наблюдения удаляется старая ветка. Конкретный порядок зависит от хранилища и миграционного инструмента; это не универсальная лицензия на обратную миграцию.

\n

До закрытия окна точка возврата может быть обычным предыдущим артефактом. После закрытия нужно отдельное решение: forward fix, обратная миграция, восстановление резервной копии или сохранение нового кода с исправлением. В release record это должен быть явный dataAction, а не слово «откат» без объекта действия.

\n

Порядок проверки перед approval

\n
  1. Снимите исходные идентификаторы. Запишите release version, точный commit SHA, digest артефакта, builder и окружение. Ветка, номер задачи и tag остаются удобными ссылками, но не заменяют SHA и digest.
  2. Проверьте provenance. Сверьте subject артефакта, source или dependency, signer и builder с политикой команды. Если используется attestation, проверяйте её криптографически, а не только открывайте страницу с метаданными.
  3. Сверьте намерение и исполнение. В manifest или release record должен быть тот же digest, который был разрешён. После deploy сохраните отдельное подтверждение состояния и времени rollout.
  4. Опишите data effect. Назовите таблицу, поля, документы или события, которые изменятся, а также допустимую старую форму. Не ставьте approval, если миграция существует только как название job.
  5. Назовите return point. Укажите конкретные version и digest для кода и отдельное решение для данных. Проверьте, что требуемая ревизия не удалена политикой хранения истории.
  6. Остановитесь при первом false. Верните запись на reconcile, не запускайте повторную попытку ради зелёного CI. Approval относится к проверенной записи; он не является доказательством фактического успеха выкладки.
\n

Границы применимости

\n

Этот маршрут подходит как минимальный контроль согласованности для релиза, где команда может получить commit, provenance, digest, план миграции и запись rollout. Он не заменяет сканирование уязвимостей, review кода, проверку секретов, контроль прав, тесты совместимости, резервное копирование, мониторинг или процедуру incident response.

\n

SLSA описывает модель provenance и требования к её проверке, но не объявляет конкретный артефакт безопасным. GitHub отдельно предупреждает, что artifact attestation связывает артефакт с источником и инструкциями сборки, а решение о доверии требует собственной политики. В частном registry, другой CI-системе или без доверенного корня проверки команды и поля будут другими.

\n

Синтетический release-record.json нельзя подключать к настоящему deploy без адаптации схемы, прав и источников фактов. Kubernetes-команды требуют доступа к конкретному кластеру и работают с историей, которую можно ограничить настройкой revisionHistoryLimit. Если миграция необратима или внешний эффект уже ушёл, честный результат может быть «код возвращён, data effect остаётся».

\n

Проверяемый результат

\n

Перед авторизованной выкладкой второй инженер должен без устных пояснений найти в записи пять вещей: точный источник сборки, digest артефакта, digest в rollout, границу совместимости миграции и раздельный план возврата кода и данных. Для каждой связи должен быть результат true или false, а для false — владелец сверки и стоп-действие.

\n

После выкладки добавьте к этим записям фактический результат rollout и наблюдаемый сигнал приложения. Только тогда можно обсуждать поведение окружения. Такая последовательность возвращает разговор к исходному симптому: мы проверяем не красивую строку версии, а то, что именно собрано, что именно запрошено, что изменилось в данных и что реально можно вернуть.

\n

Проверяемые источники

" }