Terraform configuration for a managed Kubernetes cluster on Scaleway with a fleet of isolated virtual clusters (vclusters) — one per tenant.
- Managed Kubernetes cluster with autoscaling and autohealing
- VPC private network
- Optional GPU node pool — NVIDIA L4 or H100 nodes, scales to zero when idle
- VCluster fleet: one isolated virtual cluster per tenant, provisioned via Helm (chart 0.30.4)
- Per-tenant resource quotas and network isolation (blocks cloud metadata service)
- Environment-specific configuration via
.tfvarsfiles
Cloud provider rating systems (e.g. ClusterMAX by SemiAnalysis) score providers on multi-tenancy quality as a distinct criterion. Three properties of this vcluster implementation directly contribute to that score:
1. Dedicated control plane per tenant
Namespace-based multi-tenancy is the naive approach — all tenants share one API server, so rate limits, RBAC scope, and failure blast radius are shared too. Each vcluster runs its own isolated k3s API server as a StatefulSet. A tenant crashing their workloads, exhausting API request budgets, or misconfiguring RBAC cannot affect any other tenant's control plane.
2. Noisy-neighbour prevention
Hard resource quotas are enforced 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 CPU and memory their namespace can consume on the underlying Scaleway nodes. Tenant A cannot starve Tenant B regardless of what runs inside their virtual cluster.
3. Metadata service lockdown
A network egress policy on each tenant namespace blocks all traffic to 169.254.169.254 — the Scaleway instance metadata endpoint. Without this, any workload inside a vcluster could reach the host node's metadata API and extract IAM credentials or instance configuration. This is a concrete, auditable security control for shared-infrastructure environments.
- Scaleway account — scaleway.com
- API credentials — generate at Scaleway Console > IAM > API Keys
- Terraform >= 1.0 — terraform.io
- kubectl — kubernetes.io
- vcluster CLI —
brew install loft-sh/tap/vclusteror github.com/loft-sh/vcluster
Set environment variables:
export SCW_ACCESS_KEY="your-access-key"
export SCW_SECRET_KEY="your-secret-key"
export SCW_DEFAULT_PROJECT_ID="your-project-id"Or configure ~/.config/scw/config.yaml:
access_key: your-access-key
secret_key: your-secret-key
default_project_id: your-project-id
default_region: fr-par
default_zone: fr-par-1terraform init
terraform validate
terraform plan -var-file="dev.tfvars"
terraform apply -var-file="dev.tfvars"# Option 1: via Terraform output
terraform output -raw kubeconfig_file > kubeconfig
export KUBECONFIG=$(pwd)/kubeconfig
kubectl get nodes
# Option 2: via Scaleway CLI
scw k8s kubeconfig get <cluster-id> > kubeconfig
export KUBECONFIG=$(pwd)/kubeconfigTenants are defined as a map in your .tfvars file. Each entry provisions an isolated vcluster in its own namespace.
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"
isolation_enabled = true
# ... resource quota fields
}
}Add a tenant by adding an entry; remove by deleting it. Terraform handles the rest.
vcluster connect team-ml -n vcluster-team-ml
kubectl get nodes
kubectl get namespaces
vcluster disconnect- Dedicated k3s control plane (chart 0.30.4)
- Service CIDR aligned to Scaleway host cluster (
10.32.0.0/12) - Node sync from host cluster
- Resource quota (CPU + memory) on the host namespace
- Network policy blocking cloud metadata service (
169.254.169.254)
| Variable | Description | Default |
|---|---|---|
cluster_name |
Cluster name | Required |
project_id |
Scaleway project ID | Required |
region |
Scaleway region | fr-par |
zone |
Scaleway zone | fr-par-1 |
k8s_version |
Kubernetes minor version (e.g. 1.32) |
1.32 |
cni |
CNI plugin (cilium, calico, kilo) |
cilium |
node_type |
Node instance type | DEV1-M |
node_count |
Initial node count | 3 |
min_nodes |
Autoscaler minimum | 1 |
max_nodes |
Autoscaler maximum | 10 |
autoscaling_enabled |
Enable node autoscaling | true |
autohealing_enabled |
Enable node autohealing | true |
delete_additional_resources |
Delete PVs/LBs on destroy | false |
pool_name |
Node pool name | default-pool |
private_network_name |
VPC name (defaults to {cluster_name}-pn) |
"" |
tags |
Resource tags | [] |
| Variable | Description | Default |
|---|---|---|
gpu_pool_enabled |
Enable a dedicated GPU node pool | false |
gpu_pool_name |
GPU pool name | gpu-pool |
gpu_node_type |
GPU instance type (e.g. L4-1-24G, H100-1-80G) |
L4-1-24G |
gpu_node_count |
Initial GPU node count | 1 |
gpu_min_nodes |
Minimum GPU nodes (0 = scale to zero) | 0 |
gpu_max_nodes |
Maximum GPU nodes for autoscaling | 2 |
gpu_autoscaling_enabled |
Enable GPU node autoscaling | true |
gpu_autohealing_enabled |
Enable GPU node autohealing | true |
Scaleway automatically installs the NVIDIA GPU operator on GPU node types — no manual setup required.
| Variable | Description | Default |
|---|---|---|
vclusters |
Map of tenant vclusters (see above) | {} |
Each vcluster object supports:
| Field | Description | Default |
|---|---|---|
cpu_limit |
CPU limit for vcluster control plane | 1000m |
memory_limit |
Memory limit | 2Gi |
storage_size |
Persistent volume size | 5Gi |
storage_class |
Storage class | scw-bssd |
isolation_enabled |
Enable resource quota + metadata network policy | true |
resource_quota_cpu |
Namespace CPU request quota | 10 |
resource_quota_memory |
Namespace memory request quota | 20Gi |
resource_quota_cpu_limit |
Namespace CPU limit quota | 20 |
resource_quota_memory_limit |
Namespace memory limit quota | 40Gi |
| Output | Description |
|---|---|
cluster_id |
Cluster identifier |
cluster_url |
Kubernetes API endpoint |
cluster_status |
Current cluster status |
cluster_version |
Kubernetes version |
kubeconfig_file |
Complete kubeconfig (sensitive) |
pool_current_size |
Current number of nodes |
wildcard_dns |
Wildcard DNS for cluster services |
vclusters_deployed |
Map of deployed vclusters with connect commands |
vcluster_fleet_count |
Number of provisioned vclusters |
gpu_pool_id |
GPU pool identifier (null if disabled) |
gpu_pool_status |
GPU pool status (null if disabled) |
gpu_pool_current_size |
Current number of GPU nodes (null if disabled) |
terraform destroy -var-file="dev.tfvars"If delete_additional_resources = false (default), manually clean up any remaining Persistent Volumes and Load Balancers in the Scaleway console.
auto_upgrade version error — Use minor version format (1.32, not 1.32.3). Auto-upgrade is enabled by default.
vcluster pods in CrashLoopBackOff — Check the service CIDR. The chart is pre-configured for Scaleway's 10.32.0.0/12. If your host cluster uses a different CIDR, update controlPlane.distro.k3s.extraArgs in vclusters.tf.
Nodes not scaling — Verify autoscaling_enabled = true and that pods have resource requests set.