Fleet (multi-cluster, multi-project Kubernetes)
SkillCloud & infraUse to deploy or operate a fleet of Nebius Managed Kubernetes (k8s-training) clusters across one or many projects in a tenant from an npa.fleet/v0.0.1 spec — including strict capacity-block-backed GPU pools, identical and/or custom clusters, create-on-demand projects, and a k8s-training recipe source that can consume the latest upstream changes.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Fleet (multi-cluster, multi-project Kubernetes) skill
What this skill tells your AI
The instructions your AI receives, as published by nebius/nebius-physical-ai in skills/tools/fleet/SKILL.md and read by ahel’s review.
When To Use
Use when a customer wants more than one Managed Kubernetes cluster stood up
across one or many projects in a single Nebius tenant — e.g. per-team or
per-tenant training clusters — and wants npa to drive it from one declarative
file. npa fleet wraps the public nebius/nebius-solutions-library
k8s-training recipe (the same recipe npa cluster up uses) once per cluster,
and can create the target projects on demand.
For a single cluster, prefer npa cluster up. For Slurm-on-Kubernetes, use
npa soperator.
Three-tier contract:
- CLI:
npa fleet plan|deploy|destroy|status|verify-storage|verify-graphics --spec <fleet.yaml>. - SDK:
npa.sdk.fleet.deploy(spec)/destroy/plan/statuswithFleetSpec/ProjectSpec/ClusterSpec/NodePoolSpec. - YAML / agent:
apiVersion: npa.fleet/v0.0.1spec; workflowtoolRef: infra.fleet.deploy(config keyfleet_spec).
Opt-in CPU RayCluster
Use kuberay: {enabled: true, worker_replicas: 2, worker_cpus: 2, worker_memory_gib: 4} in a cluster/default profile for a fixed CPU worker group.
The shared backend validates strict types, explicit CPU placement and the exact
reviewed recipe before provisioning. It also rejects head or worker pods larger
than one declared CPU node and fixed requests above the pool's nominal aggregate
CPU or memory; operators must still leave room for Kubernetes system pods.
KubeRaySpec is exported by npa.sdk.fleet.
A disabled block replaces enabled defaults atomically. Omitted policy remains off.
Enabled deployments require the default local Terraform workspace without
TF_CLI_ARGS* or TF_DATA_DIR overrides. Reapply rejects extra effective inputs
and destination symlinks while preserving exact state/backup and provider caches.
Retained backend metadata is unsupported; require managed implicit local state.
The module manifest is rebuilt from the reviewed recipe before Terraform init.
Historical opt-in provenance survives disabled/omitted policy reapply. Teardown
checks the same effective-input restrictions and recorded materialized digest,
then requires empty managed state before removing recovery files. Preserve state
on refusal or incomplete teardown; missing provenance needs a reviewed reapply.
See docs/fleet-kuberay.md and
npa/examples/fleet/kuberay/cpu-raycluster.yaml for the one-entry fleet and native
Ray Jobs journey. The separate CPU template pins Ray 2.58, disables autoscaling
and bundled monitoring, uses non-root pods and installs namespace ingress
isolation. GPU workers, RayService, arbitrary images and alternate recipe bytes
are unsupported; do not recreate a Jobs submitter/controller or enable the old
privileged vendor template. Preserve application outputs before owned teardown.
Run npa/tests/e2e/test_fleet_kuberay_live.py with
NPA_FLEET_KUBERAY_LIVE_CONFIG pointing at an owner-private JSON object with
spec, kubeconfig, and evidence_dir. It inspects deployed values and executes
real deterministic work on every Ray worker through native Jobs status/logs.
Verify an existing shared filesystem
Run npa fleet verify-storage --spec <private-fleet.yaml> --output json to
qualify every CPU and GPU worker. The shared implementation is
npa.fleet.storage_verification.verify_storage, also exported as
npa.sdk.fleet.verify_storage. Use --only-projects, --only-clusters,
--project-prefix, and --profile to preserve Fleet selection and identity
semantics. Unknown selectors and missing or stale registered identity fail
closed; explicitly disabled filesystems are skipped.
The verifier proves the exact read-write virtiofs source/path, reboot-safe
nofail entry, capacity in binary GiB, unique host-file checksums, CSI health,
and one RWX PVC shared across pods pinned to every exact worker. Every pod
checks every worker's unique payload. It reads no pre-existing customer entries.
Cleanup removes only owned probe paths and temporary resources with identity
labels and UID preconditions, then proves absence on every node using
server-synchronized Linux statx attributes after owned writers stop. Cached
positive directory entries cannot substitute for fresh link-count evidence;
unsupported synchronization and persistent linked entries fail closed.
Partial evidence or cleanup failure cannot pass. Do not replace this with the vendored single-node shell
smoke or infer storage health from node readiness.
Use --evidence-dir <owner-private-directory> outside the repository for exact
receipts. Publication surfaces receive only sanitized counts, requested capacity,
categories, hashes, and cleanup counts. This operation does not redeploy,
resize, change IAM, or alter customer workloads. See
docs/fleet-storage-verification.md for prerequisites and the installed SDK.
Run npa fleet verify-graphics --spec <private-fleet.yaml> --output json after
an RTX driver, node, or runtime change. It uses the registered Fleet identity,
waits for the declared health stability interval, and runs CUDA vectorAdd plus
GLX, EGL, and Vulkan qualification on every selected 8-GPU RTX worker. All
targets must declare gpu_workload_profile: rtx-rendering; a partial target or
partial per-node result fails closed. Use --concurrency for independent
clusters and the standard Fleet selectors to narrow scope. Exact provider and
node evidence goes only to the owner-private --evidence-dir outside Git; CLI
output remains safe to publish. The shared SDK entry point is
npa.sdk.fleet.verify_graphics.
The all-worker live regression is
npa/tests/e2e/test_fleet_storage_verification_live.py. Supply
NPA_INTEGRATION_E2E=1, NPA_FLEET_STORAGE_VERIFY=1, an owner-private
NPA_FLEET_STORAGE_VERIFY_SPEC, and NPA_FLEET_STORAGE_EVIDENCE_DIR.
The daily runner reaches it only with that explicit opt-in. Its expected target,
worker, and binary-GiB totals derive from the complete selected declaration.
RTX PRO 6000 hardware MIG is an additive cluster policy. Use mig: {enabled: true, strategy: mixed, config: all-balanced} only with two strict
reserved-capacity gpu-rtx6000 / 1gpu-24vcpu-218gb workers and 128 GiB boot
disks. NPA pins and live-verifies GPU Operator v26.3.3, driver 580.173.02,
device plugin/GFD v0.19.3, MIG Manager v0.14.2, exact per-node resources,
and zero whole-GPU capacity/allocatable. See
docs/fleet-rtx-pro-6000-mig.md and npa/examples/fleet/rtxpro-mig.yaml.
RTX rendering without MIG uses the separate explicit cluster setting
gpu_workload_profile: rtx-rendering. It selects RTX PRO 6000 (including an
explicitly resolved zonal platform such as gpu-rtx6000-a), the supported
single-GPU preset by default or accepts the explicit
8gpu-192vcpu-1744gb RTX PCIe preset, uses GPU Operator mounted drivers, and
requires per-node GLX/EGL/Vulkan readiness. The 8-GPU RTX shape remains
non-fabric and does not opt into the NVSwitch unsafe-operator path. The profile
does not alter the empty/default managed-image contract and cannot be
generalized to NVSwitch targets.
The rendering profile supplies the exact platform and preset to the
marketplace GPU Operator's nebius.nvidiaDriverCRDPatch.profiles values. Its
default gpu-rtx6000 selector does not match zonal gpu-rtx6000-a workers.
The override contains only the requested RTX pool, keeps RDMA disabled, and
uses a revision hash so Terraform reconciles write-only values on existing
releases. Alternate recipes must expose and wire
gpu_operator_rtx_driver_profile; incompatible recipes fail before cloud
mutation. Do not relabel workers to hide a selector mismatch.
An RTX cluster may declare gpu_driver_package_repositories as a mapping of
safe filenames to package-source text. Fleet passes it through the Operator's
driver.repoConfig and an owned ConfigMap, retaining signature verification.
Match the sources to the actual driver-image distribution. The five-cluster
example supplies Ubuntu 24.04 HTTPS sources for networks that permit HTTPS but
block HTTP. Empty configuration preserves the image defaults. Before workloads,
verify the driver pod actually mounts the configured files and can fetch its
kernel packages; do not infer network reachability from the operator VM.
The marketplace may disable automatic upgrades and use an OnDelete driver
DaemonSet. A successful Helm update then changes the template without replacing
existing pods. Compare actual pod mounts with the template. Replacing stale
driver pods is maintenance: first verify no application GPU workloads are
active, cordon each affected node, recheck workloads, and delete only the
identified stale pod with a UID precondition. Wait for its ready replacement
and restore the node's prior scheduling state. Retain private receipts and
rerun the full CUDA/graphics health gate; never delete application pods or
relax readiness to finish an upgrade.
Read-only live coverage checks exact selectors, per-node GPU quantities, ConfigMap contents, ready driver containers, and actual repository mounts:
NPA_INTEGRATION_E2E=1 NPA_FLEET_RTX_VERIFY_SPEC=<private-spec-path> \
NPA_FLEET_RTX_KUBECONFIGS=<private-project-cluster-path-mapping.json> \
npa/.venv/bin/python -m pytest \
npa/tests/e2e/test_fleet_rtx_driver_config_live.py -q
The mapping is {project_key: {cluster_name: kubeconfig_path}}; keep its exact
values outside Git. This check creates no pods and complements the real
CUDA/graphics and representative workload validation.
For qualification on every individual eight-GPU RTX cluster, additionally run
npa/tests/e2e/test_fleet_rtx_workload_live.py with the same private spec/mapping,
NPA_FLEET_RTX_RUN_WORKLOADS=1, and an owner-only
NPA_FLEET_RTX_WORKLOAD_EVIDENCE_DIR. It creates one Job per target using the
provisioning gate's immutable, payload-clean image. Each Job executes verified
CUDA arithmetic and matrix multiplication, dynamically loads GLX/EGL, and
creates a Vulkan instance with an NVIDIA physical device. It verifies the
runtime digest and deletes each Job with a UID precondition, then proves both
Job and pod absence. The evidence directory holds private execution receipts
and a separate sanitized result summary. This does not replace representative
Isaac training or the canonical Sim2Real ladder.
The RTX override also sets toolkit RUNTIME_CONFIG_SOURCE=file. With newer
containerd binaries, containerd config dump can migrate the configuration in
memory. Using that output may produce a version-4 NVIDIA drop-in beside a
version-2 root, preventing containerd startup. Reading the file preserves the
host schema; see NVIDIA's configuration-source guidance.
For an already broken node, apply the corrected toolkit configuration first.
After verifying no application workloads are active, retain the root and
drop-in files privately, move only the proven incompatible NVIDIA drop-in out
of the imports directory, validate containerd config dump, and restart
containerd. Verify the replacement toolkit produces a compatible fragment and
the full health gate passes. Do not change version numbers blindly or discard
unrelated host configuration.
A node group can report PROVISIONING again when its workers lose readiness.
Fleet treats it as zero incremental reserved demand only after a complete
Compute inventory proves the exact group/cluster/project labels, requested
template, running worker count, reservation bindings, and attached disks.
Target and actual node counts must also match. This capacity proof does not
mark the cluster healthy: every deployment readiness gate still runs.
Spec (npa.fleet/v0.0.1)
The version remains additive. A cluster without backend is the historical
mk8s shape. Explicit targets use backend: mk8s plus mk8s: {...} or
backend: soperator plus soperator: {...}; both may appear in one project or
fleet. Fleet owns target identity and shared-network inventory, while the
selected backend owns one target's plan, materialization, apply, native status,
verification, and destroy. See docs/cluster-backends.md for one-entry and
mixed examples and the fail-closed state rules.
A defaults cluster profile is deep-merged under every cluster, so identical
fleets are just project entries with no overrides. Projects may declare custom
clusters (overrides and/or several clusters), and identical + custom may be
freely mixed. Projects reference an existing project_id or a name that is
created on demand as project_prefix + name.
Project-scoped object_storage is independent of a cluster's shared
filesystem. storage_class: enhanced maps to Nebius Enhanced Throughput,
size_gibibytes becomes the bucket's exact binary capacity cap, and deploy
requires provider read-back plus a write/read/delete probe. Leave bucket_name
empty in public specs so Fleet derives a stable private name at runtime. Fleet
retains buckets and durable artifacts on cluster destroy; explicit
npa storage bucket delete remains the destructive cleanup boundary.
Fleet uses its own project-scoped storage service account and access key,
separate from LeRobot bootstrap identities, and never enables the editors
fallback. Storage reconciliation currently requires at least one selected mk8s
target in each project that declares storage; soperator-only selections fail
before mutation.
Before any project, bucket, subnet, or cluster mutation, deploy budgets all new
bucket declarations against tenant storage.bucket.count and the matching
storage.bucket.size.<class> allowance (enhanced-throughput for Enhanced).
The size allowance uses bytes, separately from filesystem quotas. Missing,
unreadable, or insufficient evidence fails closed. Every selected existing
bucket must match the exact project, name, region, active/unsuspended state,
storage class, and cap before reuse. Verified existing buckets are not charged
again as new quota demand; a new storage declaration is still checked when its
cluster is unchanged. Read/write success without verified probe deletion cannot
mark Fleet storage ready.
A bucket cap is a maximum, not reserved storage. Available tenant capacity can change after preflight, including when other buckets grow. See the official bucket contract and storage quotas.
Run the read-only reservation and storage quota regression against a private planning spec even before its target projects exist:
NPA_INTEGRATION_E2E=1 NPA_FLEET_QUOTA_VERIFY_SPEC=<private-spec-path> \
npa/.venv/bin/python -m pytest \
npa/tests/e2e/test_fleet_storage_quota_live_e2e.py -q
This proves capacity evidence only; project identity, actual bucket provisioning, GPU readiness, and representative workloads remain separate deployment gates.
apiVersion: npa.fleet/v0.0.1
name: fleet1-test
tenant_id: "" # resolved from ~/.nebius + ~/.npa when empty
region: us-central1
profile: "" # ~/.nebius profile to authenticate as; "" = active
project_prefix: "fleet1-test-"
defaults:
# Replace the next two driver lines with
# gpu_workload_profile: rtx-rendering for RTX/Isaac rendering clusters.
gpu_driver_mode: auto
managed_driver_preset: cuda13.0
allow_unsafe_nvswitch_operator: false
gpu_health_stabilization_seconds: 120
gpu_health_timeout_minutes: 60
gpu_cuda_smoke: true
cpu_nodes: { count: 1, platform: cpu-d3, preset: 48vcpu-192gb }
gpu_nodes:
count: 1
platform: gpu-rtx6000
preset: 1gpu-24vcpu-218gb
# Optional runtime-only ID; renders STRICT and never falls back to PAYG.
capacity_block_group: ""
enable_filestore: true
filestore_disk_size_gibibytes: 1024
filestore_mount_path: /mnt/data
filestore_mount_tag: npa-shared-fs
# Runtime/operator-supplied standards-based chart source. Keep private
# registry endpoints out of committed specs and PR evidence.
filesystem_csi_chart_repository: ""
projects:
- name: a # -> project fleet1-test-a (identical profile)
object_storage: # separate from enable_filestore
enabled: true
storage_class: enhanced
size_gibibytes: 1024
- name: b # -> project fleet1-test-b (identical profile)
# - name: c # custom: overrides + a second cluster
# clusters:
# - name: train
# gpu_nodes: { count: 2, platform: gpu-h200-sxm, preset: 8gpu-128vcpu-1600gb }
# enable_gpu_cluster: true
# infiniband_fabric: us-central1-a
# - name: infer # inherits defaults
# - project_id: project-existing123 # deploy into an existing project by id
# clusters: [ {} ]
Example spec: npa/examples/fleet/fleet1-test.yaml.
Targeting another tenant (profile)
A Nebius service account belongs to exactly one tenant, so deploying into a
second tenant means authenticating as that tenant's principal. Set the spec's
profile: (or pass --profile <name>, which wins) to name a ~/.nebius
profile; every nebius CLI call, the minted terraform TF_VAR_iam_token, and
the generated kubeconfig's exec-credential args are pinned to it. The machine's
active profile is never mutated, so concurrent fleets in different tenants stay
independent.
With a profile set, tenant_id resolves from that profile's tenant-id
(never the active profile's) and a profile with no tenant-id is a hard error
instead of a silent deploy into the wrong tenant. destroy falls back to the
profile recorded in each cluster's env sidecar at deploy time, so a teardown
always authenticates as the principal that created the cluster.
Register a service-account profile non-interactively:
nebius profile create <name> --endpoint api.nebius.cloud \
--service-account-id <sa-id> --public-key-id <public-key-id> \
--private-key-file-path ~/.nebius/<name>.pem \
--parent-id <sa-parent-project> --tenant-id <tenant> --skip-auth
nebius --profile <name> iam get-access-token >/dev/null # verify
nebius profile create also activates the new profile; re-activate the
previous one (nebius profile activate <prev>) if other tooling on the host
depends on it.
Procedure
Parallel applies use a shared download cache only during initialization. NPA holds the cache lock through initialization and copying each target's selected providers into its own Terraform data directory. Later initialization can then rewrite cached downloads without changing binaries used by another apply or destroy. Dependency-lock checksum verification remains enabled.
Older recipe sources can omit newer regions from their default-node table. Materialization preserves known-region defaults while allowing explicit platforms and presets in other regions. It supplies no guessed GPU or fabric; resolve those values from the provider inventory before deployment.
-
Keep committed files public-safe: never hardcode tenant/project/registry IDs or SSH keys. The spec resolves tenant/region from
~/.nebius/config.yamland~/.npa/config.yamlwhen its fields are empty; the SSH public key comes fromssh_public_keyor~/.ssh/id_ed25519.pub/id_rsa.pub. -
Plan first (no infra):
npa fleet plan --spec fleet.yamlshows the projects (create vs existing) and per-cluster node config. -
enable_gpu_cluster is auto: GPU clustering (InfiniBand fabric) is only valid on fabric-capable 8-GPU SXM presets. Single-GPU presets (e.g. RTX PRO 6000
1gpu-24vcpu-218gb) auto-setenable_gpu_cluster=false; set ittrueonly with an 8-GPU preset andinfiniband_fabric. Every GPU pool defaults togpu_driver_mode: auto, which selects Nebius's managed driver image plus the provider device plugin; CPU-only clusters emit no GPU-driver input.managed_driver_presetdefaults to the vendored recipe's supportedcuda13.0and is configurable.operatorremains an explicit escape hatch, but NVSwitch topologies reject it unlessallow_unsafe_nvswitch_operator: trueacknowledges the Network Operator/MOFED versus Fabric Manager host-device race. The same strategy resolver applies tonpa cluster upand Fleet. -
Bind reserved GPU capacity explicitly when required. Set
gpu_nodes.capacity_block_groupto a runtime-supplied Capacity Block Group ID. Fleet rendersgpu_nodes_reservation_policy = { policy = "STRICT", ... }, so an unavailable or incompatible block fails instead of falling back to ordinary on-demand capacity. Never commit a live capacity block ID.Discover and verify reservations read-only with:
nebius --profile <p> capacity capacity-block-group list \ --parent-id <tenant> --all --format json nebius --profile <p> capacity capacity-interval list \ --parent-id <capacity-block-group> --all --format json nebius --profile <p> capacity capacity-block-group list-resources \ --id <capacity-block-group> --format json nebius --profile <p> capacity resource-advice list \ --parent-id <tenant> --all --format jsonPreflight requires the named block to be active, in the target tenant and region, and matched to the GPU platform and InfiniBand fabric. It checks the aggregate GPU requirement against remaining reserved capacity. Only after that validation does it exclude those GPUs from ordinary GPU quota; all node, boot-disk, GPU-cluster, Kubernetes, and storage quotas still apply.
-
Preflight quotas at the tenant, before anything else. Each cluster needs, in the target region:
compute.instance.count(worker nodes only; the managed control plane is service-owned),compute.instance.non-gpu.vcpufor the CPU preset,compute.instance.gpu.<family>for the GPU preset (on-demand GPU quota is frequently 0),compute.disk.count/compute.disk.size.network-ssd,compute.gpucluster.countwhenenable_gpu_cluster, andcompute.filesystem.count+compute.filesystem.size.network-ssdwhenenable_filestore. Each private worker consumes twovpc.allocation.countslots (its private address and pod alias range); the managed control-plane endpoint is service-owned. A create-on-demand project additionally needs onevpc.network.count, onevpc.subnet.count, twovpc.pool.count, onevpc.routetable.count, and onevpc.route.count. List them all at once withnebius --profile <p> quotas quota-allowance list --parent-id <tenant> --all --format json(each item carriesmetadata.name,spec.region,spec.limit).deploydoes this automatically (--preflight, on by default) and refuses to apply when a capacity block or tenant limit cannot cover the in-scope clusters;--no-preflightattempts it anyway. Before calculating creation-only VPC requirements, preflight lists projects once and reuses an existing immutable project ID when a name already exists. An unreadable project inventory fails closed rather than assuming the project exists.Both explicit
project_idtargets and projects resolved by name require a provider read verifying exact ID, tenant, region, active state, and absence of suspension or deletion. An explicit ID may use a local Fleet role whose name differs from the provider project name. With--no-create-projects, a missing target aborts the entire preflight before any project's storage or workers are created; never substitute a stale or deleting project from local state.Run the read-only live identity regression against an owner-private spec with explicit tenant, region, profile, and project IDs:
NPA_INTEGRATION_E2E=1 NPA_FLEET_PROJECT_VERIFY_SPEC=<private-spec-path> \ npa/.venv/bin/python -m pytest \ npa/tests/e2e/test_fleet_project_identity_live_e2e.py -q
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 28
- Forks
- 15
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
fleet- Source
- github.com/nebius/nebius-physical-ai