Привет, Хабр!
Меня зовут Алексей Кирдяшкин, я занимаюсь инфраструктурной разработкой в Timeweb Cloud.
У нас есть сервис App Platform (раньше он назывался Apps). Если коротко, это управляемая платформа поверх VDS для деплоя приложений напрямую из Git, без ручной настройки серверов и танцев с бубном.
Я застал App Platform еще в виде MVP, участвовал в его адаптации под инфраструктуру, работал над архитектурой — по сути, провел сервис от первого прототипа до продакшена с реальной нагрузкой. Конечно, все работало — приложения деплоились, пользователи приходили, платформа росла. Но с ростом стали заметны архитектурные решения, которые были оправданы для MVP, но плохо подходили для большого сервиса.
Поэтому мы взяли и… полностью пересобрали уже работающий сервис с нуля. Как, зачем и почему — в этой статье.
❯ Что за App Platform
Допустим, у вас уже есть репозиторий на GitHub, GitLab или другом хостинге, где лежит небольшой сервис, Telegram-бот, API, фронтенд или монолит на любимом фреймворке. И, чтобы это завести в облако и заставить работать 24/7, обычно надо:
заказать VDS → настроить окружение под язык → установить Docker → поднять веб-сервер → выпустить SSL → написать Dockerfile → как-то автоматизировать деплой.
В App Platform этот путь упрощается до нескольких действий прямо в панели управления:
подключить репозиторий → выбрать стек или добавить свой Dockerfile → задать регион → прописать переменные окружения → запустить деплой.

Под капотом в этот момент APPS-API создает отдельную VDS, устанавливает туда нового агента и запускает процесс сборки. Когда билд собирается и раскатывается, в интерфейсе появляются логи:
если что-то ломается на этапе сборки или деплоя, это видно в логах;
если ошибка уже в самом приложении, ее тоже можно отследить через вывод.

По итогу через пару минут имеем работающий сервис в облаке и технический домен с SSL, который ведет на это приложение.
Уже дальше, в той же панели управления, можно:
включить автодеплой с последнего коммита в ветке;
выбрать, из какого коммита собирать приложение;
менять окружение, добавлять или править переменные;
при необходимости перейти с Dockerfile на, например, Docker Compose и задеплоить заново.
Первая версия App Platform запускалась в сжатые сроки как MVP под проверку идей и гипотез. Простая идея: дать разработчикам способ деплоить проекты без ручной настройки серверов. Он работал, пользовался спросом и быстро вырос. Но архитектурно сервис так и остался MVP, который однажды пришлось встроить в общую инфраструктуру Timeweb Cloud.
❯ Как мы оказались в тупике
На волне спроса, интеграцию в общую инфраструктуру мы делали в спешке: что-то переехало как есть, что-то временно, что-то на компромиссах, чтобы не замедлять развитие.
Последствия такой миграции быстро проявились в проде. Логи собирались самописным сервисом, который периодически отваливался. Выпуск SSL ломался из-за неверной логики обновления и ограничениями квот. Обновление версий языков и фреймворков превращалось в отдельный мини-проект. Билды шли прямо на клиентских VDS, забирали ресурсы и могли уронить приложение.
Естественно, на все это мы получали тонны хейта в отзывах, тикетах поддержки и чате сообщества:

Вы, наверное, уже поняли, где корень проблем? Да, дело в архитектуре. Околомонолит-недомикросервис оказался попросту не рассчитан на рост: поддержка съедала время команды, развитие уперлось в потолок, и каждое улучшение превращалось в дорогое приключение с непредсказуемым финалом.
❯ Архитектурный ренессанс: как перепроектировали с нуля
Мы решили, что хватит с нас бесконечного латания, и стали думать: как из архитектуры, где один сбой тянет за собой цепочку других, сделать стабильную, предсказуемую и масштабируемую платформу?
Старая схема выглядела почти примитивно: один большой APPS-API и один агент, который разворачивался на клиентских VDS и делал вообще все — и сборку, и запуск, и управление процессом. Получалось, что сборка и деплой живут в одном месте и конкурируют за ресурсы, а API вынужден знать слишком много о внутреннем устройстве агента.
Чтобы разорвать этот узел, мы выделили роли и построили систему вокруг трех независимых участников — управляющего API, агента и отдельного билд-сервиса. Вот как это выглядит:

Конечно же, новая схема должна работать предсказуемо, и поэтому внутренний дизайн мы пересобрали вокруг Domain-Driven Design и event-driven архитектуры:
DDD помог упорядочить домены и привести код к той же модели, в которой живет продукт: сущности [app], [deploy] и другие доменные объекты теперь оформлены так же, как они описаны в бизнес-логике. Это резко снизило связанность, разделило обязанности и дало возможность тестировать домены независимо от инфраструктуры.
Event-driven архитектура убрала плотные связи между компонентами. APPS-API больше не знает, где именно выполняется деплой, а просто публикует событие. Какой именно агент это событие обработает и где он развернут (на VDS или в Kubernetes) — API не волнует. Агенты тоже ничего не знают о внутреннем устройстве API: они слушают свои топики и выполняют инструкции.
С такими подходами, чтобы добавить новый тип агента или новый тип окружения для деплоя, не нужно лезть в управляющий сервис и все перекраивать. Реализуем нового агента и он просто подключается к уже существующим событиям.
Но архитектурная перестройка это только фундамент. Далее мы взялись переписывать ключевые узлы платформы.
Сборĸа Docker образов
Как уже сказано выше, мы вынесли сборку в отдельный компонент APPS Builder — самостоятельный сервис в Kubernetes с собственным registry. Он стал полностью изолирован от клиентских машин, не нагружает VDS пользователя и не зависит от агента. Вот что это дало:
Билды больше не трогают VDS пользователя. Отдельная инфраструктура выделяет ресурсы под сборку, а запущенные приложения работают без просадок и конкуренции за CPU/диск.
Сборка стала быстрее и стабильнее. Builder использует BuildKit — продакшен-уровневый инструмент, который держит нагрузку и масштабируется горизонтально.
Более безопасную и гибкую архитектуру. Builder независим от агента и APPS-API: его можно обновлять, тестировать и развивать без влияния на деплои.
Вынесение сборки в отдельный сервис полностью убрало самый опасный узел старой архитектуры: с разделенными сборкой и деплоем можно масштабироваться без страха что билд уронит прод.
Сбор логов
Если у сервиса есть автоматизация деплоя, то у него должен быть и прозрачный сбор логов. Мы решили не мелочиться, а полностью заменить старый сбор логов на продакшн-стек как в других наших продуктах:
Fluent Bit забирает логи с сервисов;
OpenSearch централизованно их хранит;
OpenSearch Dashboards позволяет читать их без доступа к VDS;
компоненты деплоя и сборки при этом вообще не знают, кто собирает их логи.
По сути, логирование стало таким, каким оно должно быть в современной PaaS, где:
надежно работает продакшен-реди связка OpenSearch + Fluent Bit вместо самописного сбора;
все логи доступны в одной точке, без SSH и ковыряния в файловой системе;
система легко расширяется, и мы можем поэтапно добавлять аналитику, повышать детализацию и подключать инструменты диагностики вплоть до LLM-подсказок.
Выпусĸ SSL сертифиĸатов
Выпуск SSL-сертификатов был одним из наиболее проблемных участков системы: APPS-API сам хранил сроки жизни сертификатов, раз в сутки обходил список, пытался продлить все скопом и… регулярно падал. Схема не учитывала квоты Let's Encrypt, была чувствительна к сбоям и могла перегружать систему.
Чинить то, что изначально было спроектировано неправильно, не имело смысла, поэтому мы заменили весь механизм отдельным компонентом на базе Caddy. Он работает по ACME из коробки, не зависит от APPS-API и сам управляет выпуском и продлением сертификатов. Так мы убрали лишнюю самописную логику внутри монолита и получили более надежную схему работы.
Шаблоны и сборĸа Dockerfile
Сборочный пайплайн оказался слишком хрупким: любое изменение могло сломать что-то совершенно неочевидное. Dockerfile генерировался прямо на стороне агента, а логика сборки была размазана по коду с множеством условий «если фреймворк X — сделай так, если Y — иначе». Плюсом, жесткие зависимости от конкретных технологий и накопленный хардкод.
Поэтому старая система генерации Dockerfile тоже пошла под нож и была переписана с нуля. Теперь генерация Dockerfile полностью вынесена из кодовой базы и построена на связке:
Jinja-шаблонов Dockerfile;
YAML-конфигов с описанием поддерживаемых языков, версий, фреймворков и команд по умолчанию.
Вместо нагромождения условий есть плоские и читаемые шаблоны, которые легко менять и расширять. Добавить новый язык или фреймворк тоже просто — это обновление конфигов, а не переписывание логики сборки.
APPS Agent
Когда мы разобрали старую архитектуру, стало ясно: большая часть сбоев приходила именно со стороны агента. Он работал прямо на клиентской VDS, имел доступ ко всему окружению и одновременно отвечал и за сборку, и за запуск. И если проблему сборки мы решили, выделив отдельный Builder, то сам агент оставался узлом, через который проходили все критичные процессы и любое его падение вызывало каскад ошибок.
Мы изменили саму модель работы агента. Теперь он запускается только внутри изолированного контейнера без доступа к хосту, с ограниченными правами в RabbitMQ и отдельной защищенной веб-консолью.
Ядро агента построили на event-driven FSM, который управляет двумя сущностями:
Deploy — процесс развертывания (от prepare до success/failed);
App — жизненный цикл приложения (создано, активно, приостановлено, удалено).
Каждый переход — отдельное событие в RabbitMQ, а каждый обработчик выполняет строго одно атомарное действие: загрузка образа, запуск контейнера, настройка Caddy, health-check, распаковка статики и т.д. Если что-то падает, событие остается в брокере и будет обработано повторно после восстановления.
Логика разнесена по специализированным сервисам ContainerService, CaddyConfigurator, NetworkManager, StaticService. Это сняло хаос, уменьшило связанность и дало архитектуре масштабируемость.
Основная практическая ценность для пользователя — бесшовные обновления. Сейчас процесс обновления приложений выглядит так:

Это есть классический Blue-Green Deployment для пользователя, который раньше реализовать было невозможно из-за архитектуры.
Для команды разработки редизайн агента это большой шаг вперед:
агент теперь можно быстро обновлять под новые технологии;
все ключевые компоненты покрыты тестами;
новые шаги пайплайна добавляются декларативно;
изоляция и событийная модель делают систему безопаснее и прозрачнее.
Переработанный APPS-API
И последний из наших больших переделок — APPS-API, который координирует жизненный цикл приложений, инициирует деплои, синхронизирует состояние с панелью и определяет, что должно происходить в системе дальше. То есть API = оркестратор по бизнес-логике.
И так как он был тесно связан со старым агентом, страдал от race conditions, имел проблемы производительности и мало подходил под асинхронную работу с событиями, его мы тоже переписали с нуля. Вот что мы сделали:
Использовали Domain-Driven Design. Внутри API теперь два bounded context — App и Deploy, каждый со своими агрегатами, событиями и правилами.
Разделили слои, направили зависимости строго внутрь и изолировали логику от инфраструктуры, чтобы тестировать домены без запуска всей системы.
Добавили Dependency Injection для прозрачного управления зависимостями, единой точки конфигурации, гибкого расширения и аккуратной работы с сессиями.
Перешли на event-driven модель. Теперь все операции асинхронные. API публикует события в RabbitMQ (api.app.*, api.deploy.*, agent.{id}.deploy.*), а обработчик подписывается и выполняет действие. API не знает, кто именно исполнит событие: старый агент, новый агент или будущий исполнитель под Kubernetes.
Добавили распределенные блокировки через Redis и Unit of Work, чтобы решить race conditions и обеспечить согласованность транзакций.
То есть, новый API не управляет агентами напрямую, а только определяет что должно быть сделано, и уже агенты реагируют на события и выполняют. При всем этом, новый тип агента или новый исполнитель не требует менять API.
Так новый APPS-API дает пользователю предсказуемые деплои, стабильную работу приложений и более быстрый доступ к новым функциям, а у платформы есть устойчивое, модульное и масштабируемое ядро.
❯ Для тех, кто хочет просто завести свой проект в облако
Что в итоге получилось после такой масштабной перестройки?
Простой ответ: стабильно работающая и горизонтально расширяемая платформа, которая при этом осталась понятным сервисом «задеплой мое приложение», а не конструктором из сотни галочек. Мы даже название сменили с Apps на App Platform — настолько мы все переделали.
Мы не скрываем: в первом Apps было много решений, которые работали не так, как нам тогда казалось. Но так устроены MVP: они делаются быстро, под гипотезу, под ограниченные сроки, и почти всегда ломаются там, где не ждешь. Важно то, что мы научились открыто признавать ошибки, собирать обратную связь, смотреть правде в глаза и подходить к решению проблем комплексно.
Подходы, которые мы выработали в Kubernetes и смежных продуктах, напрямую повлияли на дизайн системы и позволили собрать ее такой, какой она должна быть у зрелого и масштабного продукта. Можно сказать, App Platform это результат архитектурной эволюции Timeweb Cloud.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

