Автоматизация бизнес-процессов заметно изменилась за последние годы. Если раньше во многих сценариях хватало маршрутизации, правил и несложной бизнес-логики, то сейчас в процессы всё чаще встраиваются более тяжёлые вычислительные задачи, например, интеграции с внешними AI-сервисами. Иными словами, автоматизация перестаёт быть только реакцией на событие и всё чаще становится вычислительным слоем внутри самого процесса. Но для того, чтобы система выдерживала нагрузку, нужен мощный язык исполнения.

В этой статье мы расскажем, как мы в «Первой Форме» реализовали это с помощью Python. Мы встроили его в контур платформы так, чтобы получить его сильные стороны для AI- и ресурсоёмких сценариев обработки данных, но не исполнять произвольный Python-код внутри бэкенда. Для нас это была не задача в духе «поддержать ещё один язык», мы хотели расширить платформу, не размывая границы безопасности и устойчивости ядра. 

Зачем платформе понадобился Python

Когда в платформе начинают появляться AI-сценарии, меняется не только бизнес-логика, но и технический профиль автоматизаций. В них становится больше задач, где нужно не просто проверить условие, вычислить значение поля или запустить короткое действие, а обработать текст, разобрать данные, использовать NLP-инструменты, обратиться к внешнему сервису. И часто для этого нужно обрабатывать огромный массив корпоративных данных.

Для такого класса задач Python давно стал фактическим стандартом из-за его экосистемы. Поэтому для нас вопрос стоял не так: «Нужен ли платформе ещё один язык?» Вопрос был другим: как безопасно дать платформе Python для нового класса автоматизаций, не превращая бэкенд в среду исполнения произвольного кода с тяжёлыми зависимостями.

Здесь начинается архитектурная часть истории. Проблема была не в добавлении нового языка как такового. Проблема была в том, чтобы не пустить Python внутрь процессного ядра.

Почему Python нельзя было просто встроить в бэкенд

Во многих платформах первый соблазн очевиден: если уже есть механизм скриптов, значит можно встроить в бэкенд ещё один и исполнять новый язык так же, как старые. На короткой дистанции это выглядит удобно. Но в случае с Python такой путь быстро создаёт системные риски.

Python — это не только сам язык, но и среда исполнения, зависимости, пакеты, вычислительно более тяжёлые сценарии и гораздо менее предсказуемый профиль нагрузки, чем у коротких скриптов, запускающихся в процессе. Если такой код работает внутри основного процесса бэкенда, то вместе с возможностями туда заходят и риски: утечки памяти, тяжёлые импорты, нестабильные зависимости, ошибки пользовательского кода, более сложная модель ограничения доступа и более дорогая цена сбоя.

Для enterprise-платформы это уже не вопрос удобства реализации, а вопрос архитектурных границ. Чем больше произвольного пользовательского кода выполняется внутри ядра, тем сложнее сохранять устойчивость системы, предсказуемость обновлений и понятную модель безопасности.

Поэтому для Python мы сознательно отказались от модели выполнения кода внутри одного процесса. Нам нужен был отдельный контролируемый контур исполнения, который можно изолировать, ограничить по контракту, обернуть таймаутами и при необходимости развивать независимо от ядра.

Что у нас уже было: две in-process-модели и третий, внешний путь

К моменту появления Python в платформе у нас уже были два языка смарт-скриптов: Lua и JavaScript. Они исполняются внутри бэкенда и хорошо подходят для внутренних автоматизаций, где нужны минимальная задержка и плотная интеграция с API платформы. Они дают быстрый запуск, прямой доступ к внутренним объектам платформы и подходят для тех случаев, где автоматизация живёт очень близко к ядру.

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

Для этого мы сделали внешний сервис Python Executor — FastAPI-микросервис, который принимает код скрипта и контекст по HTTP, исполняет их в изолированном окружении и возвращает JSON-результат обратно в платформу.

Маршрут исполнения выглядит так:

  1. Платформа инициирует выполнение смарт-скрипта. 

  2. Бэкенд определяет язык скрипта. Если это Lua или JavaScript, используются внутренние движки. Если это Python, бэкенд идёт по отдельной ветке и вместо локального запуска отправляет HTTP-запрос в Python Executor.

При этом сам скрипт хранится не в контейнере сервиса, а в платформе — в таблицах SmartScripts и SmartScriptsVersions, вместе с другими смарт-скриптами. Это принципиальный момент: контейнер не содержит бизнес-логику клиента и не выступает хранилищем сценариев. Он получает код на лету, исполняет его и возвращает результат. 

Как устроен контур выполнения Python-скрипта

С технической точки зрения бэкенд формирует HTTP-запрос к Python Executor и передаёт туда три основные сущности: код скрипта, контекст выполнения и таймаут.

Основной маршрут для платформенных автоматизаций — это POST /execute/code. В этом режиме сервис получает произвольный Python-код из SmartScripts, исполняет его и возвращает структуру с результатом, статусом, текстом ошибки при необходимости, строковым output и длительностью выполнения.

Кроме этого, у сервиса есть и второй режим — POST /execute, где можно запускать предустановленные скрипты по имени. Такой режим подходит для заранее подготовленных утилит. Но для архитектуры платформенного Python важнее POST /execute/code, потому что он делает Python частью общей модели SmartScripts.

Чтобы такой подход был предсказуемым, у Python-скрипта должен быть очень чёткий контракт. У нас точкой входа является функция execute(ctx). Скрипт получает контекст через параметр ctx, а результат возвращает обычным return.

Это важное отличие от других скриптовых моделей, где результат может задаваться через платформенные переменные. Для Python мы выбрали более естественный контракт, совпадающий с обычной моделью языка.

Контекст формируется бэкендом с фильтрацией типов. Примитивные значения — строки, числа, булевы значения, даты — передаются как есть. Если в контексте есть EntityBase, в Python передаётся только его идентификатор. Сложные внутренние объекты платформы не пробрасываются. Также автоматически добавляется session_user_id.

И здесь находится одно из главных ограничений всей модели: Python-скрипт не получает прямого доступа к API платформы. У него нет доступа к внутренним объектам системы для работы с базой данных, HTTP-запросами, кэшем и файловой системой, которые доступны внутренним in-process-движкам. Он может работать только с тем, что бэкенд явно положил в ctx, и с теми библиотеками, которые разрешены и доступны в контейнере.

Мы планируем и дальше развивать эту часть — например, добавить асинхронное исполнение с callback для более тяжёлых сценариев. Это естественный шаг, потому что AI- и data-processing-задачи не всегда хорошо укладываются в короткую синхронную модель. Но уже в текущем виде схема решает главную задачу: бэкенд остаётся оркестратором, а Python Executor — исполнителем.

Почему это безопаснее: изоляция, песочница и ограниченный контур

Вынести Python в отдельный сервис — полезно, но само по себе этого недостаточно. Если просто запускать произвольный код во внешнем контейнере без ограничений, риски никуда не исчезают, а только переезжают в другой процесс. Поэтому следующий слой архитектуры — это песочницы.

В Python Executor используется модель white list, а не black list. Идея в том, что разрешён только ограниченный набор builtins и модулей, а потенциально опасные механизмы заранее вырезаны. В частности, запрещены вызовы вроде os.system, subprocess, eval, exec, import, операции записи через open, а также sys.exit, signal, breakpoint. Прямой доступ к файловой системе контейнера тоже ограничен.

Это даёт гораздо более управляемую модель исполнения, чем попытка бесконечно расширять чёрные списки опасных путей. Белые списки жёстче, но они лучше подходят для слоя, который исполняет пользовательский код.

Дополнительно Python ограничен и по времени выполнения. Python Executor использует более консервативный таймаут по умолчанию — 30 секунд против до 5 минут у внутренних движков Lua и JavaScript. Это связано с моделью внешнего HTTP-вызова: синхронный запрос к отдельному сервису не должен блокировать бэкенд на минуты. Для тяжёлых сценариев планируется асинхронный режим с callback, но в текущей синхронной модели таймауты жёстче.

Итог здесь простой: безопасность достигается не одним механизмом, а комбинацией решений — вынесение из core, отдельный процесс, отдельный контейнер, ограниченный контракт контекста, whitelist-модель и короткий таймаут.

Что даёт отдельный сервис на практике

Отдельный сервис также даёт и практическую инженерную выгоду: позволяет собрать в окружении тот набор Python-зависимостей, который нужен для AI- и тяжёлых сценариев, не затягивая всю эту экосистему внутрь бэкенда.

В контейнере заранее доступны библиотеки для HTTP-вызовов, табличной обработки, Excel, SQL-разбора, NLP и AI-интеграций. В документации зафиксированы, в частности, requests, pandas, openpyxl, sqlglot, pymorphy3, nltk, rapidfuzz, openai. На builder-стадии устанавливаются зависимости, требующие компиляции, а runtime-стадия остаётся более лёгкой и не содержит компилятор. NLP-данные скачиваются при сборке образа, а не в момент выполнения скрипта. 

Оценочный размер такого контейнера — около 630 MB, но в данном случае его размер — это осознанная плата за наличие полноценного Python-runtime с готовыми библиотеками внутри изолированного контура. Взамен бэкенд остаётся чище, а Python-слой — самостоятельнее и предсказуемее. 

Безопасность здесь строится также на протоколе вызова сервиса. Python Executor принимает запросы по API-ключу через заголовок X-Api-Key. Значение сверяется с EXECUTOR_API_KEY, передаваемым через environment. При этом чувствительные данные не хранятся в образе постоянно: например, отдельные служебные токены могут приходить в контексте конкретного вызова, а не храниться внутри контейнера как постоянный секрет. Это уменьшает поверхность риска и лучше согласуется с моделью изолированного runtime.

С эксплуатационной точки зрения сервис живёт как отдельный Docker-контейнер, проверяется, логирует запросы и может обновляться независимо от ядра платформы. Это важно не только для DevOps-удобства, но и для архитектуры в целом. 

Главный инженерный вывод

Когда платформа начинает всерьёз встраивать AI в процессы, главный вопрос оказывается не в выборе языка как такового. Намного важнее другое: где исполняется этот код, какие данные он получает и какие границы ему заранее заданы.

Мы добавили Python в платформу не как ещё один встроенный движок, а как отдельный контролируемый контур. С HTTP-вызовом, ограниченным контекстом, sandbox, коротким таймаутом, API-ключом и изолированным runtime. Это позволило нам расширить платформу под новый класс сценариев, не размывая границы безопасности ядра.

И, возможно, в этом и состоит главный смысл всей истории. Безопасная расширяемость — это не когда новый язык «умеет всё» и получает полный доступ ко всему окружению. Безопасная расширяемость — это когда для нового языка изначально правильно спроектирован его контур: что он может, чего не может, где он живёт и почему его ошибка не должна становиться ошибкой всего бэкенда.

Комментарии (0)