Написанный за неделю прототип живёт на ноутбуке разработчика: FastAPI, ключ к модели в переменной окружения, база знаний в файле рядом. Это всё тот же агент поддержки, на котором мы считали расходы. Отвечает он осмысленно, и кажется, что осталась формальность: перенести всё это на сервер и дать людям адрес.
На сервере у агента появляется свой набор проблем. Python другой версии, и библиотека падает на импорте. Ключ нельзя оставить в переменной окружения, потому что переменную кто-то должен туда положить, а репозиторий читает вся команда. Первый запрос после ночной тишины приходит на несколько секунд позже остальных. Ответ, который локально печатался по словам, теперь молчит до самого конца генерации и вываливается целиком, потому что прокси по дороге накапливает его у себя. А когда агенту разрешают посчитать что-нибудь по выписке клиента написанным на ходу кодом, оказывается, что этот код исполняется в том же контейнере, где лежит ключ к модели.
Ни одна из этих проблем не про языковую модель. Модель тут один компонент, а контейнер, сеть, очередь и песочницу вокруг неё называют обвязкой (harness). Всё это обычная инженерия, и меряют её обычными мерками. За каждое свойство здесь чем-то платят. Готовность отвечать сразу оплачивается счётом за инстанс, то есть за запущенный экземпляр контейнера, который большую часть суток ничего не делает. Код, который агент написал на ходу, запускают в песочнице, одноразовой среде, поднятой под один запуск и погашенной сразу после него. Внутри такой среды диск и сеть работают медленнее.
Дневник курса, урок 24. Здесь понадобится разобранное раньше: спектр изоляции и место песочницы в модели угроз (урок 9) — почему обычного контейнера мало для чужого кода; счёт агента и его раскладка (урок 22) — к нему добавится инфраструктурная строка; состояние агента между шагами (урок 6) — оно всплывёт, как только работа уедет в фон. Пост читается отдельно: все термины вводятся заново.
Сквозным примером остаётся всё тот же чат-бот банка, а числа у него из учебного расчёта: 8000 обращений в сутки, диалог в пятнадцать ходов, $36 за модели после оптимизации. К этому набору добавим одну новую способность, ради которой и городится половина всего дальнейшего. Агент умеет разобрать выписку клиента, а для этого пишет и выполняет небольшой скрипт на Python.
Содержание
- Один образ на все среды и то, что в него не кладут
- Холодный старт: за готовность платят простоем
- Очередь: когда ответ не помещается в HTTP-запрос
- Стриминг: как поток токенов теряется по дороге
- Аппаратная граница: 125 миллисекунд и при каких условиях
- Во что обходится изоляция, когда песочница уже поднята
- Побег из песочницы через разрешённый маршрут
- Почему контроль ослабляют и что ставят вместо подтверждений
- Итог
- FAQ
- Источники
1. Один образ на все среды и то, что в него не кладут
Перенос агента на сервер начинается с мелочей, каждая из которых по отдельности выглядит недоразумением. Версия интерпретатора не совпала. Зависимость, поставленную руками полгода назад, никто не записал. Ключ экспортировали в консоли один раз и забыли. Ответ на все три случая общий: артефактом релиза становится не код, а образ. В dev, stage и prod едет один и тот же образ — различается только конфигурация.
Половина смысла упаковки лежит в том, чего в образе нет. Разворачивают его ревизиями — отдельными запусками сервиса, у каждого своя конфигурация, и поменять её можно без пересборки. Переменные окружения делятся на две породы. Идентификатор каталога или уровень логирования тайны не составляют и спокойно лежат в описании ревизии. Ключ к модели и пароль к базе устроены иначе: их кладут в менеджер секретов, а контейнер получает значения при старте ревизии.
Ротация ключа перестаёт быть событием в жизни кода. Меняется секрет, поднимается новая ревизия на том же образе, сборка и тесты не запускаются вовсе.
один образ agent:1.4.0 конфигурация ревизии
┌────────────────────────────┐ + ┌──────────────────────────────┐
│ код · uv.lock · зависимости│ │ FOLDER_ID, LOG_LEVEL, PG_HOST│
│ модель эмбеддингов │ │ MODEL_API_KEY ← секрет │
└─────────────┬──────────────┘ │ PG_PASSWORD ← секрет │
│ └──────────────────────────────┘
┌─────────────┼─────────────┐ подставляется при старте
▼ ▼ ▼ ревизии, в образ не попадает
локально облако контур
compose serverless без сетиagent:1.4.0 разворачивается в трёх средах: локально через compose, в облаке как serverless-контейнер, в закрытом контуре снова через compose. Различается только конфигурация ревизии, причём ключ к модели и пароль к базе подставляются из менеджера секретов при старте и внутрь образа не попадают.Сеть строится по тому же принципу разделения. Сам контейнер и база данных живут в приватной подсети и наружу не смотрят, а публично доступен один шлюз, который проксирует запросы внутрь и заодно держит на себе маршрутизацию, таймауты и авторизацию. Исходящие вызовы к API модели уходят через отдельный NAT-шлюз, и это не бюрократия. У трафика наружу появляется одна точка, где его можно разрешить, запретить и посмотреть.
Та же логика продолжается в описании инфраструктуры кодом. Настройка облака руками воспроизводима ровно один раз, пока помнишь, что кликал. Terraform описывает облако файлами вместо кликов в консоли: контейнеры, база, секреты и сетевые правила лежат в репозитории рядом с кодом. Изменение сетевого правила приезжает через тот же пул-реквест, что и правка промпта. Как этот образ потом доезжает до людей — не сразу на всех, а ступенями и с заранее записанными порогами отката — отдельный сюжет двадцать пятого урока.
Ошибаются здесь на архитектуре процессора. Образ, собранный на ноутбуке с ARM, в облаке на amd64 просто не стартует, и сообщение об ошибке говорит про формат исполняемого файла, а не про платформу. Явное указание целевой платформы при сборке снимает вопрос, но узнаёшь об этом обычно на первом деплое.
2. Холодный старт: за готовность платят простоем
Образ уехал в облако, сервис отвечает. Почему же первый утренний клиент ждёт ответа заметно дольше, чем все следующие? У serverless-контейнера нет постоянно работающего процесса: платформа убирает инстансы, которые не обрабатывают запросы, и первый запрос после паузы оплачивает подъём контейнера и импорт всего, что агент тянет за собой.
Обходов у этой задачи три, и стоят они по-разному. Можно держать минимум инстансов постоянно прогретыми. Дешевле всего будить сервис запросом по расписанию, хотя после долгой паузы всплеск всё равно упрётся в холодные инстансы. Можно вынести тяжёлое из образа, чтобы многогигабайтные файлы своей модели не загружались при каждом подъёме. Первый обход самый надёжный, и цена у него открытая.
Арифметика по ценнику считается за минуту. В Cloud Run секунда работающего vCPU стоит $0,000024, а секунда того же vCPU в простое при заданном минимуме инстансов стоит $0,0000025, то есть в 9,6 раза дешевле. Память тарифицируется по одной ставке в обоих режимах. Пересчёт на месяц (730 часов) для конфигурации 1 vCPU / 2 ГиБ даёт $19,71 за постоянную готовность против $76,21, если тот же инстанс все 730 часов действительно обрабатывает запросы. Страховка от холодного старта обходится примерно в четверть цены полностью загруженного инстанса.
Модель тарификации важнее самой ставки. Пониженная цена простоя действует при оплате по запросам; при оплате за время жизни инстанса вы платите полный тариф всё время его существования, даже если минимум инстансов выставлен в ноль. Так что одна и та же настройка минимума инстансов в двух режимах биллинга стоит совсем разных денег.
Наш агент попадает в эту историю один раз в сутки. Восемь тысяч обращений распределены по дню неравномерно, ночью поток стихает, и клиент, написавший первым утром, ждёт дольше остальных. Ответ приходит к нему кусками, и первую единицу этого потока называют первым токеном. Ждать её приходится не привычные полсекунды, а всё время подъёма контейнера с тяжёлыми импортами. Двадцать долларов в месяц рядом с $36 в сутки, которые наш агент отдаёт за модели, на разговор о деньгах не тянут. Правда, и покупают эти двадцать долларов несколько секунд ожидания в одном обращении из восьми тысяч.
Ловушка тут в самой цифре «один». Прогрет ровно один инстанс, и обслуживает он столько запросов, сколько ему разрешено обрабатывать одновременно (конкурентность инстанса); при утреннем всплеске второй, третий и четвёртый поднимаются с нуля и снова заставляют клиента ждать. Считать прогретые инстансы надо по пиковой скорости запросов, а не по суточному среднему.
Тот же счёт задаёт и границу, за которой serverless перестаёт окупаться. Если прогретых инстансов приходится держать столько, что месячный счёт за простой догоняет цену постоянно работающей машины, платформа берёт деньги за масштабирование, которым никто не пользуется. Два других признака читаются так же. Долгая задача, которая должна пережить запрос, в эфемерную модель исполнения не помещается, а на ровной круглосуточной нагрузке обычный инстанс просто дешевле. Пока ни один из трёх не сработал, serverless обычно и дешевле, и проще в эксплуатации.
3. Очередь: когда ответ не помещается в HTTP-запрос
Пока агент отвечает на вопрос про тариф, синхронного запроса хватает. Но клиент просит сформировать справку по операциям за год, агент идёт в базу, потом в модель, потом снова в базу и крутит цикл четыре минуты. Долгую работу вынимают из HTTP-запроса целиком, и обвязка агента прирастает очередью: API кладёт задачу в брокер сообщений и сразу возвращает идентификатор, а разбирают её отдельные воркеры.
Синхронный вариант ломается двумя разными способами, и оба обидные. Шлюз рвёт соединение по таймауту, потому что таймаут там измеряется десятками секунд, а агент работает минуты. Клиент закрывает вкладку, и вместе с соединением исчезает вся проделанная работа, за которую провайдеру модели уже заплачено. Асинхронная схема обе потери убирает, потому что воркер доводит задачу до конца независимо от того, смотрит на неё кто-нибудь или нет.
За это приходится принять два требования, без которых очередь в проде разваливается. Брокер гарантирует доставку «хотя бы один раз» (at-least-once), а не «ровно один раз», и повторная доставка события рано или поздно случится. Если обработка задачи возвращает деньги клиенту или пишет в базу без ключа идемпотентности (признака, по которому обработчик узнаёт уже выполненную операцию и не делает её второй раз), повтор превращается в инцидент. Второе требование касается сбоев. Сообщения, на которых обработка сломалась, уводят в отдельную очередь недоставленных сообщений (dead letter queue). Без неё битое событие либо теряется молча, либо бесконечно возвращается в основной поток и отравляет его.
СИНХРОННО
[клиент] ──HTTP──> [шлюз] ──> [агент крутит цикл 4 минуты]
▲ │
└── таймаут шлюза / закрытая вкладка ──┘
работа потеряна, и она уже оплачена
ЧЕРЕЗ ОЧЕРЕДЬ
[клиент] ──POST──> [API] ──> [брокер] ──> [воркер 1..N]
▲ │ токены
└─ поток событий ─ [стрим-сервер] <─ Redis ┘
масштабировать по глубине очереди,
а не по загрузке процессораСамое неочевидное здесь то, по какой метрике воркеров становится больше. Автомасштабирование по умолчанию смотрит на загрузку процессора, и для обычного веб-сервиса это разумно. Воркер агента почти всё своё время ждёт ответа модели по сети, процессор у него при этом близок к нулю, и автомасштабирование видит недогруженных воркеров. Дальше нагрузка и автомасштабирование начинают тянуть в разные стороны: чем длиннее очередь, тем ниже средняя загрузка процессора и тем меньше воркеров остаётся. Масштабировать такую нагрузку надо по числу задач, ожидающих в брокере. Считает их отдельный компонент, например, KEDA, которая читает длину очереди и сама задаёт число реплик.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: agent-worker-scaler
spec:
scaleTargetRef:
name: agent-worker
minReplicaCount: 0 # без задач воркеров нет вовсе
maxReplicaCount: 50
cooldownPeriod: 300 # не сворачивать сразу после всплеска
triggers:
- type: rabbitmq
metadata:
queueName: agent-tasks
targetQueueLength: "3" # один воркер на каждые три задачи в очереди
Дороже всего в такой схеме обходится эксплуатация. Появляется брокер, который надо обслуживать, и автомасштабирование по очереди со своими метриками. А если модель вдобавок крутится тут же, на своём железе (локальный инференс вместо вызова чужого API), к обычным узлам добавляются узлы с видеокартами, и пулы между ними приходится разделять. Состояние самой задачи между попытками остаётся отдельным сюжетом, разобранным в шестом уроке, где агент собран как машина состояний с точками сохранения.
Заодно пропадает возможность посмотреть глазами, что агент делал: запрос вернул идентификатор задачи и закрылся, а вся работа прошла в чужом процессе минутой позже. Отвечает на это трассировка шагов, разобранная в девятнадцатом уроке.
4. Стриминг: как поток токенов теряется по дороге
Справка готовится в фоне, а вот ответа на обычный вопрос человек ждёт прямо сейчас, глядя в экран. Отдать этот ответ можно двумя способами. Можно дождаться конца генерации и выслать всё разом, и тогда человек несколько секунд смотрит на пустой экран. Можно слать токены по мере появления, и первые слова возникают примерно через полсекунды. Второй способ называют стримингом, а полсекунды до первого токена считаются ориентиром хорошего интерфейса. Технически токены отдают через Server-Sent Events (SSE) — однонаправленный поток текстовых событий поверх обычного HTTP, и браузер сам переподключает этот поток при обрыве. Байты дойдут до браузера только при условии, что ни один узел по дороге их не копит.
По умолчанию копит nginx, а с ним и большинство периметров. Обратный прокси собирает ответ бэкенда в память, чтобы отдать клиенту одним куском, и для обычной страницы это правильно: меньше системных вызовов, выше пропускная способность. Для потока токенов та же настройка сводит стриминг на нет: токены лежат в памяти прокси до конца генерации, и пользователь снова смотрит в пустой экран. Лечится это двумя способами. Либо буферизацию отключают в конфигурации прокси для путей стриминга, либо бэкенд ставит на конкретный ответ заголовок X-Accel-Buffering: no и общую конфигурацию не трогает.
Второе ограничение живёт в браузере и удивляет тем, что его не собираются чинить. Поверх HTTP/1.1 браузер держит к одному домену не больше шести одновременных соединений, а каждое открытое SSE-соединение занимает один слот. Три вкладки с агентом съедают половину лимита, шесть блокируют домен целиком, включая обычные запросы за картинками и статикой. В трекерах Chrome и Firefox эта проблема помечена как «Won’t fix», то есть исправления не будет. На HTTP/2 ограничение снимается мультиплексированием (в одном соединении идут несколько независимых обменов), а их число стороны согласуют между собой, и по умолчанию сервер объявляет около ста. Вечный спор между SSE и веб-сокетами для отдачи токенов упирается, выходит, совсем в другое: дошёл ли ваш периметр до HTTP/2.
Тише всего ломается отмена. Пользователь нажал «Стоп», браузер закрыл соединение, сервер получил событие разрыва и попытался отменить корутину, то есть асинхронную задачу, которая отдаёт токены. Вот только отменить её можно лишь в тот момент, когда она доходит до await. Генератор без точки передачи управления отмену просто не заметит. Он останется на воркере и будет тянуть токены от провайдера, который про нажатие «Стоп» ничего не знает, пока вызов не оборвут явно.
По модели оплаты за токены всё сгенерированное к этому моменту считается выполненной работой.
async def stream_answer(request: Request, prompt: str):
async with llm.stream(prompt) as answer: # вызов провайдера в контексте
async for token in answer: # точка передачи управления
if await request.is_disconnected():
break # и async with оборвёт вызов
yield f"data: {token}\n\n"
@app.post("/api/chat")
async def chat(request: Request, body: Ask):
return StreamingResponse(
stream_answer(request, body.prompt),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
Поэтому свежие версии FastAPI обзавелись отдельным помощником для потоковой отдачи строк, прямо описанным как способ отдавать вывод языковой модели. Он берёт обработку отмены на себя, и документация советует пользоваться им вместо ручного StreamingResponse. Скучная библиотечная деталь, за которой стоит вполне денежный эффект.
5. Аппаратная граница: 125 миллисекунд и при каких условиях
До сих пор вся обвязка была наша. Контейнер, приватную сеть, очередь с воркерами и стрим-сервер писали мы. Теперь агент получает выписку клиента, пишет скрипт на Python и выполняет его, чтобы посчитать сумму по категориям. Код исполняется там же, где живёт ключ к модели и открыто соединение с базой, а написала его модель по тексту, который прислал незнакомый человек. Под такую нагрузку и заводят песочницу. Граница у неё проходит уже по железу: микровиртуальная машина (микро-ВМ) со своим ядром, поднятая через аппаратную виртуализацию.
Цифры Firecracker, самой известной реализации этой идеи, звучат так, будто вопрос закрыт. Пользовательский код стартует за 125 миллисекунд, накладные расходы по памяти меньше 5 МиБ на машину, один хост создаёт до 150 машин в секунду. Модель устройств внутри урезана до необходимого минимума, а модель угроз сформулирована предельно жёстко: код в гостевых процессорах считается вредоносным с момента запуска.
В обзорах это число обычно округляют до «примерно 150 мс», в статье самих авторов стоит 125. Прежде чем переносить любое из них в свою презентацию, стоит прочитать условия замера, потому что они меняют вывод. Авторы мерили время от финального вызова API до запуска процесса init, пятьсот последовательных запусков, на голом железе EC2. Все машины в этом тесте работали без сети, а статически настроенный сетевой интерфейс добавляет к загрузке около 20 миллисекунд. Отдельной строкой там же советуют параметр ядра, отключающий вывод логов в последовательную консоль, и он экономит до 70 миллисекунд.
Итого 125 миллисекунд описывают конкретную сборку, а не технологию вообще.
Для масштаба: разработчики конкурирующей системы изоляции меряли на своём стенде старт обычного контейнера и получили 177 миллисекунд. Стенды разные, вычитать одно из другого нельзя, но обе величины лежат в сотнях миллисекунд.
Авторы Firecracker прямо пишут, что 125 миллисекунд быстро, но этого мало, когда Lambda добирает мощности под нагрузкой и старт машины заставляет ждать пользовательский запрос. Отвечают они на это пулом заранее прогретых машин, а размер пула считают по формуле Литтла: он равен произведению скорости создания на задержку создания. При 125 миллисекундах это означает одну прогретую машину на каждые восемь созданий в секунду.
Приём знакомый, но вопрос к нему сменился. Во втором разделе прогретый инстанс покупал секунды ожидания в ответе одного сервиса, и спор шёл о том, сколько такая покупка стоит. Здесь формула отвечает на другое — сколько машин держать. Посчитаем для нашего агента. Восемь тысяч обращений в сутки дают в среднем 0,09 создания песочницы в секунду, даже если бы код исполнялся в каждом обращении. Умножаем на 0,125 секунды и получаем 0,01 машины. Пул по среднему потоку не нужен вовсе — он существует ради всплеска, и размер его задаёт пик.
6. Во что обходится изоляция, когда песочница уже поднята
Старт оплачен, машина поднята, внутри крутится обычный Python. Насколько он медленнее, чем тот же Python на хосте? Единого числа для этого не существует, и дело не в неполноте замеров: цена изоляции зависит от того, чем занят код, причём в разы.
Разложить эти расходы помогает gVisor. Изолирует он иначе, чем микро-ВМ из прошлого раздела: отдельного ядра у него нет, вместо этого перехватчик ловит системные вызовы приложения и отвечает на них своей реализацией в пользовательском пространстве. Способ другой, а раскладка расходов годится для обоих — дополнительный слой между приложением и ядром хоста появляется и там и там. Авторы gVisor делят расходы на структурные и реализационные. Структурные вытекают из самого устройства: системный вызов проходит через лишний код, а перехватчик занимает память. Реализационные набегают оттого, что gVisor заново написал поверхность системных вызовов и оптимизирована она пока не везде; с релизами эта часть постепенно уходит. Первые не денутся никуда, вторые меняются от версии к версии, а в сводных таблицах эти две величины сложены в одну цифру.
Инструкции процессора никто не перехватывает, они исполняются напрямую, поэтому вычислительная нагрузка не платит за изоляцию практически ничего. Обращения к памяти тоже идут без надбавки. Платит тот код, который часто ходит в ядро: каждый системный вызов идёт через лишний слой, и чем их больше, тем выше счёт. Зависимость эта плавная. На Redis мелкие операции дают большие относительные накладные расходы, а LRANGE, где на один вызов приходится заметно больше работы внутри приложения, проседает куда слабее.
число системных вызовов на единицу полезной работы
───────────────────────────────────────────────────────────────►
мало средне много
нагрузка на смешанная частый мелкий
процессор: разбор нагрузка: ввод-вывод: хранилища,
выписки, веб-сервис сетевые сервисы,
арифметика, поверх базы распаковка тысяч
инференс мелких файлов
платит почти ничего платит заметно платит больше всехМикро-ВМ платит за изоляцию по другой причине — через виртуализацию устройств:
| Что меряли | В гостевой микро-ВМ | На хосте | Как мерили |
|---|---|---|---|
| диск, случайное чтение блоками 4 КБ | 13 000 IOPS | 340 000 IOPS | fio, глубина очереди 32, m5d.metal, 2020 |
| сеть, приём в один поток | 15,61 Гбит/с | 44,14 Гбит/с | iperf3, интерфейс tap, MTU 1500 |
Обе просадки упираются в устройство гостя, где операции ввода-вывода обрабатываются последовательно. Оговорок к этим числам две. Первая про возраст замера: сделан он в 2020 году, и авторы прямо писали, что оба ограничения собираются убрать. Вторая касается диска и звучит едче: сброс на диск в гостевом устройстве не реализован, так что высокая скорость записи достаётся ценой сохранности данных.
Что из этого следует для нашего агента? Скрипт, который разбирает выписку и считает суммы по категориям, ходит в ядро редко и живёт в левой части картинки, так что ни одного из ограничений он не заметит. А вот установка пакетов внутри песочницы, где распаковываются тысячи мелких файлов, попадает в правую часть и упирается в те самые 13 тысяч операций. Поэтому пакеты кладут в образ песочницы заранее и при каждом запуске не ставят.
Промах тут обычный: берут из обзора одно число накладных расходов и переносят на свою нагрузку. К тому же графики в документации gVisor сняты на ptrace — самой медленной из доступных платформ перехвата, которую авторы прямо называют худшим случаем и не рекомендуют к использованию. Число в чужой таблице может оказаться верхней границей, снятой на конфигурации, которую никто не ставит в прод.
7. Побег из песочницы через разрешённый маршрут
Про песочницу удобно думать как про запертую комнату. В июле 2026 года атака на Hugging Face показала, чего эта картинка не учитывает: платформа песочниц устояла, а наружу вышли по маршруту, который был разрешён политикой.
Началось всё с вредоносного датасета, который использовал два пути исполнения кода в конвейере обработки данных: загрузчик с удалённым кодом и внедрение шаблона в конфигурацию. Код исполнился на обрабатывающем воркере, дальше атакующий поднял привилегии до уровня узла, собрал облачные и кластерные учётные данные и за выходные ушёл в несколько внутренних кластеров.
Прямого доступа в интернет у этой среды не было. Зато политика разрешала ставить пакеты через внутренний кеш пакетного реестра. В самом кеше нашлась ранее неизвестная уязвимость, и по ней атакующий вышел в открытый интернет. Разрешённый сервис и оказался путём наружу.
[агент в песочнице] ──✗──> открытый интернет прямого доступа нет
│
└──✓──> [кеш пакетного реестра] разрешён политикой
│
└── неизвестная уязвимость ──> открытый интернет
граница песочницы цела, наружу вышли по разрешённому маршрутуСтыкуется это с одной строкой из документации Firecracker, которую читают редко: микро-ВМ не выполняет никакой фильтрации сетевого трафика, весь исходящий трафик гостя считается недоверенным и должен фильтроваться на уровне хоста. Аппаратная граница изолирует ядро, память и устройства. Про то, куда гостю разрешено ходить, она не говорит ничего, и сетевую политику приходится строить отдельным слоем.
Слой этот выглядит просто и ломается в деталях. В API песочниц Modal, например, три уровня: полная блокировка исходящего трафика, список разрешённых диапазонов адресов и список разрешённых доменов. Последний помечен как бета и работает только для TLS-трафика на порту 443, а любой другой трафик к неразрешённым адресам просто отбрасывается. Список доменов и список адресов работают по-разному, и подменять один другим в голове не стоит.
sandbox = modal.Sandbox.create(
app=app,
block_network=True, # исходящего трафика нет вовсе
# либо, когда пакеты всё-таки нужны:
# outbound_domain_allowlist=["pypi.org"], # только TLS на 443
)
Реестр уязвимостей смотрит на ту же границу с другой стороны. В январе 2026 года дыру нашли в самом jailer — вспомогательной программе, которая запирает процесс микро-ВМ обычными средствами Linux и служит второй линией на случай выхода за границу машины (CVE-2026-1386, исправлено в версиях 1.13.2 и 1.14.1). Показательна приписка AWS: собственные сервисы не затронуты, потому что доступ к каталогам jailer у них ограничен. Спасла эксплуатационная гигиена, код тут ни при чём.
Рядом с этой записью встают ещё три, каждая со своего слоя:
| Запись | Слой | Что сломалось | Оценка по CVSS (3.1; у moby — 4.0), из 10 |
|---|---|---|---|
| CVE-2024-21626 | рантайм контейнера | утечка файлового дескриптора в runc до 1.1.11: новый процесс получал рабочий каталог в файловой системе хоста | 8,6 |
| CVE-2026-17106 | библиотека распаковки | процедуры распаковки tar в moby/go-archive не удерживают запись внутри целевого каталога | 7,1 |
| CVE-2026-1386 | вспомогательная программа вокруг микро-ВМ | следование символической ссылке в предварительно созданных каталогах jailer: перезапись файлов хоста при запуске от root | 6,0 |
| CVE-2026-22708 | список разрешённых команд агента | часть встроенных команд оболочки в Cursor до 2.3 исполнялась мимо списка и без подтверждения, позволяя подменить переменные окружения для доверенных команд | 9,8 |
Записей четыре, и частоты отказов они не показывают. Показывают они другое: ломается не один барьер. Дыра нашлась и в рантайме контейнера, и во вспомогательной программе вокруг микро-ВМ, и в списке разрешённых команд, причём высший балл достался списку. Разницу в баллах создают условия эксплуатации. Чтобы воспользоваться дырой в jailer, нужен локальный пользователь на хосте, а список разрешённых команд открывается промпт-инъекцией (prompt injection) — инструкцией, которую прячут в тексте, и агент читает её как команду, а не как данные. Отсюда 9,8 против 6,0.
8. Почему контроль ослабляют и что ставят вместо подтверждений
Саму границу описывает конфигурация, а что именно через неё пропускать, каждый раз решают люди. Решают почти всегда в сторону ослабления, и виновата тут асимметрия видимости. Короткие учётные данные истекают посреди длинной задачи; узкая политика исходящего трафика ломает установку пакетов; правила, решающие, что вообще пускать в кластер, отвергают привычные команде инструменты. Каждый такой отказ виден немедленно и мешает конкретному человеку прямо сейчас. Последствия избыточных прав не видны вообще, пока не случится инцидент. Поэтому команда ослабляет ровно то, что мешает заметно, и не трогает то, что молчит.
Напрашивается человек на подтверждении каждого действия агента. Арифметика той же июльской кампании показывает, почему такой контроль не работает как основной. По разбору Docker кампания заняла четыре с половиной дня, из которых двое с половиной суток атакующий провёл внутри инфраструктуры, а восстановленный поток его действий человек не разберёт за разумное время, даже если смотреть только на смысловые группы. Модель нарушителя описана там одной фразой: способный атакующий с выносливостью фаззера, который обдумывает каждый результат и перебирает варианты без усталости. Против такого потока ручное подтверждение и обычный разбор алертов на роль основного контроля не годились ни в какой момент.
17 600 действий атакующего, 4,5 дня кампании
│
│ × 30 секунд внимания человека на действие
▼
147 часов разбора
│
│ группировка по смыслу — 6 280 кластеров
▼
52 часа разбора, и это нижняя оценкаЧто ставят взамен? Границу, описанную декларативно рядом с задачей, и проверку её кодом. В конфигурации агентного прогона это выглядит как список разрешённых сетевых адресов и явное указание, какие файлы агенту позволено менять в пул-реквесте. Такой список ревьюится тем же способом, что и код, живёт в репозитории и не зависит от того, устал ли дежурный. Ручное подтверждение при этом никуда не исчезает, однако занимает узкую полосу необратимых действий вроде возврата денег клиенту.
Декларативную границу проще всего получить там, где песочницу дают готовой. Microsoft, например, заявляет для своих динамических сессий старт за миллисекунды за счёт пула прогретых сред и аппаратную изоляцию на каждую сессию; независимых замеров этих миллисекунд найти не удалось, так что перед нами заявление вендора. У самосборного варианта своя цена: машина под микро-ВМ обязана поддерживать аппаратную виртуализацию, а на вложенной виртуализации, где часто оказываются CI-раннеры, это отдельная задача. Готовая песочница снимает обе заботы, правда, взамен привязывает к вендору и не даёт настроить рантайм под себя.
Побочный сюжет той же истории: разбор логов на коммерческих API не пошёл, потому что встроенные фильтры провайдеров не отличают расследователя инцидента от атакующего, и анализ пришлось делать на открытой модели у себя. Во что обходятся такие проверки, считали в прошлый раз.
Отрезвляющая мысль в конце принадлежит вендору, который продаёт свои песочницы. Изоляция не чинит уязвимый сервис, к которому агенту разрешено обращаться, не сужает учётные данные, выданные другой системой, и не заменяет собой архитектуру безопасности заказчика. Ни один вендор, включая Docker, не может заявить, что его технология сняла бы этот инцидент.
Итог
- Артефакт релиза — образ, а не код. Один образ едет во все среды, различаются только конфигурация ревизии и секреты, которые подставляются при старте и внутрь образа не попадают. Побочная выгода важнее основной: ротация ключа перестаёт требовать пересборки.
- Готовность покупается, и ценник открытый. Простаивающий vCPU в Cloud Run в 9,6 раза дешевле работающего, месяц постоянной готовности инстанса 1 vCPU / 2 ГиБ стоит $19,71 против $76,21 у полностью загруженного. Считают прогретые инстансы по пиковой скорости запросов; суточное среднее тут обманывает.
- Долгая работа не живёт в HTTP-запросе. Очередь снимает таймауты и потерю результата при закрытой вкладке, но требует идемпотентности и отдельной очереди для сбоев. Воркеров считают по глубине очереди: автомасштабирование по загрузке процессора под нагрузкой начнёт их гасить.
- Одного числа для накладных расходов не существует. Цену изоляции задаёт профиль нагрузки: вычисления платят почти ничего, мелкий ввод-вывод платит много, и в замере AWS 2020 года микро-ВМ выдавала 13 000 IOPS против 340 000 нативных.
- Ломается не один барьер. В инциденте июля 2026 года песочница устояла, а выход наружу нашёлся через разрешённый внутренний кеш пакетов; из четырёх записей реестра высший балл достался не ядру и не гипервизору, а списку разрешённых команд агента.
- Модель — один компонент, всё остальное обычная инженерия. Прототип с ноутбука доезжает до людей за счёт обвязки: образ и секреты, приватная сеть, очередь с воркерами, поток токенов и аппаратная граница вокруг чужого кода. Наш агент после этого разворачивается одним образом в любой среде, исполняет написанный моделью код за аппаратной границей и отдаёт первые слова примерно через полсекунды. А как он продолжает прерванную задачу с точки сохранения, разбирали в шестом уроке.
FAQ
Чем песочница для кода от модели отличается от обычного serverless-контейнера?
Назначением и жизненным циклом. Serverless-контейнер хостит ваш сервис, живёт долго и масштабируется под трафик, а изоляция у него стандартная контейнерная, с общим ядром хоста. Песочница создаётся под конкретную сессию исполнения недоверенного кода, уничтожается после неё и опирается на аппаратную границу — собственное ядро в микро-ВМ. Отсюда и разные требования к старту: контейнер может подниматься секунды, песочница обязана быть готова к моменту, когда агент решил выполнить скрипт.
Сколько стоит убрать холодный старт?
По прайс-листу Cloud Run на август 2026 года секунда простаивающего vCPU при заданном минимуме инстансов стоит $0,0000025 против $0,000024 у работающего, память тарифицируется одинаково в обоих режимах. Для конфигурации 1 vCPU / 2 ГиБ это $19,71 в месяц за постоянную готовность против $76,21 у инстанса, загруженного все 730 часов. Есть оговорка: пониженная ставка простоя действует при оплате по запросам, а при оплате за время жизни инстанса вы платите полный тариф всегда.
Правда ли, что песочница замедляет код на десятки процентов?
Такое число ничего не описывает без указания нагрузки. Перехват системных вызовов не трогает инструкции процессора и обращения к памяти, поэтому вычислительный код почти не платит за изоляцию; платит тот, кто делает много мелких операций ввода-вывода, и там же живут все крупные цифры накладных расходов. Вдобавок публичные графики gVisor сняты на платформе ptrace, которую авторы сами называют худшим случаем и не рекомендуют в прод.
Почему воркеров агента нельзя масштабировать по загрузке процессора?
Потому что воркер большую часть времени ждёт ответа модели по сети. Его загрузка процессора остаётся низкой даже тогда, когда очередь задач растёт, и автомасштабирование по загрузке процессора в этот момент начнёт гасить воркеров вместо того, чтобы добавлять. Масштабировать надо по числу сообщений, ожидающих в брокере: один воркер на заданное количество задач в очереди.
Почему поток токенов доходит до браузера рывками или не доходит вовсе?
Чаще всего виновата буферизация на обратном прокси: по умолчанию nginx накапливает ответ бэкенда и отдаёт клиенту одним куском. Лечится отключением буферизации для путей стриминга или заголовком X-Accel-Buffering: no на конкретном ответе. Мешает и лимит браузера на шесть одновременных соединений к домену поверх HTTP/1.1: несколько открытых вкладок с SSE выбирают его целиком, а исправлять этот лимит в Chrome и Firefox не планируют.
Достаточно ли микро-ВМ, чтобы агент не вышел наружу?
Нет, потому что микро-ВМ изолирует ядро и устройства, а не сеть. В документации Firecracker сказано прямо: фильтрации сетевого трафика она не выполняет, весь исходящий трафик гостя считается недоверенным и должен фильтроваться на уровне хоста. В инциденте июля 2026 года платформа песочниц устояла, а выход нашёлся через разрешённый политикой внутренний сервис. Сетевую политику приходится строить и проверять отдельно от самой границы.
Источники
- Firecracker: документация проекта и design.md — модель угроз, урезанная модель устройств и прямое заявление о том, что фильтрация сетевого трафика на микро-ВМ не лежит.
- Agache et al., «Firecracker: Lightweight Virtualization for Serverless Applications», NSDI 2020 — 125 мс с условиями замера, признание, что этого мало для масштабирования, размер прогретого пула по формуле Литтла, цена изоляции в IOPS и Гбит/с.
- gVisor — Performance Guide и Security Model — разделение структурных и реализационных расходов, зависимость накладных расходов от числа системных вызовов, оговорка про платформу ptrace.
- «Goldilocks Isolation: High Performance VMs with Edera», arXiv:2501.04580 — 177 мс старта обычного контейнера на стенде авторов; замер вендорский и с цифрами Firecracker напрямую не сопоставим.
- Docker — «17,600 Actions: Agent Security Is a Systems Problem» — арифметика ручного подтверждения, асимметрия видимости отказов и границы ответственности вендора.
- Hugging Face — раскрытие инцидента, июль 2026 — вектор входа через обработку датасета и путь наружу через разрешённый внутренний сервис.
- CVE-2026-1386 (бюллетень AWS), CVE-2024-21626, CVE-2026-17106, CVE-2026-22708 — четыре записи, на которых видно, что ломается не один барьер: рантайм контейнера, библиотека распаковки, вспомогательная программа вокруг микро-ВМ и список разрешённых команд агента.
- Cloud Run — прайс-лист и минимальное число инстансов — ставки активного и простаивающего времени, разница между режимами биллинга.
- MDN — Using server-sent events и FastAPI — Stream Data — лимит соединений на HTTP/1.1 со статусом «Won’t fix», число потоков на HTTP/2, семантика отмены асинхронного генератора.
- Modal — Sandbox networking — три уровня политики исходящего трафика и ограничение доменного списка одним лишь TLS.
Числовые ориентиры из текста зависят от профиля нагрузки, конфигурации стенда и даты замера. IOPS и гигабиты сняты в 2020 году на конкретной машине, и авторы замера сами предупреждали, что собираются эти ограничения снимать; ставки облаков меняются вместе с прайс-листами, а 125 мс относятся к сборке без сетевого интерфейса и без вывода в консоль. Перед тем как класть любое из этих чисел в расчёт, сверьтесь с первоисточником на текущую дату.