Build the platform
This is the platform team’s side of Modelplane. You set up the gateway that
fronts your models, give the control plane cloud credentials, and register your
first GPU cluster: a hardware profile published as an InferenceClass and an
InferenceCluster that offers it.
In the next step, the ML team will create a model deployment that schedules against this capacity without knowing which cluster it runs on.
Prerequisites
- An AWS account with permissions to create EKS clusters, VPCs, and IAM roles
- AWS access key ID and secret access key
A GCP service account JSON key, granted these roles on the project:
Role Needed for roles/container.adminthe cluster and its node pools roles/compute.adminthe VPC network and subnet roles/serviceusage.serviceUsageAdminenabling the APIs the cluster needs roles/iam.serviceAccountAdminthe node service account roles/iam.serviceAccountKeyAdminthe node service account’s key roles/iam.serviceAccountUserattaching that account to the nodes roles/resourcemanager.projectIamAdmingranting the node account container.adminThe last one is worth a look before you hand the key over. Modelplane grants the node service account
roles/container.admin, so the credential doing the provisioning has to be able to set project IAM policy.
- An Azure account with permissions to create AKS clusters and managed identities
- An Azure service principal JSON with
clientId,clientSecret,subscriptionId, andtenantId
- A Nebius account with permissions to create clusters
- A Nebius service account JSON key and your project ID
- A Vultr account with access to GPU plans
- A Vultr API key
Configure cloud credentials
Give the control plane credentials so it can provision clusters in your cloud account.
Create an AWS credentials file:
[default]
aws_access_key_id =
aws_secret_access_key = Create a Kubernetes secret:
kubectl create secret generic aws-creds \
--from-file=credentials= \
-n crossplane-systemApply the ClusterProviderConfig referencing your secret:
# Points the AWS provider at the credentials Secret you created. Named default,
# so InferenceClusters with an EKS source use it without further configuration.
apiVersion: aws.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credentials
Create a Kubernetes secret:
kubectl create secret generic gcp-creds \
--from-file=credentials=.json \
-n crossplane-systemApply the ClusterProviderConfig, setting projectID to your GCP project:
# Points the GCP provider at the credentials Secret you created. Named default,
# so InferenceClusters with a GKE source use it without further configuration.
apiVersion: gcp.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
projectID: my-gcp-project # replace with your GCP project
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: gcp-creds
key: credentials
curl -fsSL /examples/getting-started/clusterproviderconfig-gke.yaml \
| sed 's/my-gcp-project//' \
| kubectl apply -f -Create a Kubernetes secret from your service principal JSON:
kubectl create secret generic azure-credentials \
--from-file=credentials.json=.json \
-n crossplane-systemApply the ClusterProviderConfig referencing your secret:
# Points the Azure providers at the credentials Secret you created. Named
# default, so InferenceClusters with an AKS source use it without further
# configuration. The Secret carries a service principal JSON with
# clientId, clientSecret, subscriptionId, and tenantId.
apiVersion: azure.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: azure-credentials
key: credentials.json
Create a Kubernetes secret from your service account JSON:
kubectl create secret generic nebius-credentials \
--from-file=credentials.json=.json \
-n crossplane-systemApply the ClusterProviderConfig, setting projectID to your Nebius project:
# Points the Nebius provider at the credentials Secret you created. Named
# default, so InferenceClusters with a Nebius source use it without further
# configuration - Modelplane also reuses its credentials Secret to
# authenticate to the clusters it provisions.
apiVersion: nebius.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
identity:
type: ServiceAccount
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: nebius-credentials
key: credentials.json
projectID: project-e00example
curl -fsSL /examples/getting-started/clusterproviderconfig-nebius.yaml \
| sed 's/project-e00example//' \
| kubectl apply -f -Create a Kubernetes secret from your API key:
kubectl create secret generic vultr-credentials \
--from-literal=api-key= \
-n crossplane-systemApply the ClusterProviderConfig referencing your secret:
# Points the Vultr provider at the API key Secret you created. Named
# default, so InferenceClusters with a Vultr source use it without further
# configuration. The API key only provisions clusters - VKE kubeconfigs
# embed static client certificates, so consumers never need it to reach
# the clusters Modelplane provisions.
apiVersion: vultr.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: vultr-credentials
key: api-key
Publish hardware and register the cluster
The InferenceClass describes a hardware profile and how to provision it. The
InferenceCluster registers a cluster that offers it. Apply both:
apiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: l4-1x-g6
spec:
description: "EKS g6.xlarge, 1x NVIDIA L4"
provisioning:
provider: EKS
eks:
instanceType: g6.xlarge
diskSizeGb: 50
accelerator:
type: nvidia-l4
count: 1
devices:
- name: gpu
claim: DRA
driver: gpu.nvidia.com
deviceClassName: gpu.nvidia.com
count: 1
attributes:
architecture: { string: Ada Lovelace }
capacity:
memory: { value: "23034Mi" } # L4's real reported VRAM (not the nominal 24GB)
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: eks-us-east
labels:
modelplane.ai/region: us-east
spec:
cluster:
source: EKS
eks:
region: us-east-1
nodePools:
- name: gpu-l4
className: l4-1x-g6
nodeCount: 1
minNodeCount: 1
maxNodeCount: 1
zones:
- us-east-1b
Modelplane provisions the cluster. This takes about 15 minutes:
kubectl wait --for=condition=Ready ic/eks-us-east --timeout=20mApply the manifest:
apiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: gke-l4-1x-g2
spec:
description: "GKE g2-standard-8, 1x NVIDIA L4"
provisioning:
provider: GKE
gke:
machineType: g2-standard-8
diskSizeGb: 100
accelerator:
type: nvidia-l4
count: 1
devices:
- name: gpu
claim: DRA
driver: gpu.nvidia.com
deviceClassName: gpu.nvidia.com
count: 1
attributes:
architecture: { string: Ada Lovelace }
capacity:
memory: { value: "23034Mi" } # L4's real reported VRAM (not the nominal 24GB)
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: starter
labels:
modelplane.ai/region: us-central
spec:
cluster:
source: GKE
gke:
region: us-central1
nodePools:
- name: gpu-l4
className: gke-l4-1x-g2
nodeCount: 1
minNodeCount: 0
maxNodeCount: 2
zones:
- us-central1-a
kubectl apply -f /examples/getting-started/gke/platform.yamlModelplane provisions the cluster. This takes about 15 minutes:
kubectl wait --for=condition=Ready ic/starter --timeout=20mapiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: a10-1x
spec:
description: "AKS Standard_NV36ads_A10_v5, 1x NVIDIA A10 24GB"
provisioning:
provider: AKS
aks:
# verify: Azure has no L4 SKU; NVadsA10v5 is the small-GPU option (>=20Gi).
# Confirm it runs the DRA / GPU-operator path in your subscription.
vmSize: Standard_NV36ads_A10_v5
diskSizeGb: 100
accelerator:
type: nvidia-a10
count: 1
devices:
- name: gpu
claim: DRA
driver: gpu.nvidia.com
deviceClassName: gpu.nvidia.com
count: 1
attributes:
architecture: { string: Ampere }
cudaComputeCapability: { version: "8.6.0" }
capacity:
memory: { value: "24564Mi" } # verify: A10 reported VRAM (~24GB)
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: aks-westeurope
labels:
modelplane.ai/region: westeurope
spec:
cluster:
source: AKS
aks:
location: westeurope
nodePools:
- name: gpua10
className: a10-1x
nodeCount: 1
minNodeCount: 1
maxNodeCount: 1
Modelplane provisions the cluster. This takes about 15 minutes:
kubectl wait --for=condition=Ready ic/aks-westeurope --timeout=20mapiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: l40s-1x
spec:
description: "Nebius gpu-l40s-a, 1x NVIDIA L40S 48GB"
provisioning:
provider: Nebius
nebius:
# gpu-l40s-a (L40S PCIe, Intel Ice Lake) is available in eu-north1.
platform: gpu-l40s-a
preset: 1gpu-8vcpu-32gb
diskSizeGb: 100
driversPreset: cuda13.0
accelerator:
type: nvidia-l40s
count: 1
devices:
- name: gpu
claim: DRA
driver: gpu.nvidia.com
deviceClassName: gpu.nvidia.com
count: 1
attributes:
architecture: { string: Ada Lovelace }
cudaComputeCapability: { version: "8.9.0" }
capacity:
memory: { value: "46068Mi" } # L40S reported VRAM (48GB nominal)
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: nebius-eu-north
labels:
modelplane.ai/region: eu-north
spec:
cluster:
source: Nebius
nebius: {}
nodePools:
- name: gpu-l40s
className: l40s-1x
nodeCount: 1
minNodeCount: 1
maxNodeCount: 1
Modelplane provisions the cluster. This takes about 15 minutes:
kubectl wait --for=condition=Ready ic/nebius-eu-north --timeout=20mapiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: l40s-1x
spec:
# The largest GPU plan available in ewr. Vultr GPU plans are region-gated;
# list what a region offers with the /v2/regions/<region>/availability API
# before picking another plan.
description: "Vultr vcg-l40s-16c-180g-48vram, 1x NVIDIA L40S 48GB"
provisioning:
provider: Vultr
vultr:
plan: vcg-l40s-16c-180g-48vram
accelerator:
type: nvidia-l40s
count: 1
devices:
- name: gpu
claim: DRA
driver: gpu.nvidia.com
deviceClassName: gpu.nvidia.com
count: 1
attributes:
architecture: { string: Ada Lovelace }
cudaComputeCapability: { version: "8.9.0" }
capacity:
memory: { value: "46068Mi" } # L40S's real reported VRAM (not the nominal 48GB)
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: vultr-ewr
labels:
modelplane.ai/region: ewr
spec:
cluster:
source: Vultr
vultr:
region: ewr
nodePools:
- name: gpu-l40s
className: l40s-1x
nodeCount: 1
minNodeCount: 1
maxNodeCount: 1
Modelplane provisions the cluster. This takes about 15 minutes:
kubectl wait --for=condition=Ready ic/vultr-ewr --timeout=20mModelplane is reconciling the infrastructure against the source of truth, the manifest you just applied.
While you wait, Modelplane is creating the cloud cluster and its GPU node
pool, then installing the inference stack with LeaderWorkerSet for multi-node
serving (the default; a cluster can opt into Grove and KAI Scheduler instead via
InferenceCluster.spec.stack: Dynamo), llm-d for inference-aware routing,
Envoy Gateway for traffic management, and the storage class for model weights.
This is the same reconciliation loop Crossplane uses to configure other
infrastructure, extended to the inference layer.
Set up the InferenceGateway
The InferenceGateway is the address callers reach your models through. It
speaks the OpenAI and Anthropic APIs, authenticates callers, and resolves the
model a request names to a ModelService.
It runs on an InferenceCluster rather than on your control plane, named by
spec.clusterName, because that cluster already runs the gateway software.
It comes after registering the cluster because it needs one to run on. Here
it shares the cluster serving the model, which is fine; in a real fleet you’d
more often give a gateway a cluster of its own.
This one is the smallest useful shape: no hostname, no certificate and no caller keys, so it answers on its address over plain HTTP and authenticates nobody. Fine here, wrong on a network you don’t trust. See Set Up the Gateway for the production shape.
# An InferenceGateway is the front door for inference requests: the only address
# a caller sees. It speaks the OpenAI and Anthropic APIs, authenticates callers,
# and resolves the model a request names to a ModelService.
#
# It runs on an InferenceCluster, which already runs the gateway software, so
# this installs nothing on your control plane. The cluster needs no GPU pools: a
# cluster with none is a gateway and nothing else, and a cluster that serves
# models can host one too.
#
# You can run several, one per region, and distributing callers across them is
# yours to configure. This one is the smallest useful shape: no hostname, no
# certificate and no caller keys, so it answers on its address over plain HTTP
# and authenticates nobody. Fine for getting started, not for an untrusted
# network.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceGateway
metadata:
name: local
spec:
clusterName: local
Wait until the gateway is ready:
kubectl wait --for=condition=Ready ig/local --timeout=5mWith the cluster registered and a gateway in front of it, the ML team can deploy a model.
Next step
Now that the platform is provisioned, the ML team can deploy a model by describing what the model needs, not the infrastructure.