Skip to main content

Cluster Class with Steward

ClusterClass is a Cluster API feature that enables template-based cluster creation. When combined with Steward's hosted control plane architecture, ClusterClass provides a powerful pattern for standardizing Kubernetes cluster deployments across multiple infrastructure providers while maintaining consistent control plane configurations.

!!! warning "Experimental Feature" ClusterClass is still an experimental feature of Cluster API. As with any experimental features it should be used with caution. Read more about ClusterClass in the Cluster API documentation.

Understanding Cluster Class

ClusterClass reduces configuration boilerplate by defining reusable cluster templates. Instead of creating individual resources for each cluster, you define a ClusterClass once and create multiple clusters from it with minimal configuration.

With Steward, this pattern becomes even more powerful:

  • Shared Control Plane Templates: The same StewardControlPlaneTemplate works across all infrastructure providers
  • Infrastructure Flexibility: Deploy worker nodes on vSphere, AWS, Azure, or any supported provider while maintaining consistent control planes
  • Simplified Management: Hosted control planes reduce the complexity of ClusterClass templates

Enabling Cluster Class

To use ClusterClass with Steward, you need to enable the cluster topology feature gate before initializing the management cluster:

export CLUSTER_TOPOLOGY=true
clusterctl init --control-plane steward --infrastructure vsphere

This will install:

  • Cluster API core components with ClusterClass support
  • Steward Control Plane Provider
  • Your chosen infrastructure provider (vSphere in this example)

Verify the installation:

kubectl get deployments -A | grep -E "capi|steward"

Template Architecture with Steward

A ClusterClass with Steward consists of four main components:

  1. Control Plane Template (StewardControlPlaneTemplate): Defines the hosted control plane configuration that remains consistent across infrastructure providers.

  2. Infrastructure Template (VSphereClusterTemplate): Provider-specific infrastructure configuration for the cluster.

  3. Bootstrap Template (KubeadmConfigTemplate): Node initialization configuration that works across providers.

  4. Machine Template (VSphereMachineTemplate): Provider-specific machine configuration for worker nodes.

Here's how these components relate in a ClusterClass:

apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
name: steward-vsphere-class
spec:
# Infrastructure provider template
infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereClusterTemplate
name: vsphere-cluster-template

# Steward control plane template - reusable across providers
controlPlane:
ref:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
name: steward-control-plane-template

# Worker configuration
workers:
machineDeployments:
- class: default-worker
template:
bootstrap:
ref:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
name: worker-bootstrap-template
infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
name: vsphere-worker-template

The key advantage: the StewardControlPlaneTemplate and KubeadmConfigTemplate can be shared across different infrastructure providers, while only the infrastructure-specific templates need to change.

Creating a Cluster Class

Let's create a ClusterClass for vSphere with Steward. First, define the shared templates:

StewardControlPlaneTemplate

This template defines the hosted control plane configuration:

apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
metadata:
name: steward-controlplane
namespace: capi-templates-vsphere
spec:
template:
spec:
dataStoreName: "default" # Default datastore for etcd

network:
serviceType: LoadBalancer
serviceAddress: ""
certSANs: []

addons:
coreDNS: {}
kubeProxy: {}
konnectivity: {}

apiServer:
extraArgs: []
resources:
requests: {}
controllerManager:
extraArgs: []
resources:
requests: {}
scheduler:
extraArgs: []
resources:
requests: {}

kubelet:
cgroupfs: systemd
preferredAddressTypes:
- InternalIP

registry: "registry.k8s.io"

KubeadmConfigTemplate

This bootstrap template configures worker nodes:

apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
metadata:
name: worker-bootstrap-template
spec:
template:
spec:

# Configuration for kubeadm join
joinConfiguration:
discovery: {}
nodeRegistration:
criSocket: /var/run/containerd/containerd.sock
imagePullPolicy: IfNotPresent
name: '{{ local_hostname }}'
kubeletExtraArgs:
cloud-provider: external
node-ip: "{{ ds.meta_data.local_ipv4 }}"

# Commands to run before kubeadm join
preKubeadmCommands:
- hostnamectl set-hostname "{{ ds.meta_data.hostname }}"
- echo "127.0.0.1 {{ ds.meta_data.hostname }}" >> /etc/hosts

# Commands to run after kubeadm join
postKubeadmCommands: []

# Users to create on worker nodes
users: []

VSphereClusterTemplate

Infrastructure-specific template for vSphere:

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereClusterTemplate
metadata:
name: vsphere
namespace: capi-templates-vsphere
spec:
template:
spec:
server: "vcenter.sample.com" # vCenter server address
thumbprint: "" # vCenter certificate thumbprint

identityRef:
kind: VSphereClusterIdentity
name: "vsphere-cluster-identity"

failureDomainSelector: {}
clusterModules: []

VSphereMachineTemplate

Machine template for vSphere workers:

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
metadata:
name: vsphere-vm-base
namespace: capi-templates-vsphere
spec:
template:
spec:
# Resources will be patched by ClusterClass based on variables
# numCPUs, memoryMiB, diskGiB are dynamically set

# Infrastructure defaults - will be patched by ClusterClass
server: "vcenter.sample.com"
datacenter: "datacenter"
datastore: "datastore"
resourcePool: "Resources"
folder: "vm-folder"
template: "ubuntu-2404-kube-v1.32.0"
storagePolicyName: ""
thumbprint: ""

# Network configuration (IPAM by default)
network:
devices:
- networkName: "k8s-network"
dhcp4: false
addressesFromPools:
- apiGroup: ipam.cluster.x-k8s.io
kind: InClusterIPPool
name: "{{ .builtin.cluster.name }}" # Uses cluster name

Variables and Patching in Cluster Class

ClusterClass becomes powerful through its variable system and JSON patching capabilities. This allows the same templates to be customized for different use cases without duplicating YAML.

Variable System

Variables in ClusterClass define the parameters users can customize when creating clusters. Each variable has:

  • Schema Definition: OpenAPI v3 schema that validates input
  • Required/Optional: Whether the variable must be provided
  • Default Values: Fallback values when not specified
  • Type Constraints: Data types, ranges, and enum values

Here's how variables work in practice:

Control Plane Variables:

variables:
- name: stewardControlPlane
required: true
schema:
openAPIV3Schema:
type: object
properties:
dataStoreName:
type: string
description: "Datastore name for etcd"
default: "default"
network:
type: object
properties:
serviceType:
type: string
enum: ["ClusterIP", "NodePort", "LoadBalancer"]
default: "LoadBalancer"
serviceAddress:
type: string
description: "Pre-assigned VIP address"

Machine Resource Variables:

- name: machineSpecs
required: true
schema:
openAPIV3Schema:
type: object
properties:
numCPUs:
type: integer
minimum: 2
maximum: 64
default: 4
memoryMiB:
type: integer
minimum: 4096
maximum: 131072
default: 8192
diskGiB:
type: integer
minimum: 40
maximum: 2048
default: 100

JSON Patching System

Patches apply variable values to the base templates at cluster creation time. This enables the same template to serve different configurations.

Control Plane Patching:

patches:
- name: controlPlaneConfig
definitions:
- selector:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
matchResources:
controlPlane: true
jsonPatches:
- op: replace
path: /spec/template/spec/dataStoreName
valueFrom:
variable: stewardControlPlane.dataStoreName
- op: replace
path: /spec/template/spec/network/serviceType
valueFrom:
variable: stewardControlPlane.network.serviceType

Machine Resource Patching:

- name: machineResources
definitions:
- selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
matchResources:
machineDeploymentClass:
names: ["default-worker"]
jsonPatches:
- op: add # Resources are not in base template
path: /spec/template/spec/numCPUs
valueFrom:
variable: machineSpecs.numCPUs
- op: add
path: /spec/template/spec/memoryMiB
valueFrom:
variable: machineSpecs.memoryMiB

Advanced Patching Patterns

Conditional Patching:

- name: optionalVIP
definitions:
- selector:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
jsonPatches:
- op: replace
path: /spec/template/spec/network/serviceAddress
valueFrom:
variable: stewardControlPlane.network.serviceAddress
# Only applies if serviceAddress is not empty
enabledIf: "{{ ne .stewardControlPlane.network.serviceAddress \"\" }}"

Infrastructure Patching:

- name: infrastructureConfig
definitions:
- selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
jsonPatches:
- op: replace
path: /spec/template/spec/datacenter
valueFrom:
variable: infrastructure.datacenter
- op: replace
path: /spec/template/spec/datastore
valueFrom:
variable: infrastructure.datastore
- op: replace
path: /spec/template/spec/template
valueFrom:
variable: infrastructure.vmTemplate

Complete Cluster Class with Variables

For a comprehensive example with all variables and patches configured, see the vsphere-steward-clusterclass.yaml template.

Creating a Cluster from Cluster Class

With the ClusterClass defined, creating a cluster becomes remarkably simple:

apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: my-cluster
namespace: default
spec:
# Network configuration defined at cluster level
clusterNetwork:
pods:
cidrBlocks: ["10.244.0.0/16"]
services:
cidrBlocks: ["10.96.0.0/12"]
serviceDomain: "cluster.local"

topology:
class: vsphere-standard
classNamespace: capi-templates-vsphere
version: v1.32.0

controlPlane:
replicas: 2

workers:
machineDeployments:
- class: default-worker
name: worker-nodes
replicas: 3

variables:
- name: stewardControlPlane
value:
dataStoreName: "etcd"
network:
serviceType: "LoadBalancer"
serviceAddress: "" # Auto-assigned if empty

- name: machineSpecs
value:
numCPUs: 8
memoryMiB: 16384
diskGiB: 60

- name: infrastructure
value:
vmTemplate: "ubuntu-2404-kube-v1.32.0"
datacenter: "K8s-TI-dtc"
datastore: "K8s-N01td-01"
resourcePool: "rp-steward-dev"
folder: "my-cluster-vms"

- name: networking
value:
networkName: "VM-K8s-TI-cpmgmt"
nameservers: ["8.8.8.8", "1.1.1.1"]
dhcp4: false # Using IPAM

Create the cluster:

kubectl apply -f my-cluster.yaml

Monitor cluster creation:

clusterctl describe cluster my-cluster
kubectl get cluster,stewardcontrolplane,machinedeployment -n default

With this approach, the same StewardControlPlaneTemplate and KubeadmConfigTemplate can be reused when creating ClusterClasses for AWS, Azure, or any other provider. Only the infrastructure-specific templates need to change.

Cross-Provider Template Reuse

One of Steward's key advantages with ClusterClass is template modularity across providers. Here's how to leverage this:

Shared Templates Repository

Create a namespace for shared templates:

kubectl create namespace cluster-templates

Deploy shared Steward and bootstrap templates once:

kubectl apply -n cluster-templates -f steward-controlplane-template.yaml
kubectl apply -n cluster-templates -f kubeadm-config-template.yaml

Provider-Specific Cluster Classes

For each infrastructure provider, create a ClusterClass that references the shared templates:

AWS Cluster Class

apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
name: steward-aws-class
spec:
controlPlane:
ref:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
name: steward-controlplane
namespace: cluster-templates # Shared template

infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSClusterTemplate
name: aws-cluster-template # AWS-specific

workers:
machineDeployments:
- class: default-worker
template:
bootstrap:
ref:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
name: kubeadm
namespace: cluster-templates # Shared template
infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSMachineTemplate
name: aws-worker-template # AWS-specific

Azure Cluster Class

apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
name: steward-azure-class
spec:
controlPlane:
ref:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: StewardControlPlaneTemplate
name: steward-control-plane-template
namespace: cluster-templates # Same shared template

infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AzureClusterTemplate
name: azure-cluster-template # Azure-specific

workers:
machineDeployments:
- class: default-worker
template:
bootstrap:
ref:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
name: worker-bootstrap-template
namespace: cluster-templates # Same shared template
infrastructure:
ref:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AzureMachineTemplate
name: azure-worker-template # Azure-specific

Managing Cluster Class Lifecycle

Listing Available Cluster Classes

kubectl get clusterclasses -A

Viewing Cluster Class Details

kubectl describe clusterclass vsphere-standard -n capi-templates-vsphere

Updating a Cluster Class

A ClusterClass update affects only new clusters. Existing clusters continue using their original configuration:

kubectl edit clusterclass vsphere-standard -n capi-templates-vsphere

Deleting Clusters Created from Cluster Class

Always delete clusters before removing the ClusterClass:

# Delete the cluster
kubectl delete cluster my-cluster

# Wait for cleanup
kubectl wait --for=delete cluster/my-cluster --timeout=10m

# Then safe to delete ClusterClass if no longer needed
kubectl delete clusterclass vsphere-standard -n capi-templates-vsphere

Template Versioning Strategies

When managing ClusterClasses across environments, consider these versioning approaches:

Semantic Versioning in Names

metadata:
name: vsphere-standard-v1-2-0
namespace: capi-templates-vsphere

Using Labels for Version Tracking

metadata:
name: vsphere-standard
namespace: capi-templates-vsphere
labels:
version: "1.2.0"
stability: "stable"
tier: "standard"

Namespace Separation

kubectl create namespace clusterclass-v1
kubectl create namespace clusterclass-v2

This enables gradual migration between ClusterClass versions while maintaining compatibility.

Further Reading