Legan Studio
Все статьи
~ 14 мин чтения

Миграция Telegram-бота с устаревшего стека

Как мигрировать Telegram-бот с устаревшего стека на современный без потери пользователей: aiogram 2 → 3, telebot → grammY, монолит → сервисы.

  • Telegram
  • миграция
  • разработка

Через 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 / BotHelpPython aiogram 3ВысокаяГибкость, своя БД, кастомные интеграции
python-telegram-bot v13aiogram 3СредняяAsync, FSM, актуальный Bot API
Node.js TelegrafgrammYСредняяАктивная разработка, типизация, плагины
МонолитМикросервисыВысокаяМасштабирование команды, изоляция отказов
SQLite / локальная PGYandex Managed PostgreSQLНизкаяБэкапы, репликация, мониторинг
Long pollingWebhookНизкаяLatency, масштабирование
Pages Router (Mini App)App Router (Next.js 14)СредняяServer components, streaming

Каждый сценарий имеет свои подводные камни — ниже разберём ключевые.

Из конструктора в кастомный код

Самый болезненный путь, потому что в конструкторе обычно нет экспорта логики в формате, который можно затащить в код. Переписывают вручную: каждый сценарий, каждое условие, каждую кнопку.

Что работает:

  1. Снимаем скриншоты всех воронок из конструктора.
  2. Документируем в Notion или Miro: триггер → шаг → ответвления → выход.
  3. Просим продакта пройти каждый сценарий руками и записать ожидаемое поведение.
  4. Параллельно выгружаем БД пользователей через API конструктора (если он есть) или через service desk провайдера.
  5. Пишем кастомный бот на 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 мигрирует в FSMContext aiogram. Состояния переименовываются, переходы переписываются.
  • 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) даёт всё это из коробки.

Шаги:

  1. Снять pg_dump с боевой БД в момент минимальной активности.
  2. Создать managed-кластер той же мажорной версии PG.
  3. Восстановить дамп через pg_restore.
  4. Проверить: SELECT count(*) по ключевым таблицам сверить со старой.
  5. Переключить DATABASE_URL в боте, перезапустить.
  6. Старую БД оставить в 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 не различает «старого» и «нового» владельца кода, для него это один и тот же бот. Процедура:

  1. Деплой нового кода на новую машину, без запуска webhook.
  2. На старой машине: deleteWebhook (или останавливаем polling).
  3. На новой: setWebhook.
  4. Окно простоя — 10–30 секунд, в течение которых апдейты буферизуются Telegram-ом и доставятся новому коду.

Альтернатива — заводим @bot_v2, мигрируем пользователей через рассылку с инвайтом. Минус: retention падает на 20–50% (часть пользователей просто не нажмёт /start у нового бота). Подходит только когда смена бота — это смена бренда, и потеря части аудитории заложена в план.

FSM и активные сессии

Если у бота есть FSM (заказ оформляется в 4 шага, опрос из 8 вопросов), то в момент переключения часть пользователей зависнет в середине сценария. Варианты:

  • Дренаж: за неделю до переключения новые сессии перестают создаваться в старом боте, идут в новый. Старые сессии завершают свой жизненный цикл за 2–7 дней.
  • Миграция стейта: ETL переносит таблицу fsm_states в новую схему. Требует совместимого формата состояний.
  • Сброс: в момент переключения шлём всем активным «извините, начните сценарий заново». Худший UX, но иногда единственный вариант.

Backward-compatible миграции БД

Прямая миграция «удалили старую колонку, переименовали таблицу» означает невозможность отката. Правильный паттерн — expand / contract:

  1. Expand: добавили новую колонку, новый код пишет в неё и в старую, старый код пишет только в старую.
  2. Прогон бэкфилла: переносим старые данные в новый формат.
  3. Переключаем чтение на новую колонку.
  4. 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 шагов»:

  1. git checkout v2.4.1 (тегированный последний стабильный релиз).
  2. Восстановить БД из дампа pre-migration-2025-11-03.dump.
  3. setWebhook обратно на старый эндпоинт.
  4. Перезапустить старый процесс.
  5. Сообщение пользователям: «временные неполадки устранены».

Дамп БД делается до миграции, кладётся в 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: новый код постепенно перехватывает хендлеры, старый умирает по частям. Меньше риска, дольше по календарю.