Миграции: как переводить десятки команд и не сорвать поставку
Платформенная команда выбрала новый рантайм. Причины настоящие: старый стек держится на трёх самописных скриптах, у одного из них нет автора уже год, апгрейд базового образа занимает две недели ручной работы. Посчитали, что новый вариант снимет с платформы четверть человека постоянной поддержки, а командам даст выкат за четыре минуты вместо девятнадцати. Написали внятный документ, согласовали с руководством, разослали письмо: «до конца квартала всем перевести свои сервисы, инструкция по ссылке».
Через квартал переведено 38 % сервисов. Через пять месяцев — 88 %, и кривая встала. Через год живы оба рантайма: в старом остались четырнадцать сервисов, из них четыре — критичные платёжные, три принадлежат команде, которая весь год выпускает новый продукт и физически не имеет свободного спринта, два не принадлежат никому (авторы уволились), а про остальные пять никто не может сказать, работают они вообще или просто крутятся. Платформа год держит две дежурки, два конвейера и два счёта в облаке. Экономия, ради которой всё затевалось, не наступила ни на день: она наступает только в момент выключения старого, а старое не выключено.
Параллельно случилось то, о чём в отчётах не пишут: две команды пропустили квартальную цель, потому что часть спринтов ушла на перевод; одна команда, устав ждать ответа платформы про хитрый сайдкар, сделала свой деплой мимо платформы — и с тех пор живёт вне неё; а в опросе про инструменты появилась формулировка «платформа — это то, что раз в полгода отбирает у нас две недели».
Это не история про плохую команду или ленивых пользователей. Это история про неверную модель: миграцию считали технической задачей, у которой есть инструкция и срок. На самом деле миграция — это продуктовое изменение с одним неприятным свойством:
Миграция — это релиз платформы, выгоду от которого получает компания или сама платформа, а работу делают чужие руки.
Вся дисциплина этой главы вырастает из этой асимметрии. Если вы её не видите и не оплачиваете, миграция превращается в налог: платформа как продукт держится на добровольном использовании, а налог добровольным не бывает — его обходят.
Асимметрия выгоды и работы
Разложим на четыре типовых случая. Столбец «кто платит трудом» — это и есть счёт, который вы выставляете, часто не осознавая этого.
| Что мигрируем | Кто получает выгоду | Кто платит трудом | Что видит инженер продуктовой команды |
|---|---|---|---|
| Версия базового образа с уязвимостью | компания (риск), безопасность | продуктовые команды | «опять срочная ерунда, у нас релиз» |
| Смена рантайма или CI | платформа (меньше поддержки), компания (скорость потом) | продуктовые команды | «нам было нормально» |
| Новый формат манифеста после рефакторинга платформы | платформа | продуктовые команды | «вы поменяли, мы чиним» |
| Переезд в другое облако по решению бизнеса | бизнес | все | «нас не спрашивали» |
Отсюда — правило, вокруг которого строится всё остальное:
Кто получает выгоду, тот и платит трудом. Если выгода у платформы, работу делает платформа. Если выгода общая — работу делят, но автоматическую часть всё равно берёт на себя тот, кто ломает.
Это не благотворительность, а экономика. Час инженера продуктовой команды стоит столько же, сколько час инженера платформы, но с двумя надбавками: он отнимается от поставки (то есть от того, ради чего компания существует) и тратится в сорока местах на одну и ту же задачу — с сорока разными ошибками. Ровно этим правилом отличается платформа-продукт от платформы-налога — см. «Инфраструктура как API», где то же правило сформулировано для смены версии схемы.
Единственная честная метрика миграции
Прогресс миграции почти всегда меряют долей переведённых сервисов. Это удобная метрика для отчёта и почти бесполезная для принятия решений, потому что она не связана с деньгами. Связь с деньгами простая и жёсткая:
Экономия от миграции равна нулю до момента выключения старого пути. Пока живы оба, вы платите за оба — и дополнительно платите за переключение между ними.
Картинка описывает почти любую массовую миграцию: быстрый разгон, 85–90 % за первую треть срока, и длинный хвост, который стоит столько же, сколько весь разгон. Красная полоса внизу — это окно двойной стоимости: пока оно открыто, платформа несёт обе цены и не получает ни одной выгоды. Задача менеджера миграции — не «поднять процент», а закрыть это окно как можно раньше, и иногда самый быстрый способ закрыть его — вообще не мигрировать хвост (об этом ниже).
Отсюда набор метрик, который стоит вести с первого дня; не путайте его с метриками принятия платформы вообще (про них — в главе о developer experience). У первой строки таблицы есть приятное свойство: её нельзя нарисовать — либо старый кластер удалён и счёт за него исчез, либо нет.
| Метрика | Как считать | Порог тревоги |
|---|---|---|
| Доля выключенного старого | сервисы, где старый путь физически отключён | не растёт две недели подряд |
| Скорость волны | сервисов в неделю за последние 4 недели | падение вдвое от пика |
| Прогноз завершения | остаток / скорость, отдельно по хвосту | выходит за объявленную дату выключения |
| Доля PR от бота, слитых без правок | слитые без правок / все от бота | ниже 80 % — кодмод плохой |
| Хвост без разобранной причины | сервисы в хвосте без записанной причины | больше 20 % хвоста |
| Влияние на поставку | lead time и частота выката когорты до/во время/после | рост lead time более 20 % к базовой линии |
| Исключения без даты пересмотра | по реестру исключений | любое ненулевое значение |
Считаем до старта: три числа
Миграция — это инвестиция с длинным горизонтом. Прежде чем объявлять её, посчитайте три числа. Расчёт занимает полдня и регулярно заканчивается решением «не мигрируем».
Возьмём компанию из примера: 24 продуктовые команды, 140 сервисов.
1) Цена перевода (чужое время)
T_ручной_медиана = 6 ч на сервис
k_переключение = 1,6 (работа идёт кусками между задачами)
N_обычных = 125 сервисов
N_сложных = 15 сервисов × 3 дня = 45 дней
→ 125 × 6 × 1,6 = 1200 ч ≈ 150 дней + 45 = 195 человеко-дней
≈ 0,9 человеко-года времени продуктовых команд
2) Цена платформы
кодмод, бот пул-реквестов, документация, песочница = 60 дней
дежурство по миграции 0,3 FTE × 12 мес = 72 дня
→ 132 человеко-дня
3) Цена окна двойной стоимости
0,4 FTE платформы на поддержку старого × 12,5 мес = 105 дней
инфраструктура старого стека 1400 USD/мес × 12,5 ≈ 17 500 USD
Выгода после выключения:
платформа перестаёт поддерживать старое = 0,25 FTE
команды перестают обходить старое, 2 ч в неделю × 24 ≈ 1,2 FTE
→ ~1,45 FTE в год
Дальше арифметика неприятная, но полезная. Вложено примерно 195 + 132 + 105 ≈ 430 человеко-дней, то есть около двух человеко-лет. Возврат — 1,45 FTE в год, но он начинается после выключения, то есть с тринадцатого месяца. Полный возврат вложений наступает примерно через 25–27 месяцев от старта. Это нормальная миграция: она окупается на горизонте двух лет, а не квартала.
Три вывода, которые из этого следуют.
Первый: горизонт решения — два года, а не квартал. Если вы всерьёз допускаете, что через полтора года смените этот рантайм ещё раз, миграция не окупится никогда. «Скучная технология» здесь не поза, а способ не платить эту цену дважды (Choose Boring Technology).
Второй: длительность окна двойной стоимости — главный рычаг. Сократить окно с 12,5 до 7 месяцев дешевле и полезнее, чем сэкономить на кодмоде. Это классическая точка воздействия: вы не ускоряете отдельных исполнителей, вы меняете структуру задержки в системе.
Третий: если считать нечего, миграции нет. Расчёт вида «новый стек современнее» — не расчёт. Формулировка «мы уже начали, надо доделать» — это невозвратные издержки, а не аргумент. Соотнесите с главой про стоимость: миграция — самая дорогая строка платформенного бюджета, и почти всегда невидимая, потому что расход лежит в чужих трекерах.
Классы миграций: стратегия зависит от класса
Одинаково мигрировать версию базового образа и схему данных — верный способ провалить и то и другое. Классов немного, и они различаются по двум свойствам: возможна ли параллельная работа старого и нового, и обратимо ли изменение.
Практическая ценность классификации в том, что она сразу задаёт три вещи: нужен ли параллельный период, какой должен быть откат и сколько времени вы обязаны дать. Для совместимой миграции честный срок — недели, для миграции с состоянием — кварталы. Требовать переезда базы «до конца месяца» — это не амбициозность, а непонимание класса задачи (про выбор и переезд хранилищ).
Отдельно про принудительный класс. Уязвимость с активной эксплуатацией или конец поддержки версии — единственный случай, когда «надо» не обсуждается. Но и здесь обсуждается способ: даже в этом случае рассылка «всем обновиться за неделю» проигрывает сорока автоматическим пул-реквестам, потому что за неделю сорок команд физически не найдут сорок окон в спринтах (безопасность по умолчанию).
Мигрирует платформа, а не сорок команд
Главный технический приём массовой миграции — кодмод: программа, которая переписывает код и конфигурацию за пользователя. Классическая формулировка практики принадлежит инженерии Google: масштабные изменения (large-scale changes) там выполняет команда, которая инициировала изменение, а не тысячи владельцев кода (Software Engineering at Google, глава 22). Там же сформулировано «правило Бейонсе»: если поведение важно, оно должно быть покрыто тестом в общем прогоне — иначе оно сломается при массовой правке и это будет считаться нормой.
платформа платит за свою же выгоду
Три детали отличают работающий кодмод от рассылки с прикреплённым скриптом.
PR приходит зелёным или не приходит вовсе. Красный PR от бота — это переложенная отладка: инженер тратит на разбор больше, чем потратил бы на ручной перевод, и в следующий раз просто закроет письмо от бота не читая. Если проверка падает — это ваша задача, а не его.
Когорты, а не залп. Первая партия — 5–7 сервисов, желательно ваших собственных и добровольцев. На них ловится 80 % дефектов кодмода. Массовый заезд начинается только после того, как когорта прошла без ручных правок.
У PR есть срок жизни, владелец и откат в описании. Пул-реквест, висящий три месяца, — не прогресс, а мусор в чужом репозитории: не слито за две недели — бот пишет владельцу, за месяц — случай уходит в разбор хвоста. И одна строка «вернуть — revert этого коммита, старый путь работает до 1 марта» стоит ноль и снимает половину тревоги.
Инструменты для кодмодов существуют и не бесплатны: OpenRewrite для JVM, jscodeshift для JS/TS, comby для структурных правок почти любого синтаксиса, gofmt -r и go fix в Go, Sourcegraph Batch Changes и Renovate для массовой рассылки правок и обновления версий. Цена владения у каждого честная: первый рабочий рецепт обычно стоит от двух до шести недель, и он всегда спотыкается о чужие «особенные» файлы.
Когда кодмод невозможен
Не всё автоматизируется: если для перевода нужно принять решение (какой класс ресурса выбрать, что делать с самописным сайдкаром, куда девать локальный кэш), кодмод не поможет. Стратегия здесь другая — сократить число решений до одного.
Плохо: «прочитайте гайд на 12 страниц и решите, как вам мигрировать». Хорошо: бот открывает PR с уже выбранным разумным значением по умолчанию и одной строкой в описании: «мы поставили tier: standard, если у вас критичный сервис — поменяйте на critical и слейте; вопросы — в канал, отвечаем за рабочий день». Инженер принимает одно решение за минуту вместо пятнадцати решений за день. Это ровно та работа по снижению когнитивной нагрузки, о которой говорят Team Topologies.
Жизненный цикл сервиса в миграции
Считать миграцию по командам бесполезно: команда с одним сервисом и команда с двадцатью — это разные объёмы. Считайте по сервисам и держите у каждого явное состояние. Полезно ровно то же, что и в платформенном API: состояние должно быть выведено из реальности, а не проставлено человеком в табличке.
Два состояния здесь важнее остальных. Проверен отделяет «слито» от «работает»: без него вы обнаружите на выключении, что двадцать сервисов формально мигрировали, но продолжают ходить в старый эндпоинт. Старое_выключено — то самое, по которому считаются деньги.
И заметьте Кандидат_на_снос. В каждой миграции обнаруживается 5–10 % сервисов, которые не нужно мигрировать — их нужно выключить. Это чистая экономия и самый приятный побочный эффект любой массовой инвентаризации.
Волны: пилот, когорты, хвост
План миграции — не таблица «кто когда», а последовательность волн с явными воротами между ними. Ворота означают: следующая волна не начинается, пока предыдущая не показала нужный результат.
Три вещи обязаны быть в этом плане.
Пилот на своих сервисах. Платформа мигрирует себя первой. Это не ритуал доверия, а способ найти дефекты до того, как их найдут двадцать четыре команды одновременно. Если платформа не готова мигрировать собственные сервисы, миграция не готова.
Явная заморозка. В любой компании есть даты, когда трогать поставку нельзя: пик продаж, отчётный период, крупный запуск. Эти даты должны быть в плане миграции с самого начала, а не появляться как сюрприз в ноябре. Заморозка удлиняет миграцию — и она же спасает её от отмены после первого сорванного бизнес-срока.
Тёмный день (он же brownout) — репетиция выключения: старый путь отключается на несколько часов в заранее объявленный день. Всё, что сломалось, — это ваш реальный хвост, а не тот, что в табличке. Практика пришла из объявления устаревания публичных API и работает внутри компании ровно так же. Первый тёмный день делайте коротким и в рабочее время, с дежурным наготове, — соотнесите с практиками безопасного релиза.
Хвост: почему последние 12 % стоят половины бюджета
Хвост — не следствие лени. Это набор нескольких разных причин, каждая со своим лечением. Ошибка менеджера миграции — считать хвост однородным и давить на него одним способом (обычно эскалацией). Лечение начинается с классификации: возьмите оставшиеся сервисы и разложите по причинам, честно, поимённо.
| Причина в хвосте | Доля (типично) | Что делать | Чего не делать |
|---|---|---|---|
| Сервис фактически мёртв | 20–30 % | проверить трафик, выключить, не мигрировать | мигрировать «на всякий случай» |
| Нет владельца | 10–20 % | найти по коммитам и алертам; не нашли — платформа берёт и выключает по регламенту | оставлять «до выяснения» |
| Технический особый случай | 20–30 % | платформа переводит руками или расширяет кодмод | требовать от команды разобраться самой |
| Команда в пожаре или релизе | 20–30 % | согласовать новую дату письменно, поставить в план квартала | эскалация к руководству как первый ход |
| Команда не согласна с миграцией | 5–15 % | разговор о ценности; если правы — исключение или отмена миграции | продавливать мандатом |
Последняя строка — самая ценная. Несогласие команды почти всегда содержит информацию: либо новый путь действительно хуже для их случая, либо выгода реальна, но неочевидна, либо вы просите их оплатить чужую выгоду. Все три случая лечатся разговором и расчётом, и ни один — приказом. Мандат даёт формальные 100 % и уводит недовольство в теневые обходы, которые вы обнаружите через год (про принятие и обходы).
Отдельная техника против хвоста — обратный отсчёт с автоматикой. За два месяца до выключения бот начинает добавлять в старый путь предупреждение в логи и в вывод конвейера, за месяц — сообщение при каждом выкате, за неделю — обязательное подтверждение. Не «письмо в рассылку», а сигнал ровно в тот момент, когда человек работает с этим кодом.
# Шаг в старом конвейере: предупреждение появляется там, где его увидят,
# а не в почте. Дата, владелец и готовый PR — обязательны.
DEADLINE="2027-02-01"
LEFT=$(( ( $(date -d "$DEADLINE" +%s) - $(date +%s) ) / 86400 ))
echo "::warning::Старый рантайм отключается $DEADLINE (осталось $LEFT дн.)."
echo " Сервис: $SERVICE Владелец: $OWNER Готовый PR: $MIGRATION_PR_URL"
echo " Не подходит? Заявка на исключение: https://wiki/migrations/exception"
# За две недели до срока — объявленная заранее задержка (brownout),
# чтобы срок стал осязаемым. Она не растёт и не появляется внезапно.
if [ "$LEFT" -le 14 ]; then
echo "Старый путь работает с задержкой 120 с. Это последнее предупреждение."
sleep 120
fi
Приём работает, но у него есть предел: он ускоряет тех, кто просто откладывал, и никак не помогает тем, у кого техническая причина. Не используйте задержки как замену разбору хвоста и никогда не включайте их без объявленной даты — иначе это выглядит как саботаж собственных пользователей.
Как не сорвать поставку
Миграция конкурирует с продуктовой работой за одни и те же спринты. Если этой конкуренцией не управлять, ею начнут управлять сами команды — в вашу пользу они это не сделают. Рабочий контракт состоит из шести пунктов, и его стоит записать явно, один раз, для всех миграций.
- Одна обязательная миграция на команду одновременно. Это требует общего реестра миграций и человека, который держит очередь. Две одновременные обязательные миграции — гарантированный срыв обеих.
- Бюджет прерываний: не более 10 % ёмкости команды в квартал на платформенные требования. Всё, что не влезло, переносится в следующий квартал вместе с датой выключения старого.
- Предупреждение до планирования, а не после. Дата и объём приходят за 4–6 недель до начала квартала, чтобы попасть в планирование, а не в середину спринта.
- Ворота качества по метрикам поставки. Если у мигрировавшей когорты p50 lead time вырос больше чем на 20 % или частота выката упала — волна останавливается до выяснения причин. Это тот же механизм, что и бюджет ошибок в SRE, только защищаемая величина — скорость поставки (метрики DORA).
- Откат — обязанность платформы. Если после перевода у команды посыпались инциденты, чинит платформа, а при необходимости возвращает на старый путь. Команда, которую бросили с чужой поломкой, не мигрирует добровольно больше никогда.
- Дежурство по миграции. Отдельный человек в неделю, канал с ответом в течение рабочего дня, регулярные часы поддержки. Без этого весь план держится на письме, а письмо не отвечает на вопросы.
Мандат сверху иногда нужен — например, когда миграция принудительная и связана с риском, — но он оплачивает только приоритет, а не работу: приказ не пишет кодмод и не разбирает хвост. Мандат без автоматизации и бюджета времени даёт формальные отчёты о переводе при фактически работающем старом пути.
Обратимость: параллельный запуск и переключение трафика
Для миграций с состоянием и смены рантайма «слить PR и посмотреть» не работает: ошибка обнаруживается на проде и на живых данных. Здесь применяют параллельный запуск.
доля трафика на новый путь"} B -->|"100 %"| C["Старый путь → ответ пользователю"] B -->|"тень 0 %"| D["Новый путь: только чтение,
ответ отбрасывается"] D --> F["Сравнение результатов
и задержек"] F --> G{"Расхождения
ниже порога?"} G -->|"нет"| H["Чиним новый путь.
Пользователь ничего не заметил"] H --> D G -->|"да"| I["Канареечное переключение:
1 % → 10 % → 50 % → 100 %"] I --> J{"SLO держатся?"} J -->|"нет"| K["Откат за одну команду.
Старый путь ещё живой"] K --> D J -->|"да"| L["Старый путь в режиме ожидания
оговорённый срок"] L --> M["Выключение старого:
окно двойной стоимости закрыто"]
Ключевая деталь — теневой режим: новый путь получает копию реальных запросов, но его ответ не показывается пользователю. Расхождения ловятся до того, как кто-то пострадал. Классическая реализация идеи — библиотека Scientist от GitHub, но своя версия на 100 строк пишется за день; ценность не в библиотеке, а в дисциплине сравнивать, а не верить.
Теневой трафик — это побочные эффекты. Если новый путь пишет в базу, шлёт письма или списывает деньги, теневой режим превращается в двойные операции. Тень безопасна только для чтения; для записи нужна отдельная песочница или явная защита на уровне зависимостей.
Двойная запись — это распределённая транзакция без транзакции. Пока пишутся два хранилища, у вас есть окно рассинхронизации, и «сверим потом» превращается в отдельный проект. Планируйте сверку и починку расхождений как часть миграции, а не как надежду (модели согласованности).
Откат имеет срок годности. Через две недели двойной записи вернуться назад уже нельзя: в новом хранилище появились данные, которых нет в старом. Дата, после которой откат невозможен, должна быть объявлена заранее и записана в плане — «мы больше не можем откатиться начиная с 15 марта» — это важнейшая строка документа.
Для схем данных используется приём «расширяй и сжимай» (expand/contract, он же parallel change, Martin Fowler): сначала добавляем новое, не ломая старое; потом переводим читателей; потом переводим писателей; и только затем удаляем старое. Четыре шага, каждый — отдельный релиз, между ними живут обе формы. Стратегия постепенного вытеснения большого куска системы описана там же как strangler fig и годится для смены рантайма целиком.
Учёт: миграция как объект платформы, а не письмо
Если миграция существует только в виде письма и таблицы, она умрёт вместе с вниманием её автора. Опишите её как объект — так же, как вы описываете сервисы в каталоге и API платформы.
# migrations/runtime-v3.yaml — миграция как декларация, а не как рассылка
apiVersion: platform/v1
kind: Migration
metadata:
name: runtime-v3
owner: platform-team
spec:
reason: "Старый рантайм держится на неподдерживаемых скриптах; апгрейд образа — 2 недели ручной работы"
benefit:
platform_fte_saved: 0.25 # что платформа перестанет делать
cost:
team_days_estimate: 195 # чужое время — считаем явно
platform_days_estimate: 132
double_run_months: 12 # окно двойной стоимости
class: runtime-change # определяет требования к откату и срокам
automation:
codemod: recipes/runtime-v3
green_checks_required: true # красный PR к команде не уходит
budget:
max_team_capacity_percent: 10
freeze_windows:
- { from: 2026-11-01, to: 2026-12-15, reason: "пик продаж" }
dates:
announced: 2026-01-12
waves_start: 2026-02-23
dark_day: 2027-01-18 # репетиция выключения
old_path_off: 2027-02-01 # дата, после которой экономия существует
exceptions:
require: [owner, reason, review_date]
max_duration_days: 180 # бессрочных исключений не бывает
rollback:
supported_until: 2026-12-01 # после этой даты откат невозможен
Такой файл делает три вещи, которых не делает письмо. Он фиксирует обещанную выгоду — через год можно проверить, случилась ли она. Он делает цену видимой — 195 чужих человеко-дней трудно проигнорировать при обсуждении. И он задаёт дату выключения как обязательство платформы, а не как пожелание.
Отчётность по нему считается автоматически. Минимальный скрипт, который живёт годами:
"""Отчёт по миграции: состояние, скорость, прогноз и разбор хвоста.
Источник истины — реальность (реестр, факт выката, статус PR), а не таблица.
Сложность: O(n log n) по числу сервисов из-за сортировки в отчёте, память O(n).
"""
from collections import Counter
from dataclasses import dataclass
from datetime import date, timedelta
ГОТОВЫЕ = {"Проверен", "Старое_выключено"}
@dataclass(frozen=True)
class Service:
name: str
state: str # состояния из диаграммы жизненного цикла
tail_reason: str | None # "мёртв" | "без владельца" | "особый случай" | ...
changed_at: date # когда состояние менялось последний раз
def report(services: list[Service], today: date) -> dict:
total = len(services)
by_state = Counter(s.state for s in services)
done = sum(by_state[s] for s in ГОТОВЫЕ)
# Скорость — по факту за 4 недели, а не по обещаниям из плана.
window = today - timedelta(days=28)
per_week = sum(1 for s in services
if s.state in ГОТОВЫЕ and s.changed_at >= window) / 4
# Хвост: всё, что стоит на месте дольше трёх недель.
stuck = [s for s in services
if s.state not in ГОТОВЫЕ and (today - s.changed_at).days > 21]
tail = Counter(s.tail_reason or "не разобрано" for s in stuck)
return {
# Деньги считаем только по выключенному старому пути.
"процент_выключенного_старого": round(100 * by_state["Старое_выключено"] / total, 1),
"процент_готовых": round(100 * done / total, 1),
"скорость_в_неделю": round(per_week, 1),
"прогноз_недель": round((total - done) / per_week, 1) if per_week else "не сходится",
"хвост": tail.most_common(),
"хвост_без_разбора": tail["не разобрано"], # главный дефект управления
}
Строка хвост_без_разбора — самая полезная в отчёте. Пока у застрявшего сервиса нет причины, у миграции нет плана; «мы напоминаем» планом не является.
Стратегии: перевести, дать вымереть, заморозить
Принудительный перевод всех — не единственный вариант и часто не лучший. Есть три стратегии, и выбор между ними — продуктовое решение.
Перевести. Старое активно мешает: стоит денег, создаёт риск, требует поддержки. Окно двойной стоимости закрывается как можно быстрее, миграция обязательна для всех.
Дать вымереть. Новые сервисы создаются только на новом пути; старые доживают свой срок и уходят вместе с сервисами. Стоит дёшево, не отнимает у команд ни часа, но работает только при двух условиях: старый путь не создаёт риска и не требует активной разработки, а сервисы действительно имеют конечный срок жизни. Иначе вы получите вечное окно двойной стоимости — худший из вариантов.
Заморозить. Старый путь остаётся, но возможностей в него не добавляют, а бюджет фиксируют. Разумно, когда миграция не окупается на горизонте двух лет, а старое пока не опасно.
Правый нижний угол — самое полезное место на этой диаграмме. Там лежат миграции, которые кажутся обязательными («ну не может же легаси остаться на старой базе»), но не окупаются никогда. Решение «не мигрировать и записать почему» — это полноценное инженерное решение; оформите его как архитектурное решение с контекстом, чтобы через год к нему не вернулись случайно.
Команда, которой миграция не подходит
Это тот же вопрос, что и с золотым путём: путь помогает, пока он путь, а не забор. В миграции забор выглядит как «переводите или отключим». Иногда это законная позиция (принудительный класс), но чаще — признак того, что вы не разобрали случай.
Рабочая процедура состоит из четырёх шагов.
- Выслушать и записать техническую причину. Не «они не хотят», а «сервис использует локальный диск для 400 ГБ кэша, на новом рантайме его нет». Формулировка причины — уже половина решения.
- Проверить, сколько ещё таких. Один случай — исключение. Три случая — дыра в новом пути, и чинить надо путь, а не команду. Это ровно тот сигнал, который глава про абстракции описывает как «одна абстракция поверх трёх разных потребностей».
- Выдать исключение как объект, а не как молчание. У исключения есть владелец, причина, дата пересмотра и максимальный срок. Бессрочных исключений не бывает; пустой реестр исключений — не порядок, а слепота.
- Договориться о стоимости отказа. Команда остаётся на старом пути — значит, она берёт на себя его поддержку после даты выключения общей поддержки: дежурство, обновления, безопасность. Это не наказание, а честная передача цены владельцу решения.
Четвёртый пункт разрешает главный конфликт миграции. Платформа не может держать старый путь вечно ради одной команды — это налог на всех остальных. Но и выключить чужой прод она не имеет права. Передача владения превращает спор в выбор с понятной ценой: «мигрируйте за два дня либо берите поддержку на себя, вот её объём».
Типовые провалы
Письмо с дедлайном вместо кодмода. Самый частый и самый дорогой. Вы экономите свои шесть недель и тратите чужие двести человеко-дней, а получаете 88 % и вечный хвост. Плюс репутационный ущерб, который дороже: следующая ваша инициатива встретит сопротивление до того, как её прочитают.
Миграция без выгоды для пользователя. «Мы переехали на новый шаблон, теперь переезжайте вы». Если вся выгода у платформы, работу делает платформа — иначе вы просто перекладываете свою уборку на соседей и называете это стандартизацией.
Миграция без даты выключения. Отсутствие даты означает, что окно двойной стоимости открыто навсегда и экономии не будет никогда: дата объявляется одновременно с началом миграции, а не тогда, когда «почти все перешли». Соседний провал — большой взрыв, когда все переключаются в один день, а откат описан словами «ну, вернёмся как-нибудь»: хорошо выглядит на слайде и превращается в самый дорогой инцидент года.
Миграция в обёртку над облаком. Перевод сорока команд с прямого использования облака на свой тонкий слой, повторяющий облачный API один в один, — только с отставанием в поддержке новых возможностей и без документации в интернете. Цена платится, выгода отсутствует; проверка простая — сколько полей пользователь больше не заполняет (про обёртки). Тот же вид провала — миграция каталога в портал, которым не пользуются: «заполните карточки сервисов» стоит сотни часов чужого времени в обмен на данные, которые устареют за квартал, а каталог обязан выводиться из реальности автоматически.
Три потребности, склеенные в одну миграцию. «Раз уж переезжаем на новый рантайм, заодно переведём всех на новый формат логов, новый CI и новый способ конфигурации.» Каждая склейка умножает вероятность отката и делает невозможным ответ на вопрос «что именно сломалось»: одна миграция — одно изменение.
Миграция ради инструмента. «Все переезжают на Kubernetes» или «нам нужен Backstage» как исходная посылка, а не как вывод из расчёта. У обоих есть настоящая цена владения: кластер с апгрейдами — это 1–2 постоянных инженера, портал — половина-один инженер на сопровождение, и оба ничего не стоят без потока задач, который они обслуживают (цена владения — в главе про стоимость и в главе про инфраструктуру как API). Инструмент выбирают после того, как посчитали миграцию, а не вместо этого.
Забыли посчитать, что мигрируют не только сервисы. Дашборды, алерты, рантбуки, привычки дежурных, ссылки в постмортемах, права доступа. Перенос наблюдаемости между стеками стоит отдельного человеко-квартала на сотню сервисов (глава про наблюдаемость), а окружения и стенды переезжают отдельно от прода (глава про окружения).
Локальная оптимизация в миграциях
Полезно посмотреть на всё это глазами системного мышления. Платформенная команда оптимизирует свою метрику — «процент переведённых», продуктовая свою — «поставка в срок»; обе локально правы, а система теряет: миграция идёт медленно, старое живёт долго, обе стороны считают другую нерациональной. Лечится это не призывами, а изменением того, что измеряется. Как только метрикой платформы становится «доля выключенного старого» (а не «переведено»), у неё сразу появляются стимулы писать кодмоды, разбирать хвост поимённо и удалять мёртвые сервисы вместо их перевода. Как только у продуктовой команды появляется бюджет прерываний и защита поставки, миграция перестаёт быть угрозой квартальным целям. Это же задержка в системе: выгода отложена на год, боль наступает сейчас — значит, о выгоде нужно напоминать данными, иначе её никто не почувствует.
Мини-итог
- Миграция — это релиз платформы, работу по которому делают чужие руки. Асимметрия выгоды и труда — источник всех провалов; правило «кто получает выгоду, тот и платит трудом» разрешает большинство споров до их начала.
- Экономия равна нулю до выключения старого. Считайте долю выключенного, а не долю переведённых; главный рычаг — длительность окна двойной стоимости, а не скорость отдельных исполнителей.
- Считайте до старта. Чужие человеко-дни, свои человеко-дни, стоимость двойного периода и горизонт возврата. Двухлетняя окупаемость — норма; отсутствие расчёта — не норма.
- Мигрирует платформа. Кодмод, бот с зелёными PR, когорты, откат в описании PR. Там, где автоматика невозможна, сокращайте задачу до одного решения с разумным значением по умолчанию.
- Хвост неоднороден. Мёртвые сервисы выключают, бесхозные забирают, особые случаи делают руками, занятым командам двигают дату, несогласных слушают. Эскалация — последний ход, а не первый.
- Поставку защищают явным контрактом: одна обязательная миграция за раз, бюджет прерываний, окна заморозки, предупреждение до планирования, ворота по метрикам поставки, откат за счёт платформы, дежурство по миграции.
- «Не мигрировать» — полноценное решение. Дать вымереть или заморозить дешевле принудительного перевода, если старое не создаёт риска. И ни одна миграция не оправдывается тем, что целевой инструмент моден: у Kubernetes, портала и любого другого выбора есть постоянная цена владения, которую платит тот же бюджет.
Источники
- T. Winters, T. Manshreck, H. Wright. Software Engineering at Google, глава 22 «Large-Scale Changes» — кто выполняет массовые изменения и почему не владельцы кода; правило Бейонсе.
- M. Fowler. ParallelChange (expand/contract) и StranglerFigApplication — базовые схемы обратимых миграций.
- GitHub. Scientist — параллельный запуск со сравнением результатов. Kubernetes. Deprecation Policy — окна устаревания как обещание, а не как объявление.
- Инструменты кодмодов: OpenRewrite, jscodeshift, comby, Sourcegraph Batch Changes, Renovate.
- N. Forsgren, J. Humble, G. Kim. Accelerate, IT Revolution, 2018 и DORA Research 2024 — метрики поставки как защищаемая величина во время миграций.
- M. Skelton, M. Pais. Team Topologies, IT Revolution, 2019 — когнитивная нагрузка при массовых изменениях. D. McKinley. Choose Boring Technology, 2015 — почему частая смена стека убивает окупаемость любой миграции.
Что дальше
Мы разобрали самую конфликтную часть платформенной работы: как менять общее так, чтобы у сорока команд не сорвалась поставка, и как честно считать цену того, что вы просите сделать других. Почти каждый пункт этой главы упирается в один вопрос — а кто, собственно, всё это делает: пишет кодмоды, дежурит по миграции, разбирает хвост поимённо, ведёт переговоры об исключениях и говорит «нет» миграции, которая не окупается. Это работа платформенной команды, и у неё есть свой состав, свои границы и свой предел ёмкости, за которым любая инициатива начинает съедать поддержку.
Платформенная команда: состав, поддержка, дежурство и границы