Через 2–3 года любой Telegram-бот рано или поздно упирается в стек, на котором был написан. Фреймворк не получает обновлений, конструктор перестаёт справляться с нагрузкой, бывший подрядчик ушёл и не передал документацию. Полная переписка кажется единственным выходом — но это самый рискованный и самый долгий путь. Разберём, когда миграция действительно нужна, какие стратегии работают на проектах разного размера и как не потерять пользователей в процессе.
Когда миграция действительно нужна
Признаки, что пора серьёзно задуматься:
- Фреймворк не получает обновлений больше года, а Bot API ушёл вперёд: появились Stars, Business Bots, новые типы апдейтов — и вы не можете их использовать.
- Зависимости застряли на Python 2 или Node 12 — на которые уже не выпускают патчи безопасности.
- Бывший подрядчик ушёл, исходники частично потеряны, новая команда отказывается поддерживать чужой no-name-стек.
- Конструктор (Salebot, BotHelp, ManyChat-аналоги) уперся в потолок: нужна интеграция, которой нет; стоимость подписки растёт быстрее выручки; SLA провайдера не устраивает.
- Compliance: заказчик требует on-prem развёртывание, а текущий стек — SaaS-конструктор без экспорта.
- Каждое изменение занимает два дня и три молитвы, потому что нет тестов и архитектура «всё в одном файле на 4000 строк».
Когда мигрировать НЕ стоит
Половина миграций случается не потому, что они нужны бизнесу, а потому что разработчику «хочется на новом фреймворке». Это плохой повод. Воздержитесь, если:
- Бот работает, пользователи довольны, ничего не болит. Любая переписка — это риск регресса.
- Нет конкретного problem statement: «хочется чище», «хочется современнее» — это не задача, это эстетика.
- Проблема не в стеке, а в коде: грязная архитектура переедет вместе с вами в любой новый фреймворк, если её не пересобрать.
- На горизонте крупный продуктовый релиз — миграция и новая фича одновременно гарантированно превратятся в катастрофу.
- Команда не имеет опыта в целевом стеке. Учиться на проде — дорогое удовольствие.
Типовые сценарии миграции
| Откуда | Куда | Сложность | Зачем |
|---|---|---|---|
| Salebot / BotHelp | Python aiogram 3 | Высокая | Гибкость, своя БД, кастомные интеграции |
| python-telegram-bot v13 | aiogram 3 | Средняя | Async, FSM, актуальный Bot API |
| Node.js Telegraf | grammY | Средняя | Активная разработка, типизация, плагины |
| Монолит | Микросервисы | Высокая | Масштабирование команды, изоляция отказов |
| SQLite / локальная PG | Yandex Managed PostgreSQL | Низкая | Бэкапы, репликация, мониторинг |
| Long polling | Webhook | Низкая | Latency, масштабирование |
| Pages Router (Mini App) | App Router (Next.js 14) | Средняя | Server components, streaming |
Каждый сценарий имеет свои подводные камни — ниже разберём ключевые.
Из конструктора в кастомный код
Самый болезненный путь, потому что в конструкторе обычно нет экспорта логики в формате, который можно затащить в код. Переписывают вручную: каждый сценарий, каждое условие, каждую кнопку.
Что работает:
- Снимаем скриншоты всех воронок из конструктора.
- Документируем в Notion или Miro: триггер → шаг → ответвления → выход.
- Просим продакта пройти каждый сценарий руками и записать ожидаемое поведение.
- Параллельно выгружаем БД пользователей через API конструктора (если он есть) или через service desk провайдера.
- Пишем кастомный бот на aiogram 3, первая версия — точная копия, без улучшений. Улучшения потом.
С python-telegram-bot v13 на aiogram 3
PTB v13 — синхронный, aiogram 3 — async. Это значит, что нельзя «по строчке» перенести код: меняется модель выполнения. Что важно:
- Все хендлеры становятся
async def. - Блокирующие вызовы (
requests.get,time.sleep) надо заменить наaiohttp/asyncio.sleep, иначе один медленный запрос блокирует весь event loop. ConversationHandlerмигрирует вFSMContextaiogram. Состояния переименовываются, переходы переписываются.- Middleware пишутся иначе: PTB-style декораторы заменяются на
BaseMiddleware-классы.
Хороший приём — поднять aiogram 3 рядом со старым ботом, перевести один сценарий целиком, прогнать на тест-аккаунте, потом следующий.
С Node.js Telegraf на grammY
Telegraf 4 ещё жив, но grammY активно развивается, имеет более строгую типизацию и удобную систему плагинов. Миграция:
- API похоже, но
ctx.replyсохраняет совместимость лишь частично. - Сессии: в Telegraf —
telegraf-session-*, в grammY —@grammyjs/storage-*. Формат хранения отличается, нужен ETL. - Middleware-цепочки в grammY типизируются через
Context-extension; в Telegraf обычно нетипизированные. - Conversations:
telegraf-conversations→@grammyjs/conversations(другая семантика).
Из монолита в микросервисы
Соблазн «разнесём всё на сервисы» возникает, когда монолит вырос до 50+ тысяч строк. Прежде чем декомпозировать, ответьте на вопрос: какую конкретно проблему это решит?
Имеет смысл, если:
- Разные команды должны независимо релизить разные части бота.
- Часть нагрузки (например, обработка платежей) требует другого SLA / другой инфраструктуры.
- Нужна изоляция отказов: падение модуля рассылок не должно ронять основной flow.
Не имеет смысла, если команда — три человека, нагрузка — 5 RPS, а инфраструктурного инженера в штате нет.
С локальной БД на managed
Самая безопасная и при этом самая полезная миграция. Локальный PostgreSQL на VPS = ручные бэкапы, ручной мониторинг, ручной failover. Managed (Yandex Managed PG, Selectel, Timeweb Cloud) даёт всё это из коробки.
Шаги:
- Снять
pg_dumpс боевой БД в момент минимальной активности. - Создать managed-кластер той же мажорной версии PG.
- Восстановить дамп через
pg_restore. - Проверить:
SELECT count(*)по ключевым таблицам сверить со старой. - Переключить
DATABASE_URLв боте, перезапустить. - Старую БД оставить в read-only на 2 недели — на случай отката.
С polling на webhook
Polling прост в разработке, но плох в проде: лишние запросы к Telegram, latency 1–3 секунды, сложности с горизонтальным масштабированием. Webhook требует HTTPS-эндпоинта с валидным сертификатом.
Минимальный путь: ставим nginx с Let's Encrypt, прописываем setWebhook, останавливаем старый polling-процесс. Если нужен zero-downtime — поднимаем webhook рядом, переключаем setWebhook, ловим оставшиеся апдейты polling-ом, останавливаем polling.
Стратегии миграции
| Стратегия | Когда подходит | Риск | Время |
|---|---|---|---|
| Big bang | Маленький бот, 5–10 сценариев | Высокий | Дни |
| Strangler Fig | Средний / большой бот | Средний | Недели–месяцы |
| Side-by-side с feature flags | Бот с активной аудиторией | Низкий | Месяцы |
| Canary (% трафика) | Бот с измеримыми метриками | Низкий | Недели |
Big bang
Останавливаем старого, выкатываем нового, молимся. Подходит только если:
- Бот совсем простой.
- Есть полный набор регрессионных тестов.
- Окно для отката в пределах часа.
Ошибка дорого обходится: пользователи, написавшие в момент переключения, могут потерять контекст.
Strangler Fig
Классика. Новый код стоит перед старым и постепенно перехватывает запросы. Сегодня перехватили /start, через неделю — оплату, через месяц — рассылки. Старый код жив, пока его последний хендлер не уйдёт в новый.
В Telegram-боте «прокси» — это сам webhook-роутер: он смотрит на тип апдейта и решает, кому передать.
Side-by-side с feature flags
Две версии бота крутятся параллельно. Флаг в БД определяет, кто из пользователей попадает в новую логику. Удобно для A/B-тестирования и быстрого отката (выключили флаг — все вернулись на старую версию).
Canary
Сначала 1% трафика на новую версию, затем 5%, 25%, 100%. На каждом шаге смотрим метрики: error rate, latency, конверсию. Если что-то деградирует — откатываем долю обратно.
Подготовка к миграции
Без подготовки любая стратегия превращается в Big bang. Минимальный чек-лист:
- Аудит текущего: список всех сценариев, всех интеграций, всех cron-джоб, всех webhook-эндпоинтов.
- Фиксация контрактов: что бот обещает пользователю (кнопки, тексты, тайминги). Это станет regression-тестами.
- Метрики baseline: DAU, MAU, конверсия по ключевым воронкам, среднее время ответа. Без них вы не поймёте, стало лучше или хуже.
- Согласия пользователей на ПДн: переносим как есть, если форма согласия не меняется. Если меняется — спрашиваем заново.
- Резервная копия: полный дамп БД и текущая версия кода в тегированном релизе.
Перенос пользователей и данных
Идентификация пользователя в Telegram стабильна — user_id не меняется никогда. Это упрощает ETL: достаточно сохранить соответствие старой и новой схем.
Простой ETL-скрипт на Python:
import asyncio
import asyncpg
OLD_DSN = "postgresql://user:pass@old-host/oldbot"
NEW_DSN = "postgresql://user:pass@new-host/newbot"
async def migrate_users():
src = await asyncpg.connect(OLD_DSN)
dst = await asyncpg.connect(NEW_DSN)
rows = await src.fetch(
"SELECT tg_id, username, lang, created_at, consent_pdn FROM users"
)
async with dst.transaction():
for r in rows:
await dst.execute(
"""
INSERT INTO users (telegram_id, username, locale, created_at, pdn_consent_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (telegram_id) DO UPDATE
SET username = EXCLUDED.username,
locale = EXCLUDED.locale
""",
r["tg_id"], r["username"], r["lang"] or "ru",
r["created_at"], r["created_at"] if r["consent_pdn"] else None,
)
src_count = await src.fetchval("SELECT count(*) FROM users")
dst_count = await dst.fetchval("SELECT count(*) FROM users")
print(f"src={src_count} dst={dst_count}")
assert src_count == dst_count, "row count mismatch"
asyncio.run(migrate_users())
После запуска — обязательная валидация: count по таблицам, выборочная проверка 50–100 случайных записей, сверка агрегатов (сумма платежей, число активных подписок).
Согласия на ПДн при миграции
По 152-ФЗ согласие даётся на конкретного оператора и конкретные цели обработки. Если оператор и цели не меняются — переносим согласие как есть. Если меняется юрлицо или появляются новые цели (например, маркетинговые рассылки) — нужно собрать согласие заново. Это один из тех случаев, когда юриста стоит спросить до миграции, а не после.
Миграция бот-токена
Токен бота можно переиспользовать в новом коде — Telegram не различает «старого» и «нового» владельца кода, для него это один и тот же бот. Процедура:
- Деплой нового кода на новую машину, без запуска webhook.
- На старой машине:
deleteWebhook(или останавливаем polling). - На новой:
setWebhook. - Окно простоя — 10–30 секунд, в течение которых апдейты буферизуются Telegram-ом и доставятся новому коду.
Альтернатива — заводим @bot_v2, мигрируем пользователей через рассылку с инвайтом. Минус: retention падает на 20–50% (часть пользователей просто не нажмёт /start у нового бота). Подходит только когда смена бота — это смена бренда, и потеря части аудитории заложена в план.
FSM и активные сессии
Если у бота есть FSM (заказ оформляется в 4 шага, опрос из 8 вопросов), то в момент переключения часть пользователей зависнет в середине сценария. Варианты:
- Дренаж: за неделю до переключения новые сессии перестают создаваться в старом боте, идут в новый. Старые сессии завершают свой жизненный цикл за 2–7 дней.
- Миграция стейта: ETL переносит таблицу
fsm_statesв новую схему. Требует совместимого формата состояний. - Сброс: в момент переключения шлём всем активным «извините, начните сценарий заново». Худший UX, но иногда единственный вариант.
Backward-compatible миграции БД
Прямая миграция «удалили старую колонку, переименовали таблицу» означает невозможность отката. Правильный паттерн — expand / contract:
- Expand: добавили новую колонку, новый код пишет в неё и в старую, старый код пишет только в старую.
- Прогон бэкфилла: переносим старые данные в новый формат.
- Переключаем чтение на новую колонку.
- Contract: спустя неделю стабильной работы — удаляем старую колонку.
Пример Alembic-миграции (expand):
"""add normalized phone column
Revision ID: 7f3a2c1b9e0d
Revises: 4c8b1a2f3d5e
Create Date: 2025-10-20 12:00:00
"""
from alembic import op
import sqlalchemy as sa
revision = "7f3a2c1b9e0d"
down_revision = "4c8b1a2f3d5e"
def upgrade() -> None:
op.add_column(
"users",
sa.Column("phone_e164", sa.String(length=16), nullable=True),
)
op.create_index("ix_users_phone_e164", "users", ["phone_e164"])
def downgrade() -> None:
op.drop_index("ix_users_phone_e164", table_name="users")
op.drop_column("users", "phone_e164")
Колонка добавлена nullable — старый код продолжает работать, новый код начинает заполнять. Через релиз делается бэкфилл, ещё через релиз nullable=False и удаление старой колонки.
Feature flags для canary
Простейший вариант на стороне бота — флаг по user_id % 100:
from dataclasses import dataclass
@dataclass(frozen=True)
class CanaryConfig:
new_handler_percent: int # 0..100
def use_new_handler(user_id: int, cfg: CanaryConfig) -> bool:
return (user_id % 100) < cfg.new_handler_percent
# в роутере
async def order_handler(message, state, cfg: CanaryConfig):
if use_new_handler(message.from_user.id, cfg):
await new_order_flow(message, state)
else:
await legacy_order_flow(message, state)
CanaryConfig.new_handler_percent хранится в БД или Redis, обновляется без релиза. Меняется с 1 → 5 → 25 → 100 по мере накопления статистики.
Blue-green переключение через nginx
Если бот за nginx (webhook на HTTPS-эндпоинт), переключение между версиями делается через upstream:
upstream bot_blue {
server 127.0.0.1:8081;
}
upstream bot_green {
server 127.0.0.1:8082;
}
upstream bot_active {
server 127.0.0.1:8081; # blue active
# server 127.0.0.1:8082; # green standby
}
server {
listen 443 ssl http2;
server_name bot.example.ru;
location /webhook/SECRET_PATH {
proxy_pass http://bot_active;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
}
Переключение: правим bot_active, делаем nginx -s reload. Окно простоя — 0 секунд, апдейты буферизуются на уровне TCP.
Тестирование до и после
Без тестов миграция — это лотерея. Минимум:
- Golden tests на критичные сценарии: записываем эталонную последовательность апдейтов и ожидаемых ответов на старом боте, прогоняем на новом, сравниваем побайтово.
- Smoke-тесты: после каждого деплоя —
/start, регистрация, базовая воронка. - Нагрузочные: если ожидается прирост трафика —
locustилиk6с реалистичным профилем.
Запись golden-теста на pytest:
import pytest
from bot.testing import BotClient
@pytest.mark.asyncio
async def test_order_flow_golden(bot: BotClient, snapshot):
await bot.send("/start")
await bot.click("Сделать заказ")
await bot.send("Пицца Маргарита")
await bot.click("Оформить")
await bot.send("ул. Ленина, 1")
snapshot.assert_match(bot.history, "order_flow.yaml")
snapshot.assert_match сравнивает текущую переписку с эталонной — расхождение валит тест.
Rollback-план
Перед миграцией — обязательно письменная инструкция «как откатиться за 5 шагов»:
git checkout v2.4.1(тегированный последний стабильный релиз).- Восстановить БД из дампа
pre-migration-2025-11-03.dump. setWebhookобратно на старый эндпоинт.- Перезапустить старый процесс.
- Сообщение пользователям: «временные неполадки устранены».
Дамп БД делается до миграции, кладётся в S3-совместимое хранилище, доступ проверяется отдельным человеком. Не доверяйте автоматическим бэкапам, которые «должны были сработать».
Сроки и команда
Очень грубые ориентиры:
- Маленький бот (5–10 сценариев, 1 БД): 1–2 недели одним разработчиком.
- Средний (FAQ + платежи + рассылки + админка): 1–3 месяца командой из 2 человек.
- Большой (мультиязык, сложная FSM, интеграции с CRM/ERP): 6+ месяцев командой из 3–5 человек.
Закладывайте +30–50% к оценке на риски: непредвиденные edge cases, регресс, доработки по итогам бета-теста.
Стоимость
Миграция — это всегда дороже, чем кажется. Считается в человеко-часах: разработка + тестирование + DevOps + проджект. Типичный бюджет среднего бота — от 400 до 1500 часов. Не забывайте включить:
- Время на параллельную поддержку старой версии в течение переходного периода.
- Стоимость новой инфраструктуры (managed PG, Redis, мониторинг).
- Постмиграционную стабилизацию: 2–4 недели после переключения уйдут на ловлю редких багов.
Коммуникация с пользователями
Большинство мигрирующих ботов забывают сказать пользователю «привет, мы переехали». Минимум:
- За неделю — пост в канале / сторис в Mini App: «обновляем бот, возможны кратковременные задержки».
- В день переключения — короткое сообщение всем активным пользователям: «готово, что нового».
- В первые 48 часов — оперативная поддержка в личке: ловим баг-репорты.
Антипаттерны, которые гарантированно сломают миграцию
- Миграция и редизайн одновременно: не понимаете, баг в новом коде или в новой UX-логике.
- Нет тестов на старом коде: не знаете, что работало.
- Нет rollback-плана: при первом серьёзном инциденте — паника.
- Миграция в пятницу вечером: классика. Только в начале недели, только в рабочие часы.
- Один разработчик-герой: уйдёт в отпуск — встанет всё.
- «Сначала перепишем, потом подумаем про данные»: пользователи не подождут, пока вы придумаете схему миграции.
Итого
Миграция Telegram-бота — это инфраструктурный проект, а не «давайте перепишем за выходные». Чек-лист, который окупается:
- Чёткий problem statement: что именно болит, чего хотим достичь.
- Аудит: список фич, контрактов, интеграций, метрик baseline.
- Выбор стратегии под размер: Big bang только для совсем мелких, остальные — Strangler Fig или canary.
- ETL пользователей и данных с валидацией count + sample.
- Backward-compatible миграции БД (expand / contract).
- Тесты на критичные сценарии до и после.
- Rollback-план в одной странице, проверенный руками.
- Коммуникация с пользователями: до, во время, после.
Частые вопросы
Полный zero-downtime сложно, но окно можно сократить до 10–30 секунд: blue-green через nginx upstream, переключение setWebhook, буферизация апдейтов на стороне Telegram. Для большинства ботов этого достаточно.
Если используется тот же токен — нет, для пользователя это тот же бот. Если заводится новый @bot_v2 — теряется 20–50% retention, потому что часть пользователей просто не нажмёт /start у нового.
Если оператор и цели обработки не меняются — переносим как есть. Если меняется юрлицо или появляются новые цели (рассылки, передача третьим лицам) — собираем заново. Лучше уточнить у юриста до миграции.
Документируем все воронки в Notion (скриншоты + текстовое описание), выгружаем БД пользователей через API или через service desk, пишем кастомный бот на aiogram 3 как точную копию. Улучшения добавляем после стабилизации.
400–1500 часов разработки в зависимости от сложности FSM, числа интеграций и требований к надёжности. Закладывайте +30–50% к оценке на риски.
Три варианта: дренаж (новые сессии в новом боте, старые доживают в старом 2–7 дней), миграция стейта через ETL, сброс с уведомлением пользователя. Дренаж — оптимальный, если можно подождать неделю.
Big bang — только для маленьких ботов с полным набором тестов и быстрым откатом. Для среднего и большого — Strangler Fig: новый код постепенно перехватывает хендлеры, старый умирает по частям. Меньше риска, дольше по календарю.