Managed Kubernetes
Kubernetes clusters whose control plane we run, with whole GPU or CPU hosts as nodes. Create a cluster and a node pool, write a kubeconfig, and use kubectl.
A managed Kubernetes cluster has a control plane we run and nodes that are
whole OpenRelay hosts, either GPU or CPU, in one data center. You create and
size it through the Kubernetes API or the
orl kube commands, and use it with kubectl.
Limited availability
Managed Kubernetes is in limited availability and is enabled per organization.
Until it is enabled for yours, every Kubernetes endpoint answers 409 Conflict
with code FEATURE_DISABLED (and orl kube exits with code 6). Contact
support to have it enabled.
What a cluster is
- Kubernetes v1.37.1. It is the only version offered, and a cluster is not upgraded in place.
- One data center. A cluster's nodes all run in the location you create it
in (
locationId, fromorl catalog location listorGET /v1/locations). - Node pools of whole hosts. Every node in a pool is the same SKU: one GPU
model (
gpuModelId, fromorl catalog gpu-model listorGET /v1/gpu-models) or one whole-host CPU SKU (cpuSku). A GPU node is a whole host of that model with every GPU passed through to it. A pool holds 1 to 100 nodes; mix SKUs by adding several pools. - Credentials from your API key. The API exchanges your key for a cluster
token: a bearer token only that cluster accepts, carrying
cluster-admin, which expires after one hour. There are no client certificates to manage. - Fixed networks. Pods get addresses from
10.244.0.0/14and services from10.96.0.0/16in every cluster.
Reading clusters, pools and nodes needs Kubernetes: Read; getting a
kubeconfig or token needs Kubernetes: Connect; creating, scaling or
deleting needs Kubernetes: Manage (Permissions). A
legacy key needs clusters:read to read and clusters:write for the rest.
1. Create a cluster
orl kube cluster create --name training --location-id <locationId>The same request as an API call is
POST /v1/kubernetes/clusters:
curl -X POST https://api.openrelay.inc/v1/kubernetes/clusters \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "training", "locationId": "<locationId>"}'It answers 202 with the cluster's id and status: creating. Poll the
cluster until its status is ready:
orl kube cluster get <cluster-id>A name is lowercase letters, digits and hyphens, starts with a letter, does not end with a hyphen, and is at most 63 characters. It is unique within your organization until the cluster's deletion completes.
You can also pass up to 10 node pools in the create request (nodePools, each
shaped like the node pool request below). They are added once the control
plane is ready.
2. Add a node pool
Once the cluster is ready, add nodes:
orl kube node-pool create <cluster-id> \
--name gpu \
--count 2 \
--sku '{"gpuModelId": "<gpuModelId>"}'As an API call,
POST /v1/kubernetes/clusters/{id}/node-pools
takes {"name": "gpu", "sku": {"gpuModelId": "<gpuModelId>"}, "count": 2}. For
a CPU pool, the SKU is {"cpuSku": "<cpuSku>"}.
The pool reaches its count as hosts are claimed and their nodes join; until
then its readyCount is lower. Watch the nodes:
orl kube node list <cluster-id>A node is registered while its host boots, then joined once it is in the
cluster, where kubernetesStatus reports its Ready condition.
3. Use kubectl
orl kube kubeconfig writes a
kubeconfig for the cluster. It holds no credential: kubectl runs
orl kube token to get a cluster token, and a new one whenever the last one
expires, so orl must be installed where kubectl runs.
orl auth login # once, if you have not stored a key yet
orl kube kubeconfig --cluster <cluster-id> --output ~/.kube/openrelay
export KUBECONFIG=~/.kube/openrelay
kubectl get nodesWithout the CLI,
GET /v1/kubernetes/clusters/{id}/kubeconfig
returns a kubeconfig with a cluster token embedded, which stops working when
the token expires after one hour, and
POST /v1/kubernetes/clusters/{id}/token
returns a token as a client.authentication.k8s.io/v1 ExecCredential for your
own credential plugin.
4. Run on a GPU
Request GPUs with the nvidia.com/gpu resource. Each GPU on a node is one,
and a container that requests none sees no GPU:
apiVersion: v1
kind: Pod
metadata:
name: gpu-check
spec:
restartPolicy: Never
containers:
- name: cuda
image: nvcr.io/nvidia/cuda:12.8.1-base-ubuntu24.04
command: ["nvidia-smi", "-L"]
resources:
limits:
nvidia.com/gpu: 1kubectl apply -f gpu-check.yaml
kubectl logs gpu-checkScale and delete
orl kube node-pool scale <cluster-id> <pool-id> --count 4 # 1 to 100
orl kube node delete <cluster-id> <node-id> # drains it first
orl kube node-pool delete <cluster-id> <pool-id>
orl kube cluster delete <cluster-id>- Scaling down drains the nodes it removes before removing them.
- Deleting a node drains it first unless you pass
--drain=false(?drain=falseon the API). The pool's count drops by one, so the node is not replaced. A pool's last node cannot be deleted on its own: delete the pool. - Draining cordons the node and evicts every pod on it except a
DaemonSet's and any pod that tolerates the cordon
(
node.kubernetes.io/unschedulable). The drain does not wait on those: they get no eviction, so no disruption budget is consulted, and they stop when the node is removed. - Taints. The managed GPU operator and MPI operator are scheduled only
onto nodes with no
NoScheduleorNoExecutetaint of yours. Cluster DNS, the Cilium operator and the local-path provisioner run through yourNoScheduletaints but leave a node with aNoExecutetaint of yours. Keep at least one node with neither so they always have a place to run. - Deleting a pool drains and removes every node in it.
- Deleting a cluster removes its pools and every node, and is final.
orl kube cluster getanswers404once it completes. Repeating the delete finishes one that could not remove everything.
When a request is refused
| Status | Code | Meaning |
|---|---|---|
404 | The cluster, pool or node does not exist. | |
400 | BAD_REQUEST | The request breaks the schema: a name, count or version out of range, or an unknown field. The message names it. |
400 | INVALID_LOCATION | locationId is not a data center from GET /v1/locations. |
400 | INVALID_GPU_MODEL | gpuModelId is not a GPU model from GET /v1/gpu-models. |
400 | INVALID_CPU_SKU | cpuSku is not a whole-host CPU SKU on sale. |
403 | INSUFFICIENT_BALANCE, ACCOUNT_RESTRICTED, ACCOUNT_PENDING_VERIFICATION | Creating a cluster or a pool, or scaling a pool up, passes the same account checks as creating a VM. See Account and balance refusals. |
409 | FEATURE_DISABLED | Managed Kubernetes is not enabled for your organization. Contact support to have it enabled. |
409 | INVALID_CLUSTER_STATE | The cluster's status blocks the request, for example adding a pool to a cluster that is still creating. The message names the status. |
409 | NAME_TAKEN | Your organization already has a cluster with this name. |
409 | TOO_MANY_CLUSTERS_CREATING | Your organization has as many clusters creating as it may at once. Create another once one of them is ready. |
409 | INVALID_NODE_POOL_STATE | The node pool is deleting. |
409 | LAST_NODE_IN_POOL | The node is its pool's last. Delete the pool instead. |
409 | CONCURRENT_UPDATE | The pool's count, or your organization, changed while the request ran. Retry. |
Pods and persistent storage
Bring-your-own-container GPU pods, what survives a stop, restart, crash or node loss, and how the /workspace volume is billed and retained.
Overview
The OpenRelay Inference API, with OpenAI-compatible chat completions and embeddings, an Anthropic-compatible Messages API, and typed decisions.