Инженерная практика
can-i-deploy и версионирование контрактов: гейт безопасности деплоя
checkout-web Прии добавляет фичу, которой нужно новое поле discount_cents. Она обновляет тест консьюмера, pact перегенерируется, и CI публикует его в брокер. Провайдер pricing Сэма ещё не выкатил discount_cents — задача верификации против latest-сборки pricing зелёная для старых взаимодействий, а новое всё ещё помечено pending, поэтому ни у кого сборка не красная. Пайплайн Прии весь зелёный, так что он деплоит checkout-web в прод. Через сорок секунд частота ошибок ползёт вверх: инстанс pricing, реально работающий в проде, на три версии позади latest у pricing, и он не возвращает discount_cents. Pact был проверен — просто не против той версии, что стоит там. Контрактное тестирование сделало своё дело, а деплой всё равно сломал прод, потому что гейт задавал не тот вопрос.
Зелёный pact — это попарный факт, а не факт безопасности деплоя
Урок 03 оставил тебя с брокером, полным результатов верификации. Каждый результат — точное, узкое утверждение: pact версии X консьюмера был проверен против версии Y провайдера, и он прошёл. Это действительно полезно, но отвечает на вопрос, который на момент деплоя никто на самом деле не задаёт. Вопрос на момент деплоя такой: если я выкачу версию X консьюмера в продакшен прямо сейчас, сработает ли она против версии провайдера, которая физически работает в продакшене? Верификация, что у тебя есть, — для некой версии Y, обычно latest CI-сборки провайдера, которая может быть на несколько деплоев впереди того, что стоит в проде.
Это и есть зазор, в который проваливается Hook. Pact был проверен против новейшей сборки pricing, в которой уже было discount_cents. Но в проде работал более старый pricing, а отдельные результаты верификации брокера ни словом не говорили о том, какая версия провайдера была где задеплоена. Куча зелёных галочек говорит тебе, какие пары версий тестировались вместе; она не говорит, является ли конкретная пара, которую ты вот-вот создашь в продакшене, — новый консьюмер, старый провайдер — одной из зелёных. Тебе нужен инструмент, который скрещивает твою версию с реальными обитателями целевого окружения, и этот инструмент — can-i-deploy.
Matrix: каждая протестированная пара и что где задеплоено
Брокер собирает всё это в matrix — таблицу каждой версии консьюмера × версии провайдера, которые когда-либо тестировались друг против друга, с результатом pass/fail каждого спаривания. В неё поступают две вещи. Во-первых, результаты верификации: когда pact версии X консьюмера проверяется версией Y провайдера, брокер записывает ячейку (X, Y) → pass/fail. Во-вторых, записи о деплоях: когда ты реально деплоишь версию в окружение, ты сообщаешь брокеру, так что он знает, что в проде сейчас стоит, скажем, pricing v56. Затем can-i-deploy делает один запрос: взять версию, которую я хочу задеплоить, найти версии каждого интегрированного приложения, что сейчас стоят в целевом окружении, и проверить, что каждая требуемая ячейка между ними зелёная.
| версия checkout-web | версия pricing | Проверено? | can-i-deploy в прод (pricing=v56)? |
|---|---|---|---|
| v22 (a1b2c3d) | v56 (задеплоено в проде) | pass | Да — пара зелёная, exit 0 |
| v23 (e4f5a6b) | v56 (задеплоено в проде) | pass | Да — пара зелёная, exit 0 |
| v24 (7c8d9e0) — добавляет discount_cents | v56 (задеплоено в проде) | нет результата | Нет — пара не проверена, exit 1, деплой заблокирован |
| v24 (7c8d9e0) — добавляет discount_cents | v59 (latest CI, не задеплоено) | pass | Неважно — v59 не в проде |
Последние две строки — это весь урок. checkout-web v24 проверена — против pricing v59, сборки, в которой уже есть discount_cents. Но v59 не задеплоена; в проде по-прежнему работает v56. can-i-deploy --pacticipant checkout-web --version 7c8d9e0 --to-environment production полностью игнорирует результат для v59, не находит зелёной ячейки для (v24, v56), завершается с ненулевым кодом и останавливает пайплайн. Тот самый деплой, что вызвал пейджер Прии в Hook, — это деплой, в котором этот гейт отказывает.
Версии должны быть git SHA, а деплои должны записываться
Matrix работает, только если правильны две вещи учёта. Во-первых, номер версии. Сильная рекомендация Pact: версия pacticipant есть git commit SHA или содержит его (напр. 0.0.10+76a39e5). Причина механическая: версия должна меняться ровно тогда, когда мог измениться pact. Если ты версионируешь по semver из package.json, две разные сборки с разными контрактами могут делить версию 2.3.0, и брокер с радостью переиспользует устаревший результат верификации для не того кода. Git SHA меняется на каждом коммите, известен на момент деплоя и позволяет постить статус верификации обратно как статус коммита. Фичеветки автоматически получают отдельные версии от main. SHA — единственный идентификатор, гарантированно отслеживающий контракт.
Во-вторых, отслеживание деплоев. Брокер не может знать, что стоит в проде, если ты не скажешь ему в конце каждого деплоя через pact-broker record-deployment --pacticipant checkout-web --version 7c8d9e0 --environment production. Есть две разновидности. record-deployment — для того, что деплоится в известный инстанс: сервисы, консьюмеры, API; он автоматически помечает предыдущую версию в этом окружении как более не задеплоенную, потому что одновременно работает лишь одна версия. record-release — для артефактов, которые ты публикуешь, но не заменяешь: мобильные приложения в сторе, библиотеки в реестре, где много выпущенных версий сосуществуют, так что он не снимает с деплоя предыдущую. (Старые конфигурации использовали для той же цели environment-теги вроде prod; теги всё ещё работают, а брокеры начиная с 2.81.0 автоконвертируют тег с именем окружения в деплой, но deployed/released-версии — это модель на будущее.)
Почему это работает
Почему вызывать record-deployment после успешного деплоя, а не до? Потому что запись брокера призвана отражать физическую реальность — что реально обслуживает трафик, — чтобы can-i-deploy следующей команды получил правдивый ответ. Если ты запишешь деплой до того, как раскатка завершится, а раскатка затем провалится, брокер теперь считает, что в проде работает версия, которой там нет, и чей-то гейт даст зелёный свет деплою против провайдера, которого ещё не существует. Записывай это в самом конце, когда нет шанса на провал и старая версия полностью слита. Matrix честна ровно настолько, насколько честны записи о деплоях, которыми ты её питаешь; ложь о том, что задеплоено, побеждает гейт так же верно, как и его пропуск.
Независимая деплоябельность — весь выигрыш
Отступи на шаг и заметь, что это даёт. Без гейта на «безопасно ли деплоить?» можно ответить, лишь деплоя всё вместе синхронно или поднимая весь мир в общем окружении — ровно та проблема N², что убил первый урок. С гейтом каждый сервис отвечает на вопрос за себя, локально, против записанной правды каждого окружения: я могу задеплоить свою новую версию тогда и только тогда, когда у каждого приложения, уже стоящего в целевом env, есть со мной зелёный pact. Это и есть независимая деплоябельность — способность выкатывать один сервис по своему графику без координации связки релизов, — и это вся причина, ради которой существует юнит. Контрактные тесты ловят поломку; can-i-deploy — то, что позволяет тебе безопасно действовать по этому знанию, по одному сервису за раз.
Режим отказа, когда ты это пропускаешь, — ровно Hook: консьюмер, чьи новые ожидания прод-провайдер никогда не удовлетворял, проходит сквозь CI (его pact проверен против latest-версии провайдера, а не задеплоенной) и ломается в рантайме — заново внося ровно тот продакшен-инцидент, который контрактное тестирование должно было предотвратить. Верификация была реальной; решение о деплое её проигнорировало. Задача гейта — заставить решение о деплое использовать matrix вместо «на ощупь».
Упорядочьте шаги, которые делают can-i-deploy надёжным гейтом деплоя:
- 1 CI консьюмера версионирует pact по git SHA и публикует его в брокер
- 2 CI провайдера верифицирует pact, и брокер записывает ячейку (консьюмер, провайдер) как pass/fail
- 3 Перед деплоем пайплайн запускает can-i-deploy --version SHA --to-environment production
- 4 Брокер проверяет matrix для этой версии консьюмера против версии провайдера, сейчас стоящей в проде
- 5 При exit 0 деплой продолжается; затем пайплайн вызывает record-deployment, чтобы гейт следующей команды видел правду
Pact консьюмера проверен зелёным против latest CI-сборки провайдера. Почему этого недостаточно, чтобы безопасно задеплоить консьюмер?
Почему Pact рекомендует, чтобы версия pacticipant была (или содержала) git commit SHA, а не semver вроде 2.3.0?
Ваш провайдер верифицирует pact-ы в CI, но никто не запускает can-i-deploy и никто не записывает деплои. Консьюмер добавляет поле, которое прод-провайдер ещё не возвращает. Как закрыть зазор?
- 01Пройдитесь точно по тому, что проверяет can-i-deploy, и почему зелёная верификация против latest-сборки провайдера недостаточна, чтобы задеплоить консьюмер.
- 02Почему версионировать pact-ы по git SHA, и в чём разница между record-deployment и record-release? Почему вызывать их в конце раскатки?
После верификации провайдером у тебя есть брокер, полный зелёных результатов, но каждый из них — попарный факт: версия X консьюмера согласуется с версией Y провайдера — и это не вопрос, который задаёт момент деплоя. Момент деплоя спрашивает, безопасна ли твоя версия против версии провайдера, физически работающей в целевом окружении, которая может быть на несколько деплоев позади latest CI-сборки, против которой был проверен pact. can-i-deploy закрывает этот зазор: он читает matrix брокера (каждую протестированную пару консьюмер×провайдер плюс что где задеплоено), скрещивает версию, которую ты выкатываешь, с версиями, сейчас стоящими в целевом env, и завершается с ненулевым кодом, если какая-либо требуемая пара не проверена, блокируя деплой. Чтобы гейт был надёжным, должны быть правильны две записи: версионируй pact-ы по git SHA, чтобы версия менялась всякий раз, когда мог измениться контракт, и устаревший результат никогда не переиспользовался, и записывай, что задеплоено — record-deployment для сервисов (который снимает с деплоя предыдущую версию) и record-release для стор-артефактов (который нет) — вызываемые в конце каждой раскатки, чтобы брокер отражал реальность. Выигрыш — независимая деплоябельность: каждый сервис отвечает на свой вопрос безопасности деплоя за секунды из записанных фактов, без общего окружения и без синхронного релиза. Пропусти гейт — и ты выкатишь консьюмер, чьи новые ожидания задеплоенный провайдер никогда не верифицировал — зелёный пайплайн, сломанный прод — воссоздавая ровно тот инцидент, который контрактное тестирование было построено предотвратить.