Как безопасно запускать чужой код: Firecracker microVM и тёплые пулы

Когда сервис выполняет чужой код, ему нужна песочница

Как безопасно запускать чужой код: Firecracker microVM и тёплые пулы

AI-ассистенты по программированию, интерпретаторы кода, автономные агенты — рано или поздно все они делают одно и то же: выполняют код внутри AI-стека, которому нужна не только модель, но и безопасная инфраструктура исполнения. Модель генерирует скрипт, пользователь отправляет фрагмент, агент решает что-то запустить. Этот код должен где-то выполняться.

Запускать его на хосте напрямую — не вариант. Стоит это сделать, как вы начинаете доверять каждой строке пользовательского или модельного кода — что он не прочитает чужие файлы, не откроет лишние сетевые соединения, не выжрет всю память, не устроит fork-бомбу. Такое доверие неразумно. Нужна изоляция.

И вот тут возникает противоречие. Изоляция на уровне общего ядра слишком слабая для всего, что реально важно защитить. Полный запуск VM — слишком медленный для всего, что должно ощущаться интерактивным. Долгое время нормального решения посередине не было. Тёплые microVM на базе Firecracker — это и есть такое решение: аппаратная изоляция ядра и получение sandbox меньше чем за 5 мс на одной машине. Вот как это работает и почему тёплый пул — самая важная часть.

Компромисс изоляции

Каждая команда, которая строит выполнение кода, проходит через одну и ту же последовательность. Сначала `subprocess` — работает, отгружаем, идём дальше. Потом кто-то из безопасности спрашивает, что будет, если пользователь пришлёт `import os; os.system(“rm -rf /”)`. Ответ неприятный. Переходите на Docker. Docker ощущается надёжным: неймспейсы, cgroups, целая экосистема инструментов. А потом вы читаете про container escapes — и это ощущение пропадает.

Проблема не в том, что эти инструменты плохие. Ни один из них не проектировался под конкретную модель угроз — запуск по-настоящему недоверенного кода от незнакомых людей на инфраструктуре, которую вам важно защитить. Чтобы понять, почему Firecracker — правильный ответ, нужно честно разобраться, что каждая альтернатива реально даёт и где ломается.

ПодходИзоляция ядраВремя стартаПроблема
subprocessНет~0 мсПолный доступ к хосту
DockerНет (общее ядро)~500 мсKernel exploits работают
gVisorЧастичная~200 мсПерехват syscall, не настоящая VM
Полная VM (KVM)Да10–30 сСлишком медленно
Firecracker microVMДа~125 мс

subprocess — очевидно нет: произвольный код запускается прямо на хосте с полным доступом к файловой системе, сети и дереву процессов. Любой код, который хочет навредить, — навредит.

Docker — стандартный ответ, и он неправильный для этой задачи. Контейнеры дают изоляцию процессов и отдельное пространство файловой системы, но ядро общее между хостом и всеми контейнерами. В этом и есть фундаментальная проблема. Container escapes — уязвимости, позволяющие коду внутри контейнера вырваться на хост — это задокументированный, регулярно встречающийся класс атак. Они происходят потому, что поверхность общего ядра большая и сложная, а атакующие умеют находить её края.

gVisor — более серьёзная попытка решить задачу. Он работает в пространстве пользователя и перехватывает каждый системный вызов процесса, реализуя их заново в безопасном изолированном контексте, а не передавая напрямую ядру. Это действительно сокращает поверхность атаки. Но это не настоящая изоляция ядра — у gVisor есть собственная кодовая база и собственная поверхность атаки, а оверхед от перехвата и перереализации syscall добавляет и латентность, и сложность. Лучше Docker по безопасности, но жёсткой границы всё равно нет.

Полные VM решают проблему ядра. Когда поднимается настоящая KVM-машина, гость получает своё ядро, и баг в этом ядре не затрагивает хост. Именно такой уровень изоляции и нужен. Проблема — время загрузки: полная VM стартует от 10 до 30 секунд в зависимости от образа и железа. Для интерпретатора кода, которому нужна свежая среда на каждый запрос, это неприемлемо.

Firecracker находится в промежутке между этими вариантами. Это VMM (Virtual Machine Monitor), который AWS создал для Lambda и Fargate — специально под сценарий запуска множества короткоживущих изолированных нагрузок. Философия проектирования — радикальный минимализм: убрать всё, без чего microVM может обойтись. Никакой эмуляции USB. Никакого BIOS. Никакой поддержки устаревшего железа, неактуального уже десять лет. В результате — кодовая база около 50 000 строк, против миллионов в QEMU, на котором работает большинство полнофункциональных VM. Меньше кода — меньше поверхность атаки — меньше мест, где могут прятаться уязвимости.

Тёплый пул

Итак, Firecracker даёт 125 мс. Реальный прогресс — с 10–30 секунд. Но это число, которое пользователь всё равно ощущает. Нажал «запустить», подождал восьмую долю секунды, увидел результат. Не катастрофа. Но и не незаметно — а инструменты для программирования на базе ИИ живут и умирают именно от того, насколько отзывчиво они ощущаются. 125 мс на sandbox при нагрузке также означает, что инфраструктура тратит значительную часть времени просто на загрузку VM, а не на выполнение кода.

Есть лучший способ думать об этой проблеме. Вопрос не «как сделать загрузку VM быстрее» — это задача железа и ядра, не решаемая на уровне кода приложения. Настоящий вопрос: обязательно ли загрузка VM происходит на критическом пути вообще? Ответ — нет. Если загрузка VM по запросу занимает 125 мс, не грузите по запросу. Грузите заранее. Держите пул уже запущенных VM, каждая из которых полностью инициализирована — с собственным ядром, rootfs и сетевым интерфейсом — в режиме ожидания. Когда приходит запрос, берёте одну из пула — это займёт несколько миллисекунд — и сразу же запускаете в фоне новую ей на замену.

Клиент              API                    Пул
  |                  |                      |
  |-- POST /sandboxes->|                    |
  |                  |<-- acquire warm VM --|
  |<-- sandbox_id ---|                      |
  |                  |-- boot new VM ------>| (фоново)
  |                  |                      |
```

Со стороны клиента — почти мгновенно. Штраф холодного старта в 125 мс никуда не делся — он просто происходит до прихода запроса, а не во время него. В устойчивом состоянии всегда есть готовая VM. Задержка получения стабильно держится ниже 5 мс вне зависимости от количества выданных sandbox, потому что никакой реальной загрузки на критическом пути нет.

Это тот же паттерн, что и пул соединений к базе данных. Открыть новое соединение с БД дорого: TCP-хендшейк, аутентификация, установка сессии. Поэтому поднимают пул соединений при старте и переиспользуют их — дорогая операция выносится за пределы горячего пути. Тёплый пул VM делает то же самое, только для виртуальных машин.

Сценарий сбоя, который нужно понимать заранее: если трафик вырастет быстрее, чем пул успевает пополняться, запросы начнут попадать на холодные старты. Пул временно исчерпывается, новые запросы ждут загрузки свежей VM — 125 мс вместо 5 мс. Два параметра, которые это контролируют: глубина пула (сколько тёплых VM держать в запасе) и скорость пополнения (как быстро стартует новая VM, когда одна взята из пула). Настройка этих параметров под ожидаемые паттерны трафика — основная операционная задача при эксплуатации Pandora в продакшне.

Полезный блок со скидкой

Если после Firecracker, microVM, тёплых пулов, FastAPI, rootfs, сетевой изоляции и метрик стало понятно, что здесь нужна крепкая инженерная база, можно начать с фундаментальных направлений: Python, backend, тестирование или инфраструктура. Это не курс по Firecracker, но такая база помогает лучше понимать серверы, API, контейнеры, изоляцию и безопасный запуск кода.

Для старта держите промокод Практикума на любой платный курс: KOD (можно просто нажать). Его можно просто нажать и применить при покупке — он даст скидку и поможет сэкономить на обучении.

Бесплатные курсы в Практикуме тоже есть — по всем специальностям и направлениям, начать можно в любой момент, карту привязывать не нужно, если что.

Как это устроено

Описать концепцию тёплого пула несложно. Реализация — вот где начинается интересное, потому что поддерживать флот предзапущенных VM на одном хосте означает управлять множеством движущихся частей: сетевые интерфейсы, которые нужно создавать и сносить; образы rootfs, которые нужно клонировать для каждого тенанта; процессы Firecracker, которые нужно конфигурировать, грузить, мониторить и убирать. Система построена в три слоя, у каждого — чётко определённая задача.

Слой пула и API (api.py)

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

Три основных эндпоинта: `POST /sandboxes` берёт тёплую VM из пула и возвращает sandbox ID; `POST /sandboxes/{id}/exec` передаёт команду в VM через SSH и возвращает результат; `DELETE /sandboxes/{id}` уничтожает VM и освобождает её ресурсы.

Есть и фоновый idle reaper. Sandbox-ы, которые долго не трогали, или которые клиент бросил без явного удаления, со временем накапливаются. Reaper запускается с настраиваемым интервалом и принудительно завершает любой sandbox, превысивший таймаут простоя, возвращая слот в пул. Именно так Pandora избегает медленных утечек ресурсов от клиентов, отключившихся без cleanup.

Метрики Prometheus экспортируются на `/metrics`, охватывая каждую фазу жизненного цикла sandbox.

Жизненный цикл VM (vm_manager.py)

Этот слой отвечает за всё ниже абстракции пула: реальный запуск процессов Firecracker, их конфигурирование, ожидание готовности и чистое завершение.

При загрузке каждая VM получает полный изолированный набор ресурсов:

  • Приватная копия rootfs. Каждая VM получает собственную копию корневой файловой системы, клонированную из базового образа. Это принципиально: если бы VM делили один образ, запись одного тенанта была бы видна другому. Приватные копии обеспечивают полную изоляцию файловой системы между sandbox-ами.
  • TAP-интерфейс и выделенная подсеть /30. Каждая VM получает собственный виртуальный сетевой интерфейс на хосте и IP-адрес из приватной подсети, принадлежащей только ей. Так сетевая изоляция обеспечивается на уровне устройства.
  • Unix domain socket. Firecracker управляется через REST-подобный API на Unix-сокете. У каждой VM свой сокет, через который она получает начальную конфигурацию (образ ядра, путь к rootfs, сетевые настройки) и по которому можно запросить её статус.
  • SSH-ключевая пара. Перед загрузкой VM генерируется свежая ключевая пара, и публичный ключ внедряется напрямую в rootfs гостя. Именно это позволяет API-слою заходить в VM по SSH без пароля и выполнять внутри неё команды.

Как только VM запущена и SSH доступен, `vm_manager` помечает её как тёплую и добавляет в пул. С этого момента вызовы `exec` обрабатываются открытием SSH-сессии в VM и выполнением команды внутри гостя. Сессия переиспользуется между несколькими вызовами `exec` в рамках одного sandbox — файловое состояние накапливается между вызовами: файл, записанный в одном вызове, читается в следующем. При уничтожении sandbox SSH-сессия закрывается, процесс Firecracker убивается, TAP-интерфейс удаляется, копия rootfs стирается.

Сетевая изоляция (setup_network.sh)

Но VM не должны иметь возможности обращаться друг к другу, и хост не должен принимать несанкционированные входящие соединения от кода VM. Скрипт настройки сети конфигурирует правила iptables для соблюдения этих ограничений. Правило MASQUERADE делает NAT исходящего трафика каждой VM через сетевой интерфейс хоста, давая VM доступ в интернет. Отдельные правила дропают входящий трафик на IP VM и блокируют межвмешный трафик. Каждая VM видит интернет — и больше ничего из локальной сети.

Использование

Всё это — загрузка VM, внедрение SSH-ключей, настройка сети, клонирование rootfs — снаружи не видно. Клиентский API намеренно прост: попросили sandbox, выполнили команды, готово. Вся сложность спрятана за HTTP-интерфейсом.

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

from pandora.client import PandoraClient

async with PandoraClient("http://localhost:8000") as client:
    async with client.sandbox() as sandbox:
        result = await sandbox.exec("python3 -c 'print(2 + 2)'")
        print(result.stdout)  # 4

        # файловое состояние сохраняется между вызовами exec
        await sandbox.exec("echo 'hello' > /tmp/state.txt")
        result = await sandbox.exec("cat /tmp/state.txt")
        print(result.stdout)  # hello

`client.sandbox()` берёт тёплую VM из пула и возвращает объект `Sandbox`. Все вызовы `exec` на этом объекте выполняются внутри одной VM, в одном контексте файловой системы. Когда блок `async with` завершается — штатно или через исключение — sandbox уничтожается, VM завершается, слот освобождается.

API поддерживает и явное управление жизненным циклом для случаев, когда паттерн контекстного менеджера не подходит:

sandbox = await client.create_sandbox()
result = await sandbox.exec(
    "pip install numpy && python3 -c 'import numpy; print(numpy.__version__)'"
)
await client.destroy_sandbox(sandbox.id)

Производительность

Числа легко заявить и трудно принять на веру, особенно в теме латентности. «Быстро» — не число. Вот реальные метрики Prometheus под устойчивой нагрузкой на одном хосте, с контекстом для каждого значения:

  • Acquire latency p50: ~3 мс (тёплый пул)
  • Acquire latency p99: ~8 мс (тёплый пул)
  • Cold start p50: ~125 мс (пул исчерпан)
  • Exec overhead: ~1–2 мс SSH round-trip, без учёта времени выполнения команды

p99 в 8 мс отражает редкую конкуренцию, когда несколько запросов приходят одновременно и борются за одни слоты пула. При нормальной нагрузке с правильно подобранным размером пула p50 и p99 держатся близко.

Из коробки поставляется десять метрик:

  • Гистограммы латентности для boot, acquire, exec и teardown — видно, куда именно уходит время на каждой фазе жизненного цикла VM
  • Счётчики успешных и неудачных выполнений команд по отдельности
  • Gauge-ы для количества активных sandbox и глубины тёплого пула в реальном времени

Gauge глубины тёплого пула — то, за чем нужно следить при настройке размера пула. Если он регулярно уходит в ноль до того, как пополнение успевает наверстать, пул занижен под ваш трафик. Grafana-дашборд автоматически разворачивается через Docker Compose и включает временны́е ряды для всех этих метрик — поведение пула видно без ручной настройки.

Что реально даёт тёплый пул

Есть соблазн, представляя такую систему, сделать её похожей на магию. Получение VM меньше чем за 5 мс звучит так, будто мы сделали виртуальные машины быстрыми. Нет. Стоит быть точным в том, что тёплый пул делает и не делает.

Он не ускоряет загрузку VM. Firecracker по-прежнему грузится ~125 мс. Тёплый пул это число не меняет. Что он делает — полностью убирает эти 125 мс из пути запроса. Время загрузки становится фоновой стоимостью обслуживания, а не налогом на каждый запрос.

Практический результат: в устойчивом состоянии запуск sandbox неотличим от получения соединения из пула БД или чтения из кэша в памяти. Вся лежащая под капотом сложность — загрузка ядра, инициализация rootfs, внедрение SSH-ключа, настройка сетевого интерфейса — полностью невидима вызывающему. Изоляция уровня VM при скорости получения как у кэша.

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

Распределённая версия — несколько рабочих узлов, каждый со своим пулом Firecracker, скоординированные общим реестром sandbox с планировщиком, учитывающим ёмкость — распространяет эту модель на флот. Когда одного хоста недостаточно, та же логика тёплого пула работает на уровне кластера. Об этом — в следующей статье.

Код открытый: github.com/daukadolt/pandora

Советуем дополнительно почитать по теме

12 AI GitHub-репозиториев 2026: Ollama, n8n, Claude Code и OpenHands — инструменты для локального запуска моделей, автоматизации без подписок и сборки AI-агентов: Ollama, Open WebUI, Dify, n8n, Claude Code, LangChain и другие.

15 скиллов для AI-агентов: один раз настроил — агент запомнил — как упаковать повторяемые инструкции, правила и рабочие сценарии в скиллы для Claude Code, Cursor, Gemini CLI и других агентных инструментов.

20 AI GitHub-репозиториев для разработчика в 2026 году — подборка репозиториев для локальных моделей, инференса, RAG, агентов и работы с проектом из терминала.

Роадмап Golang: путь от нуля до джуниора в 2026 — что учить Go-разработчику: конкурентность, backend, PostgreSQL, тестирование, Docker и инфраструктурные навыки, которые пригодятся в системной разработке.

Бонус для читателей

Если вам интересно погрузиться в мир IT и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.

Вам может быть интересно
hard