Делаем песочницу для запуска кода на любом языке

Делаем песочницу для запуска кода на любом языке

Всем привет! Меня зовут Макс. Понадобилось для своего проекта мне запилить платформу, в которой бы я мог запускать код, любой, на любом языке — безопасно и быстро, и чтобы было удобно добавлять новый код.

Давайте попробуем это сделать 🚀

Исследуем готовые решения

Кандидат1. Если посидеть на гитхабе, то можно найти judge0 — крутая штука, сервис написан на руби, встроено 60+ языков.

Изоляция у него сделана просто: в один Dockerfile напиханы бинарники для всех языков. Но что с изоляцией выполняющего кода?

Изоляция — важный момент для такой задачи, тк код запускается любой и нужно оградиться от злонамерений — использование ресурсов, памяти, CPU, времени рабоыт сервиса и сама система должна быть не тронутой

Изоляция сделана в judge0 через ограния linux: cgroup и namespaces через старое решение для конкурсов isolate. Всем вроде хороша, но наружу торчит много ограничений — обновляется последние пару лет слабо — ребята продают свое АПИ через разные площадки, запуск много-файлового кода немного проблемная, куча не нужного функционала — тянут БД, сложные обертки.

Например даже TS код корректно запустить получилось с пируэтами, тк tsconfig никакой не поставляется и банальные типы вроде []Foo или Array<Foo> не работают из коробки (даже в их веб IDE).

Кандидат 2. Я пишу на го и часто соприкасался с песочницей Google для Golang. Поизучал их код — сделано прикольно. Основной пинцип основан на том, что есть сервис, который принимает го-код, компилирует его и потом отдает внутреннему сервису по приватной сети для выполнения. Этот внутренний сервис поднимает на каждый запрос (заранее) контейнеры, готовый принять скомпилированные бинарные данные и выполнить их.

Изоляция (которая важна) для среды контейнеров являлась всегда слабым местом, тк приложение, которое запускает их, само в докере и ему надо в докере поднимать другие контейнеры, а значит нужен привилегированный доступ. Для решения этой проблемы Google разработал решение — gVisor. Суть в том, что этот gVisor подменяет рантайм докера и когда сервис, создающий контейнеры, создает новый докер — он обращается к докеру с указанием, что будет работать через эту прослойку. Этим и достигается изоляция.

Параллельное решение — создавать виртуалки. Но виртуалки медленные, но очень изолированные, а контейнеры супер-быстрые. Google пошел по решению проблемы с изоляцией — написал ранйтайм для Docker. Его использует, например, Digital Ocean для запуска сервисов в App Platform. Тогда как AWS для своих сервисов пошел по пути ускорения виртуалок и появился Firecracker и по сути решил эту же проблему, но через виртуальные машины.

Пишем свое решение

Итого judge0 не подошел из-за слабой поддержки, минусами расширения и акценте на продажу API, а Google Playground оказался простым для прототипа — его за основу решения и решил взять.

Летс гоу!!!

Так как у нас предполагается любой язык, то вариант Google с сервисом для компиляции кода на Go не подходи и все уезжает в sandbox. Схема вышла такой:

Делаем песочницу для запуска кода на любом языке

Сборка/компиляция происходят в том же sandbox, где и выполнение кода.

Немного подшаманив код наша задача сводится лишь к тому, чтобы в Sandbox загрузить готовые контейнеры, накрутить маппинг языка к запущенным контейнерам, поставить конфигурации и ву-а-ля — все готово.

Ко всему прочему добавили в API возможность грузить несколько файлов в таком виде

{ "templateId": "php83", "files": { "index.php": "<?php\n// /index.php\n\n// Some comment\n require_once __DIR__ . '/src/foo.php';\nrequire_once __DIR__ . '/src/bar/bar.php';\n\n// Call functions\n$resultFoo = foo();\nsleep(0);\n$resultBar = bar();\n\n// Calculate\n$product = $resultFoo * $resultBar;\n\n// Result\nvar_dump($product);\nvar_dump('Second output');", "src/foo.php": "<?php\n\nfunction foo() {\n return 20;\n}", "src/bar/bar.php": "<?php\n\nfunction bar() {\n return 3;\n}" }, "stdin": "1 2 3" }

И получается все очень даже удобно и красиво.

Так выглядит доставка конфигураций

Делаем песочницу для запуска кода на любом языке

Изоляция

Опять злополучная изоляция. Так как мы используем gVisor от Google, то мы перекрыли в нашей песочнице все что можем, запустив песочницу без сети и с ограничением памяти. Но так как, в отличие от Google Golang, у нас первый сервис (Playground) с доступом к сети не может собрать проект, а код собирать как-то нужно (pip у Python, или npm у JS/TS или composer у PHP) через сеть, то нужно что-то придумать.

На помощь нам пришло решение squid — в одной Docker-сети с изолированной от всего песочницей поднимается еще один контейнер, который контролирует исходящие сетевые запросы. Конфигурация его выглядит примерно так — белый список разрешенных ресурсов, остальное запрещено:

acl allowed_sites dstdomain .pypi.org .npmjs.org .golang.org .maven.org .rubygems.org .crates.io .packagist.org http_access allow allowed_sites http_access deny all

Тут список разрешенных ресурсов с зависимостями для разных языков

Все!

Результат

Спустя двух с половиной месяцев написания не самого сложного Go-кода мы получили готовое решение. Сейчас проект еще немного в разработке, но уже можно запускать. Готовы даже инструкции Terraform для деплоя в Digital Ocean.

Буду рад любой помощи, вопросам и предложениям 🤗🤗🤗

3
Начать дискуссию