Миграция с Docker Compose на Kubernetes с понятными правилами
Миграция с Docker Compose на Kubernetes проходит лучше, когда команда заранее определяет владельцев, правила деплоя и пути отката до разделения живых сред.

Почему всё быстро становится запутанным
Миграция с Docker Compose на Kubernetes начинает путаться в тот момент, когда оба рантайма остаются включёнными одновременно. Одна команда выкатывает исправление в Kubernetes, другая перезапускает старый стек Compose, и уже никто не понимает, какая версия на самом деле обслуживает пользователей. Обычно путаница начинается раньше, чем кто-то это замечает.
Главная проблема проста: один и тот же сервис оказывается запущен в двух местах. API может принимать публичный трафик в Kubernetes, а старая копия в Compose всё ещё выполняет cron-задачу, webhook или внутреннюю админскую операцию. Воркеры могут обработать одну и ту же очередь дважды. Плановые задания могут сработать дважды. Миграция базы данных может выполниться не там, где надо, и сломать сразу обе среды.
Общие зависимости делают ситуацию ещё хуже. Секреты меняются в одном рантайме, но не в другом. Одна команда обновляет переменные окружения в Compose, а другая правит ConfigMap или значения Helm в Kubernetes. Кто-то переводит приложение на новый экземпляр Redis, но старый контейнер всё ещё обращается к старому. Система выглядит «вроде бы рабочей» ровно до того момента, пока эти мелкие расхождения не превращаются в реальный сбой.
Происходит это быстро. Небольшая команда может сначала перенести публичное API в Kubernetes, оставить воркеры в Compose и использовать одну и ту же базу данных для обеих сред. Потом хотфикс попадает только в Kubernetes. API начинает записывать данные в новом формате, а старый воркер в Compose всё ещё ждёт старый формат. Задания падают, повторы накапливаются, и команда тратит часы на то, чтобы понять, проблема в коде, конфигурации или маршрутизации трафика.
Сбои обычно начинаются на границе разделения. Проверки здоровья смотрят на одну версию, а реальные пользователи попадают на другую. Логи лежат в двух местах. Откаты затягиваются, потому что никто не может договориться, какая среда на самом деле владеет сервисом.
У каждого сервиса во время разделения должен быть один понятный дом. Один рантайм владеет им. Один путь деплоя обновляет его. Один путь отката возвращает его назад. Если сервис какое-то время зависит от другой среды, считайте это временным исключением и обязательно зафиксируйте его.
Решите, кто владеет каждым сервисом
Большинство проблем при миграции связаны не с YAML. Они связаны с людьми.
Если две команды обе считают, что владеют одним сервисом, никто не решается принять жёсткое решение, когда деплой падает в шесть вечера. Совместное владение звучит дружелюбно, но во время разделения оно обычно превращается в ожидание, догадки и решения наполовину.
До того как что-то переносить, назначьте одного конкретного владельца для каждого сервиса. Достаточно простой таблицы владения. Для каждого сервиса укажите его название, одного прямого владельца, кто утверждает изменения конфигурации, кто ведёт реагирование на инциденты и кто может одобрить перенос между Compose и Kubernetes.
Правило должно быть жёстким: один сервис — один владелец. Остальные могут проверять и помогать, но решение принимает один человек. Это убирает обычную перепалку, когда сервису нужно сменить порт, обновить секрет или быстро откатиться.
Отдельной строкой нужно указать, кто утверждает конфигурацию, потому что этим часто занимается другой человек. Лидер backend-команды может владеть API, а операционный лидер — утверждать лимиты памяти, секреты и изменения ingress. Если это не записать, мелкие правки превращаются в бесконечное ожидание.
Не менее важна и ответственность за инцидент. Когда сервис падает, команда должна понимать, кто подключается первым, кто проверяет логи и метрики и кто может приостановить выкладку. В параллельных средах часть стека всё ещё работает в Compose, а другая часть — в Kubernetes. Кто-то всё равно должен отвечать за результат, даже если корень проблемы находится с другой стороны.
Сам перенос тоже нуждается в ответственном человеке. Не позволяйте кому-либо переводить сервис только потому, что манифест выглядит готовым. Тот, кто утверждает перенос, должен проверить зависимости, пути трафика, секреты, health checks и шаги отката.
Небольшая команда может сделать это без лишней бюрократии. Например, стартап может назначить владельцем payment API backend-лида, конфигурацию базы данных — CTO, обработку инцидентов — дежурного инженера, а решение о переходе — только одному человеку. Звучит строго, но это предотвращает расползание процесса.
Храните этот список владения в одном общем документе и обновляйте его каждый раз, когда что-то меняется. Старые предположения ломают миграции чаще, чем плохие манифесты.
Задайте правила деплоя до разделения
Параллельные среды начинают ломаться, когда команды выкатывают одно и то же изменение в двух местах.
Во время миграции с Docker Compose на Kubernetes дрейф обычно начинается с мелких решений. Одна команда добавляет флаг в Compose, другая меняет секрет в Kubernetes, и через неделю уже никто не может сказать, какая конфигурация правильная.
До переноса чего-либо решите, куда пойдут новые изменения. Одна сторона должна стать основным местом для новых правок. Многие команды отправляют все новые функции в Kubernetes, а Compose оставляют только для исправления ошибок в сервисах, которые ещё там живут. Это правило кажется жёстким, но оно снижает число ошибок при релизе и не позволяет людям дважды собирать одно и то же поведение.
Переменные окружения тоже должны иметь один источник правды. Ведите один утверждённый список с названиями переменных, значениями по умолчанию и владельцами. Используйте этот список для обеих сред. Если настройка существует только в файле Compose или только в чарте, исправьте это сразу. Небольшие различия вроде REDIS_URL с одной стороны и CACHE_URL с другой тратят часы.
По dual deploy тоже нужно говорить прямо. Не позволяйте выкатывать изменения сразу в обе стороны просто потому, что так спокойнее. Ограничьте это несколькими заранее названными случаями:
- запланированный перевод трафика
- окно миграции данных
- тренировочный откат
- срочное исправление после неудачного релиза
Во всех остальных случаях деплой должен идти только в тот рантайм, который владеет сервисом.
У релиза тоже должен быть один центр. Выберите одно место, где релиз становится официальным, и придерживайтесь его. Это может быть pipeline в GitLab, тикет на релиз или один CI-job с ручным подтверждением. Если один человек утверждает изменения Compose в чате, а другой утверждает Kubernetes в панели, процесс уже разделён.
Допустим, API теперь работает в Kubernetes, а воркер всё ещё в Compose. Команда всё равно должна утверждать оба через один и тот же релизный процесс, использовать один список переменных и фиксировать одно решение по откату. Раздельных рантаймов и так достаточно. Раздельные правила делают всё хуже.
Сначала отметьте общие части
Большая часть проблем при миграции начинается не в контейнерах.
Приложение переезжает, а база данных, очередь, кеш, секреты, логи и запланированные задачи часто ещё какое-то время остаются на прежнем месте. Если заранее не описать эти общие части, обе среды начнут менять одно и то же, и никто не заметит этого до падения продакшена.
Начните с тех компонентов, к которым обе стороны всё ещё обращаются. Обычно это основная база данных, Redis или другой кеш, очереди сообщений, значения секретов и любое общее хранилище. Запишите, какой сервис читает из каждого компонента, какой пишет в него и кто может менять его настройки. Один владелец на каждый общий ресурс — хорошее правило.
Возьмём обычный пример. API всё ещё работает в Docker Compose, а фоновый воркер уже переехал в Kubernetes. Оба используют одну и ту же базу Postgres и один экземпляр Redis. Если деплой воркера добавит изменение схемы или начнёт очищать записи кеша по-новому, API может упасть, хотя стек Compose никто не трогал. Изменения схемы, правила кеша и ротация секретов должны иметь назначенных владельцев ещё до начала разделения.
Не пропускайте и менее очевидные вещи. Общие файлы постоянно создают тихие сбои. Генератор отчётов может по-прежнему писать в смонтированную папку, которую видит только хост Compose. Ночная задача очистки может запускаться по cron на одном сервере, а новый Kubernetes Job попробует сделать ту же работу ещё раз. Так команды получают дублирующиеся письма, удалённые файлы или потерянные данные.
Для каждого общего элемента держите короткую запись:
- что это такое и где оно находится
- какие сервисы от него зависят
- кто отвечает за изменения во время переходного периода
- затрагивают ли его cron-задачи или общие пути к файлам
- какие логи и метрики должны совпадать по обеим сторонам
Логи и метрики тоже требуют внимания. Если Compose отправляет логи одним способом, а Kubernetes — другим, вы теряете полную картину во время инцидента. По возможности используйте одинаковые названия сервисов, правила алертов и базовые метки. Тогда откат будет проще, потому что обе среды показывают одни и те же сигналы.
Переносите сервисы в фиксированном порядке
Разделённая миграция обычно срывается, когда команды переносят слишком много сразу.
Выберите один низкорисковый сервис и держите объём изменений маленьким. Хорошие первые кандидаты — воркер отчётов, внутреннее админское API или cron-задача, которая не обрабатывает вход в систему, платежи или основные записи.
Заморозьте все несвязанные изменения вокруг этого сервиса, пока переносите его. Если разработчики продолжают выкатывать новые функции одновременно, вы перестаёте тестировать миграцию и начинаете тестировать случайные изменения в коде. Даже простая миграция с Docker Compose на Kubernetes становится шумной, когда приложение меняется у вас под ногами.
Сначала перенесите сервис на staging, а уже потом трогайте production. Запускайте там тот же образ, ту же конфигурацию и те же проверки здоровья. Если staging-трафика слишком мало, чтобы увидеть реальные проблемы, запустите контролируемую нагрузку, похожую на обычное использование.
Хорошо работает простой порядок:
- Перенесите один сервис с низким бизнес-риском.
- На первом проходе оставьте его зависимости там, где они были.
- Проверяйте staging, пока логи и метрики не станут спокойными.
- Переведите небольшой кусок продакшен-трафика.
- Подождите устойчивый период, а затем уберите старый маршрут.
После перевода внимательно следите за тремя вещами: ошибками, задержкой и записями в базу. Ошибки показывают, когда запросы не проходят. Задержка показывает, замедляется ли новый путь под нагрузкой. Записи в базу важны, потому что сервис может выглядеть здоровым, но тихо писать неверные данные или записывать их дважды.
Представьте стартап, который переносит воркер email-уведомлений из Compose в Kubernetes. Это намного безопаснее, чем переносить auth первым, потому что задержанные письма вредят меньше, чем пользователи, которые не могут войти. Команда замораживает изменения шаблонов и логики очереди, тестирует воркер на staging, а затем отправляет в Kubernetes только часть продакшен-очереди. Если время доставки и число неудачных заданий несколько дней остаются нормальными, можно переключать остальное.
Оставляйте старый путь живым до тех пор, пока новый не пройдёт полный бизнес-цикл без сюрпризов. Это значит обычный трафик, обычные шаблоны записи и отсутствие ручных исправлений. Только после этого удаляйте старую задачу деплоя и считайте перенос завершённым.
Простой пример разделения
Представьте, что ваш сервис /api/orders работает в Docker Compose на одной VM, и вы хотите сначала перенести только это API в Kubernetes. Это разумный первый шаг, потому что сервис небольшой, легко тестируется и обычно находится за одним публичным endpoint.
Оставьте endpoint без изменений. Клиенты должны использовать тот же домен и тот же путь до, во время и после переноса. Разделение трафика настраивайте в proxy или ingress, а не внутри приложения. Так изменение будет проще контролировать и проще откатывать.
На первом релизе оставьте базу данных на месте. Пусть обе версии API обращаются к одной и той же базе с одной и той же схемой, если новый код обратно совместим. Перенос приложения и базы одновременно создаёт две точки отказа, и именно так миграции уходят в сторону.
Начните с небольшого процента трафика, направленного в Kubernetes. Пяти процентов достаточно, чтобы увидеть что-то реальное, но не рисковать всем сервисом. Старая версия в Compose по-прежнему обрабатывает большую часть запросов, поэтому откат остаётся простым.
Сравнивайте обе стороны по одинаковым сигналам. Проверяйте число ошибок, время ответа, перезапуски и объём запросов. Смотрите и логи. Сервис может выглядеть здоровым на дашборде, но при этом заполнять логи таймаутами или лавиной повторных попыток. Если команда уже использует Grafana, Prometheus или Sentry, покажите обе версии на одном экране, чтобы различия были заметны.
Увеличивайте трафик поэтапно только если показатели остаются близкими. Переходите с 5 процентов на 25, потом на 50, а затем на весь трафик. Если версия в Kubernetes начинает чаще отдавать ошибки 500 или становится медленнее под нагрузкой, сначала верните трафик в Compose, а уже потом ищите причину. Откат должен быть одним изменением маршрутизации, а не ночной пересборкой.
Отключайте старый сервис в Compose только после того, как версия в Kubernetes несколько дней отработает весь трафик без сюрпризов. Перед остановкой проверьте фоновые задачи, health checks и запланированные операции. Этот финальный шаг важнее первого деплоя.
Постройте рабочие пути отката
План отката бесполезен, если команде приходится спорить о нём во время инцидента.
До переноса живого трафика напишите короткий путь назад к старой схеме. Распишите шаги переключения в том порядке, в котором команда будет их выполнять. Укажите, кто принимает решение, кто меняет трафик, кто проверяет приложение и кто следит за логами и ошибками. Если трафик идёт через load balancer, proxy или DNS-запись, укажите, какая настройка меняется первой и сколько обычно занимает это изменение.
Практическая заметка по откату часто включает четыре действия:
- вернуть трафик в среду Compose
- развернуть туда последний удачный тег образа
- восстановить предыдущий набор конфигурации или секретов
- подтвердить health checks, вход в систему и один реальный пользовательский сценарий
Храните старые образы, файлы окружения, ссылки на секреты и манифесты Compose в месте, куда on-call команда сможет быстро добраться. Не оставляйте их на ноутбуке одного инженера или в старом чате с прошлого месяца. Если доступ требует согласования, исправьте это до дня перевода.
Задайте и таймер решения. Выберите лимит и придерживайтесь его. Например, если уровень ошибок остаётся высоким 15 минут или checkout падает в двух подряд проверках, команда откатывается. Без чёткой границы люди ждут, надеются и теряют час, пока клиенты сталкиваются со сломанной системой.
Один раз потренируйте откат до первого живого перевода. Используйте staging, если он у вас есть, или проведите контролируемый тест с низкорисковым трафиком. Запустите Kubernetes, вызовите искусственную проблему и переключитесь обратно на Compose, пока кто-то следует написанным шагам буквально по строкам.
Такая репетиция обычно показывает реальные пробелы. Где-то отсутствует секрет. Пропал старый тег образа. DNS обновляется дольше, чем ожидалось. Лучше узнать об этом тихим днём, чем во время релизного окна.
Ошибки, которые создают дрейф
Самая сложная часть миграции с Docker Compose на Kubernetes — не первый перенос. Самое трудное — медленный дрейф, который начинается после него.
Одна команда правит файл Compose, другая меняет Deployment, и через месяц уже никто не доверяет ни одной среде. Сервис всё ещё работает, поэтому несоответствие остаётся скрытым до дня релиза. Потом staging ведёт себя одним образом, production — другим, и откат превращается в угадывание.
Изменения базы данных наносят ещё больший ущерб. Команды часто меняют схему до переноса трафика, потому что так кажется быстрее. Обычно это не так. Если Kubernetes ожидает новый столбец, а Compose всё ещё запускает старое приложение, у вас появляются две версии, по-разному зависящие от одной базы. Даже небольшое изменение, например сделать поле обязательным, может сломать воркеры, которые ещё работают на старой стороне.
Фоновые задачи — ещё одна слепая зона. Веб-трафик привлекает внимание, а запланированные задачи, очереди и воркеры часто остаются в серой зоне. Если никто не записал, кто владеет задачей, её могут запускать обе среды. Это приводит к дублирующимся письмам, двойным списаниям, повторным импорту или двум задачам очистки, которые мешают друг другу работать с одними и теми же записями.
Ранние сигналы обычно заметны, если знать, на что смотреть:
- хотфиксы попадают только в одну конфигурацию
- секреты используют разные названия или значения
- одна сторона запускает воркеры, а другая считает, что это делает она
- миграции базы данных выходят раньше, чем переводится трафик
Дрейф секретов ломает откат быстрее, чем ожидают большинство команд. Откат работает только если старая среда по-прежнему может стартовать с теми же учётными данными, адресами и feature flags, которые были у неё раньше. Если стороне Kubernetes достаётся новый формат секрета, ротация токена или другой bucket, Compose может подняться уже сломанным.
Небольшой файл владения решает большую часть этих проблем. Храните короткую запись по каждому сервису: кто может его менять, где запускаются задачи, какие секреты используются и какая конфигурация теперь доступна только для чтения. Если изменение не называет одного владельца, оно не должно попасть в релиз.
Короткий релизный чеклист
Релизный день становится хаотичным, когда один сервис всё ещё живёт в Docker Compose, а другой уже работает в Kubernetes. Короткий письменный чеклист делает перенос скучным, а именно скучным он и должен быть.
Используйте один общий чеклист для каждого релиза во время миграции:
- Поставьте одного понятного владельца рядом с каждым сервисом. Используйте реальное имя или название команды, а не «platform» или «backend», если это может означать трёх разных людей.
- Выберите одно правило деплоя для релиза. Например, только Kubernetes получает новые версии для перенесённых сервисов, а Compose — только срочные исправления для сервисов, которые ещё не переехали.
- Проверьте, что целевая среда здорова, прежде чем отправлять туда трафик. Проверьте само приложение, его зависимости и health endpoint, который вы реально используете в продакшене.
- Убедитесь, что на целевой стороне до перевода трафика работают логи, метрики и алерты. Если ошибки растут, команда должна увидеть это за считаные минуты.
- Сделайте шаги отката достаточно короткими, чтобы они помещались на одной странице. Если план требует длинного документа и трёх согласований, под давлением им никто не воспользуется.
Допустим, API теперь работает в Kubernetes, а воркер всё ещё в Compose. Владелец API утверждает релиз, команда выкатывает только через Kubernetes pipeline, health checks проходят, Grafana и Sentry показывают нормальное поведение, а в заметке по откату написано, кто возвращает трафик назад и как. Этого достаточно, чтобы избежать грязного релиза.
Распечатайте чеклист, держите его в релизных заметках или закрепите в командном чате. Если люди не могут найти его за 10 секунд, они начнут угадывать.
Ваши следующие шаги
Зафиксируйте линию разделения до того, как кто-то начнёт трогать production. Запишите каждый сервис и назначьте одного владельца на ближайшие 30 дней: Docker Compose или Kubernetes. Если обе среды могут деплоить один и тот же сервис, дрейф начнётся почти сразу.
Сделайте это до первого неудачного релиза:
- составьте список владения и держите его рядом с релизными заметками
- перенесите сначала один небольшой сервис, например воркер, cron-задачу или внутреннее API
- выполните этот перенос в обычное релизное окно и зафиксируйте точные шаги
- протестируйте откат в тот же день, пока детали ещё свежи
Выберите сервис, который не сильно ударит по бизнесу, если он несколько минут поработает неправильно. Фоновый воркер обычно лучше подходит для первого шага, чем основной веб-приложение. Избегайте входа, биллинга и всего, чем клиенты пользуются напрямую, пока у команды не будет одного чистого теста за плечами.
После теста разберите, что именно сломалось. Возможно, одно значение конфигурации жило в двух местах. Возможно, логи в Compose было легко найти, а в Kubernetes их было труднее проследить. Возможно, откат сработал только потому, что один инженер вспомнил неописанную команду. Эти детали важнее, чем красиво оформленный план.
Затем ужесточите правила. Уберите дублирующиеся пути деплоя, сократите шаги отката и решите, кто может утверждать изменения, пока обе среды работают параллельно. Хорошая миграция с Docker Compose на Kubernetes становится проще после первого шага, а не сложнее.
Некоторые команды могут разобраться сами. Другие продолжают спорить о владении, порядке релизов или о том, кто принимает финальное решение, когда растёт риск. В таких случаях может помочь внешний технический лидер. Oleg Sotnikov на oleg.is работает как Fractional CTO и советник для стартапов, и такое планирование миграции хорошо подходит для этой роли.
Один небольшой перенос на этой неделе научит вашу команду большему, чем ещё один месяц споров.