-
Notifications
You must be signed in to change notification settings - Fork 8k
[core][sandbox] Add an experimental HTTP API service for Ray Sandbox #65633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
efa4778
921912e
e3ccb9d
e041afa
a12febc
b3aa0fd
108e824
2af9b07
ad5bfc1
9ed7383
46ece24
6532885
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,6 +323,108 @@ Ray Sandboxes implement multi-layered defense-in-depth isolation: | |
| * **Network containment**: By default, `network="none"` disables all outbound network interfaces, which prevents untrusted code from making external API calls or scanning the internal cluster network. When internet access is needed, `network="public"` grants egress without handing over the host's resolver configuration or network identity; see [Networking and DNS](#networking-and-dns). | ||
| * **Resource quotas**: cgroups enforce CPU quotas and memory limits, which prevents CPU starvation and out-of-memory (OOM) conditions from affecting other Ray actors. | ||
|
|
||
| ## HTTP API service | ||
|
|
||
| Ray Sandbox ships an experimental REST API service so you can manage sandboxes from outside the Ray cluster with nothing but an HTTP client and a bearer token. The service is a FastAPI app on Ray Serve (`ray.experimental.sandbox.http`). Each sandbox is held by a named, detached actor, so the service itself is stateless and its replicas can scale or restart without losing sandboxes. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why make sandboxes detached actors instead of replicas? We can treat sandboxes as multiplex ids in serve to do multiplex aware routing, but we can certainly have each serve replica own multiple sandbox actors that can be suspended resumed etc. i think this will be a more scalable system. |
||
|
|
||
| Image pulls and commands can far outlive an HTTP request and the load balancer in front of a deployed service, so creation and execution are asynchronous. `POST` returns immediately and clients poll, optionally long-polling with `wait_seconds` for up to 30 seconds per request. | ||
|
|
||
| ### Endpoints | ||
|
|
||
| All endpoints sit under `/api/v1`. Except for `GET /health`, they require `Authorization: Bearer <token>` when a token is configured. | ||
|
|
||
| | Method and path | Description | | ||
| | --- | --- | | ||
| | `GET /health` | Liveness probe; never requires auth. | | ||
|
xyuzh marked this conversation as resolved.
|
||
| | `POST /sandboxes` | Create a sandbox. Returns `202` with `status: pending`. Poll until `running` or `error`. Send a `client_token` to make creation idempotent, so a retry returns `200` with the existing sandbox. | | ||
| | `GET /sandboxes?label=k=v` | List sandboxes, optionally filtered by labels. | | ||
| | `GET /sandboxes/{id}?wait_seconds=N` | Sandbox status; long-polls while it boots. | | ||
|
xyuzh marked this conversation as resolved.
|
||
| | `DELETE /sandboxes/{id}` | Terminate the sandbox and its actor. Idempotent from any state. | | ||
| | `POST /sandboxes/{id}/execs` | Start a command. Returns `202` with an `exec_id`, or `409` while the sandbox isn't running. A string command runs under the sandbox's shell, `/bin/bash` by default and configurable per sandbox and per exec via `shell`. A list runs argv-style. | | ||
| | `GET /sandboxes/{id}/execs/{exec_id}?wait_seconds=N` | Exec status and result: `running`, `completed` with `exit_code`, `stdout`, and `stderr`, `timeout`, or `error`. Output is capped per stream by `max_output_bytes` with a loud truncation marker. | | ||
| | `PUT /sandboxes/{id}/files?path=/abs/path` | Write the raw request body to a file in the sandbox. Returns `413` above `max_file_bytes`. Pass `append=true` to extend the file, which lets clients chunk large uploads under proxy body-size limits. | | ||
| | `GET /sandboxes/{id}/files?path=/abs/path` | Read a file from the sandbox as `application/octet-stream`. | | ||
|
|
||
| Errors use a JSON envelope of the form `{"error": {"code": "...", "message": "..."}}`. The codes are `401 unauthorized`, `404 sandbox_not_found`, `404 exec_not_found`, `404 file_not_found`, `409 conflict`, `409 unschedulable`, `400 invalid_request`, `413 payload_too_large`, and FastAPI's native `422` for schema violations. The full OpenAPI schema is served at `/openapi.json`. | ||
|
|
||
| Server behavior worth knowing: | ||
|
|
||
| * **TTL**: Every sandbox gets a TTL that reclaims both the sandbox and its hosting actor. Request it with `ttl_seconds`, capped and defaulted by the server's `max_ttl_seconds`. | ||
| * **Resources**: `resources` separates cluster reservations from in-sandbox cgroup caps. `cpu_request`, `memory_request_mb`, and `custom` Ray resources reserve cluster capacity, and custom resources such as `{"gvisor": 1}` pin sandboxes to runsc-equipped nodes. `cpu_limit` and `memory_limit_mb` become cgroup caps. Requests default to the limits. | ||
| * **Capabilities**: By default sandboxes get Docker's default Linux capability set so images behave the way they do under Docker. Ray's own default is far narrower and breaks `apt-get` and `tar`. The sets are written exactly, so `capabilities: []` runs the sandbox with no capabilities at all. | ||
| * **Network modes**: The modes are the Python API's, validated by Ray: `none` (the default), `public` for egress with generated DNS that `dns` overrides, `host`, and `sandbox`. See [Networking and DNS](#networking-and-dns). | ||
|
|
||
| ### Self-hosted quickstart | ||
|
|
||
| On a Linux machine or cluster with `runsc` on `PATH`: | ||
|
|
||
| ```bash | ||
| pip install "ray[serve]" | ||
| export RAY_SANDBOX_API_TOKEN=dev-token # Optional. Unset disables app-level auth. | ||
| serve run ray.experimental.sandbox.http.app:build_app | ||
| ``` | ||
|
|
||
| ```bash | ||
| curl -s -H "Authorization: Bearer dev-token" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"image": "busybox:latest", "readonly": false, "shell": "/bin/sh"}' \ | ||
| http://localhost:8000/api/v1/sandboxes | ||
| ``` | ||
|
|
||
| Builder arguments configure the server. See `ray.experimental.sandbox.http.schemas.SandboxAPISettings` for the full list. For example: | ||
|
|
||
| ```bash | ||
| serve run ray.experimental.sandbox.http.app:build_app max_ttl_seconds=86400 num_replicas=2 | ||
| ``` | ||
|
|
||
| ### Deploying as an Anyscale service | ||
|
|
||
| Build a cluster image whose worker nodes have `runsc`: | ||
|
|
||
| ```dockerfile | ||
| FROM anyscale/ray:2.58.0-py312 | ||
| RUN ARCH=$(uname -m | sed 's/arm64/aarch64/') && \ | ||
| curl -fsSL -o /usr/local/bin/runsc \ | ||
| "https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}/runsc" && \ | ||
| chmod +x /usr/local/bin/runsc | ||
| ``` | ||
|
|
||
| Then deploy the builder as the service's application: | ||
|
|
||
| ```yaml | ||
| # service.yaml | ||
| name: ray-sandbox-api | ||
| image_uri: <your-registry>/ray-sandbox-api:latest | ||
| applications: | ||
| - name: sandbox-api | ||
| import_path: ray.experimental.sandbox.http.app:build_app | ||
| args: | ||
| max_ttl_seconds: 86400 | ||
| ``` | ||
|
|
||
| ```bash | ||
| anyscale service deploy -f service.yaml | ||
| ``` | ||
|
|
||
| Anyscale services require their own bearer token at the platform edge, so leave `RAY_SANDBOX_API_TOKEN` unset and hand clients the service's base URL and token. Consumers such as the [Harbor](https://harborframework.com) `ray-sandbox` environment take exactly that pair as `RAY_SANDBOX_API_URL` and `RAY_SANDBOX_API_KEY`. | ||
|
|
||
| ### Local development loop on macOS | ||
|
|
||
| `runsc` is Linux-only. Develop against the service in a privileged container: | ||
|
|
||
| ```bash | ||
| docker run --privileged -p 8000:8000 \ | ||
| -v ~/path/to/ray/python/ray/experimental/sandbox:/overlay:ro \ | ||
| rayproject/ray:nightly-py312 bash -lc ' | ||
| pip install "ray[serve]" && | ||
| SITE=$(python -c "import ray, os; print(os.path.dirname(ray.__file__))") && | ||
| cp -r /overlay/* "$SITE/experimental/sandbox/" && | ||
| ARCH=$(uname -m | sed "s/arm64/aarch64/") && | ||
| curl -fsSL -o /usr/local/bin/runsc "https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}/runsc" && | ||
| chmod +x /usr/local/bin/runsc && | ||
| RAY_SANDBOX_API_TOKEN=dev-token serve run --host 0.0.0.0 ray.experimental.sandbox.http.app:build_app' | ||
| ``` | ||
|
|
||
| ## API reference | ||
|
|
||
| For detailed signatures, parameters, and return types, see {ref}`ray-sandbox-ref`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| load("@rules_python//python:defs.bzl", "py_library") | ||
| load("//bazel:python.bzl", "py_test_module_list") | ||
|
|
||
| py_library( | ||
| name = "sandbox_http_lib", | ||
| srcs = glob( | ||
| ["**/*.py"], | ||
| exclude = ["tests/**/*.py"], | ||
| ), | ||
| visibility = [ | ||
| "//python/ray/experimental/sandbox/http:__pkg__", | ||
| "//python/ray/experimental/sandbox/http:__subpackages__", | ||
| ], | ||
| deps = [ | ||
| "//python/ray/experimental/sandbox:sandbox_lib", | ||
| ], | ||
| ) | ||
|
|
||
| py_library( | ||
| name = "conftest", | ||
| srcs = ["tests/conftest.py"], | ||
| deps = [ | ||
| "//python/ray/tests:conftest", | ||
| ], | ||
| ) | ||
|
|
||
| py_test_module_list( | ||
| size = "medium", | ||
| files = glob( | ||
| ["tests/**/*.py"], | ||
| exclude = [ | ||
| "tests/conftest.py", | ||
| "tests/__init__.py", | ||
| ], | ||
| ), | ||
| tags = [ | ||
| "sandbox_tests", | ||
| "team:core", | ||
| ], | ||
| deps = [ | ||
| ":conftest", | ||
| ":sandbox_http_lib", | ||
| ], | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """HTTP API service for Ray Sandbox. | ||
|
|
||
| This subpackage exposes ``ray.experimental.sandbox`` over a versioned REST | ||
| API (``/api/v1``) served by Ray Serve, so sandboxes can be managed from | ||
| outside the Ray cluster with nothing but an HTTP client and a bearer token. | ||
|
|
||
| Importing this package requires the Serve extra | ||
| (``pip install "ray[serve]"``); the base ``ray.experimental.sandbox`` | ||
| package deliberately never imports it. | ||
| """ | ||
|
|
||
| from ray.experimental.sandbox.http.app import build_app, create_app | ||
| from ray.experimental.sandbox.http.host import SandboxHost | ||
| from ray.experimental.sandbox.http.schemas import ( | ||
| DOCKER_DEFAULT_CAPABILITIES, | ||
| CreateSandboxRequest, | ||
| ExecInfo, | ||
| ResourceSpec, | ||
| SandboxAPISettings, | ||
| SandboxInfo, | ||
| StartExecRequest, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "build_app", | ||
| "create_app", | ||
| "SandboxHost", | ||
| "DOCKER_DEFAULT_CAPABILITIES", | ||
| "CreateSandboxRequest", | ||
| "ExecInfo", | ||
| "ResourceSpec", | ||
| "SandboxAPISettings", | ||
| "SandboxInfo", | ||
| "StartExecRequest", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe named detached actors are currently not as scalable as normal actors (e.g. each actor creation has to wait for a roundtrip to the GCS to make sure the names are unique), it would be great to check with @edoakes or @MengjinYan about that and what we should do (it it is indeed a problem)