Running Multi-Tenant GPU Workloads on Scaleway with vCluster

published at 07-22-2026 by Mohit Gupta

Namespace-per-team is the default way to share a GPU cluster. It's also the wrong one. This is what true multi-tenancy looks like in Terraform.

GPU clusters are expensive to run and expensive to misuse. When multiple teams share one, the standard Kubernetes answer — give each team a namespace — turns out to be surprisingly thin. This post walks through a Terraform-based setup that replaces namespace isolation with per-tenant virtual clusters on Scaleway, and explains why the distinction matters specifically for GPU workloads.
 
Rating frameworks like ClusterMAX (SemiAnalysis) increasingly score GPU cloud providers on Kubernetes quality — not just raw hardware — with per-tenant isolation and multi-tenancy as explicit criteria. The setup described here directly targets that dimension.
 

 

The Problem: Namespace Isolation Is Not Enough

Consider a typical GPU cloud setup. A provider deploys a single Kubernetes cluster and gives
each AI team their own namespace:
cluster: k8s-prod
├── namespace: team-ml ← Team A
├── namespace: team-dev ← Team B
└── namespace: team-research ← Team C
 
This looks clean until you look at what's actually shared:
1. Shared control plane — all teams share the same API server and etcd. A misconfigured RBAC rule can expose one tenant's secrets to another. On a GPU cluster where teams are running proprietary model weights and training data, this is a real risk.
2. Single kubeconfig — there is no clean per-tenant authentication boundary. Access is managed through RBAC rules on a shared system, not through independent credentials.
3. No API server isolation — a team running a large batch of GPU jobs can exhaust API server request budgets and slow down every other tenant's control plane operations.
4. Shared metadata endpoint — any pod in any namespace can reach `169.254.169.254`, the cloud instance metadata API. On Scaleway nodes, this endpoint can return IAM credentials. In a multi-tenant GPU environment, that is a straightforward path to credential theft.
 

 

The Solution: vCluster for True Multi-Tenancy

vCluster creates a virtual Kubernetes cluster inside a namespace of a host cluster. Each tenant gets their own:
 
- API server (lightweight k3s)
- kubeconfig with independent authentication
- Control plane isolated from other tenants
- Resource quotas enforced at the namespace level on the host
 
From the tenant's perspective, they have their own full Kubernetes cluster. From the provider's perspective, it runs on existing nodes with roughly 200MB RAM overhead per virtual cluster.
 
host cluster: k8s-prod (Scaleway)
├── namespace: vcluster-team-ml
│ └── vcluster: team-ml ← Team A gets their own K8s API
├── namespace: vcluster-team-dev
│ └── vcluster: team-dev ← Team B gets their own K8s API
└── GPU pool (shared physical hardware)
└── L4-1-24G nodes (NVIDIA L4 24GB)
 
GPU workloads run on the real host nodes — the vcluster is transparent to CUDA, NCCL, and device plugins. Tenants request GPU resources through standard Kubernetes resource limits inside their vcluster, and those requests are synced down to the host cluster for actual scheduling on the GPU nodes.
 

 

Implementation: Scaleway + Terraform + vCluster

The full configuration provisions a Scaleway Kubernetes cluster with a per-tenant vcluster fleet. The host cluster runs Cilium CNI in fr-par-1, where Scaleway's L4 and H100 GPU nodes are available.

Per-Tenant vCluster Fleet

Tenants are defined as a map in your .tfvars file. Each entry provisions an isolated virtual cluster with its own namespace, resource quotas, and network policies:
 
vclusters = {
  "team-ml" = {
    cpu_limit = "2000m"
    memory_limit = "4Gi"
    storage_size = "10Gi"
    storage_class = "scw-bssd"
    isolation_enabled = true
    resource_quota_cpu = "20"
    resource_quota_memory = "40Gi"
    resource_quota_cpu_limit = "30"
    resource_quota_memory_limit = "60Gi"
  }
  "team-dev" = {
    cpu_limit = "500m"
    memory_limit = "1Gi"
    storage_size = "5Gi"
    storage_class = "scw-bssd"
    isolation_enabled = true
    resource_quota_cpu = "10"
    resource_quota_memory = "20Gi"
    resource_quota_cpu_limit = "15"
    resource_quota_memory_limit = "30Gi"
  }
}

 

For each tenant, vclusters.tf provisions:
 
  1. A dedicated Kubernetes namespace (vcluster-{name})
  2. A Helm-managed vcluster release (chart 0.30.4, k3s-backed)
  3. A ResourceQuota capping CPU and memory at the host namespace level — not just inside the vcluster. Even if a tenant's workloads spike beyond their vcluster limits, the host-side quota caps how much the namespace can consume on the underlying Scaleway nodes. Tenant A cannot starve Tenant B's GPU jobs regardless of what runs inside their virtual cluster.
  4. A NetworkPolicy blocking egress to 169.254.169.254 — the Scaleway instance metadata endpoint. This is enforced at the host namespace level, so it cannot be bypassed from inside the vcluster.

 

resource "kubernetes_network_policy" "vcluster_isolation" {
  for_each = { 
               for k, v in var.vclusters : 
                    k => v if v.isolation_enabled 
             }
  spec {
    egress {
      to {
        ip_block {
          cidr = "0.0.0.0/0"
          except = ["169.254.169.254/32"]
        }
       }
    }
   policy_types = ["Egress"]
   }
}

 

Connecting to a Tenant's vCluster

Each tenant receives their own kubeconfig and connects directly to their virtual cluster:
 
# Connect as team-ml
vcluster connect team-ml -n vcluster-team-ml
kubectl get nodes # sees the real GPU nodes
kubectl get namespaces # only sees their own namespaces
vcluster disconnect
 
# Connect as team-dev
vcluster connect team-dev -n vcluster-team-dev
kubectl get nodes # cannot see team-ml's workloads
 
Two teams on the same physical GPU hardware, with no visibility into each other's control plane, credentials, or workloads.
 

Deploying

terraform init
terraform plan -var-file="dev.tfvars"
terraform apply -var-file="dev.tfvars"
 
Add a tenant by adding an entry to the vclusters map. Remove one by deleting the entry.
Terraform handles the rest.
 

 

What vCluster Does NOT Improve

It is important to be precise about scope. vCluster is transparent to:
 
- GPU performance — CUDA jobs, NCCL collectives, and GPU-to-GPU bandwidth are unaffected. Pod networking goes through the host cluster, so there is no additional latency layer.
- Hardware availability — GPU instance types, availability zones, and interconnect speed are Scaleway infrastructure concerns, not Kubernetes ones.
- Storage IOPS — persistent volumes use the host's block storage (`scw-bssd`) directly. The vcluster layer adds no overhead to storage I/O.
 
What vCluster does improve is the managed Kubernetes experience for each tenant: isolated control planes, independent credentials, enforced resource boundaries, and a locked-down security posture on shared infrastructure.

 


 

Conclusion

Namespace-based multi-tenancy is a reasonable starting point but it leaves meaningful gaps - shared API servers, a single authentication boundary, and no enforcement at the metadata layer. For GPU workloads, where jobs are long-running, expensive, and often proprietary, those gaps matter more than they do in a typical web application cluster.
 
vCluster closes those gaps without requiring dedicated hardware per tenant. The Terraform configuration in this repository is a deployable reference for running an isolated, multi-tenant GPU cluster on Scaleway — each team gets their own Kubernetes cluster, backed by the same pool of GPU nodes.
 

 

References

  1. vCluster — Virtual Clusters for Kubernetes, Loft Labs
  2. Cilium CNI — eBPF-based Networking, Observability, Security, Cilium Project
  3. Scaleway Kubernetes Kapsule, Scaleway
  4. Scaleway GPU Instances — L4 & H100, Scaleway
  5. SemiAnalysis, ClusterMAX GPU Cloud Benchmark, SemiAnalysis