{ "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. Если перед этим миграция изменила данные, откат контейнера может вернуть старый код, но не вернуть прежнее состояние базы. В итоге спорят не о причине сбоя, а о том, какой именно артефакт вообще работает.
Номер версии удобен человеку, но слаб как единственное доказательство. Надёжнее проверять цепочку: заявленный commit связан с provenance сборки, rollout указывает на неизменяемый digest образа, миграция называет целевую границу совместимости, а для возврата записана конкретная ревизия. Это не разрешение на выкладку и не обещание успешного production deploy. Это stop-проверка, которая не даёт продолжить при расхождении записей.
\nСначала разделим четыре разных вопроса. Источник. Из какого точного commit получены входы сборки? Артефакт. Какой digest соответствует результату сборки? Изменение данных. Какую схему или форму записи ожидает новая версия? Исполнение. Какой digest запросил rollout и чем подтверждено его завершение?
\nЭти вопросы связаны, но не взаимозаменяемы. Provenance — это подписанная или иным образом проверяемая информация о том, как получен артефакт; она не заменяет политику потребителя. Digest идентифицирует содержимое образа, но не подтверждает, что образ запущен в нужном окружении. Успешный статус rollout говорит о состоянии Deployment, а не об обратимости миграции. Поэтому release record должен хранить отдельные поля, а проверка — отдельные результаты.
\n| Связь | Проверяемое утверждение | Что не следует из успеха проверки |
|---|---|---|
| commit → provenance | В provenance зафиксирован ожидаемый источник или dependency с точным идентификатором. | Сборка безопасна от всех атак и полностью воспроизводима. |
| provenance → artifact | Subject attestation относится к конкретному артефакту, а signer и builder входят в доверенную политику. | Артефакт без уязвимостей и подходит каждому окружению. |
| artifact → rollout | В намерении выкладки указан тот же immutable digest. | Контейнер уже запущен и прошёл проверки доступности. |
| migration → release | Миграция называет совместимую целевую версию и известный data effect. | Миграцию можно безопасно отменить одной командой. |
| return point → rollback | Названы точные version, digest и действие для данных. | Старый код поддерживает новую схему после закрытия окна совместимости. |
Ниже — самодостаточная проверка для командной строки. Записи вымышлены: значение примера в том, что одна связь намеренно сломана. Файл не вызывает registry, cluster или deploy runner; он только вычисляет логический результат из локального release record.
\ncat > 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.
Если в карточке и в CI одна версия, а поведение разное, первым делом не запускайте новый deploy. Получите фактический digest запущенного образа и сравните его с digest в намерении выкладки. Затем сопоставьте provenance с ожидаемым commit и доверенным builder. Здесь важно не восстановить «примерно ту же сборку», а найти конкретную точку, где цепочка перестала быть доказуемой.
\nЕсли rollout указывает на правильный digest, но сервис всё ещё ведёт себя иначе, это уже другой класс проверки: конфигурация окружения, feature flag, кеш, трафик, версия зависимого сервиса и само состояние приложения. Цепочка артефакта не доказывает идентичность всех этих входов. Она лишь не позволяет списать любой эффект на слово «релиз».
\nПри расхождении миграции с release version нужно остановить и повторно согласовать план данных. Подмена значения в карточке задним числом стирает след ошибки. Зафиксируйте, какая миграция уже запущена, какие записи она изменила и с какой версией остаётся совместимой. Если этих сведений нет, статус должен быть «данные не классифицированы», а не «rollback готов».
\nУ приложения есть минимум два состояния: кодовый артефакт и состояние данных. Kubernetes Deployment хранит историю ревизий и позволяет вернуть Pod template к предыдущей ревизии, если она ещё доступна. Это полезно при проблеме с образом или параметрами запуска. Но команда kubectl rollout undo не отменяет SQL-миграцию, изменение документа или уже отправленное внешнему сервису событие.
Поэтому миграцию стоит проектировать с окном совместимости, когда новый код умеет читать старую и новую форму данных. Сначала выкладывается код, способный работать в этом окне, затем выполняется изменение данных, после наблюдения удаляется старая ветка. Конкретный порядок зависит от хранилища и миграционного инструмента; это не универсальная лицензия на обратную миграцию.
\nДо закрытия окна точка возврата может быть обычным предыдущим артефактом. После закрытия нужно отдельное решение: forward fix, обратная миграция, восстановление резервной копии или сохранение нового кода с исправлением. В release record это должен быть явный dataAction, а не слово «откат» без объекта действия.
Этот маршрут подходит как минимальный контроль согласованности для релиза, где команда может получить commit, provenance, digest, план миграции и запись rollout. Он не заменяет сканирование уязвимостей, review кода, проверку секретов, контроль прав, тесты совместимости, резервное копирование, мониторинг или процедуру incident response.
\nSLSA описывает модель provenance и требования к её проверке, но не объявляет конкретный артефакт безопасным. GitHub отдельно предупреждает, что artifact attestation связывает артефакт с источником и инструкциями сборки, а решение о доверии требует собственной политики. В частном registry, другой CI-системе или без доверенного корня проверки команды и поля будут другими.
\nСинтетический release-record.json нельзя подключать к настоящему deploy без адаптации схемы, прав и источников фактов. Kubernetes-команды требуют доступа к конкретному кластеру и работают с историей, которую можно ограничить настройкой revisionHistoryLimit. Если миграция необратима или внешний эффект уже ушёл, честный результат может быть «код возвращён, data effect остаётся».
Перед авторизованной выкладкой второй инженер должен без устных пояснений найти в записи пять вещей: точный источник сборки, digest артефакта, digest в rollout, границу совместимости миграции и раздельный план возврата кода и данных. Для каждой связи должен быть результат true или false, а для false — владелец сверки и стоп-действие.
После выкладки добавьте к этим записям фактический результат rollout и наблюдаемый сигнал приложения. Только тогда можно обсуждать поведение окружения. Такая последовательность возвращает разговор к исходному симптому: мы проверяем не красивую строку версии, а то, что именно собрано, что именно запрошено, что изменилось в данных и что реально можно вернуть.
\n