Skip to main content

StatefulSet Workloads - Details

← Back to StatefulSet Workloads Tutorial

This document provides comprehensive information about spec.deployment.kind: StatefulSet, including the immutability constraint, volumeClaimTemplates provisioning, podManagementPolicy semantics, and the deferred headless-Service support.

kind Field​

spec.deployment.kind selects the workload controller:

ValueMeaning
Deployment (default)The platform builds a Kubernetes Deployment. All previous lessons assume this.
StatefulSetThe platform builds a Kubernetes StatefulSet. Unlocks volumeClaimTemplates and podManagementPolicy.

The field is immutable after creation. A CEL rule on the CRD (self.kind == oldSelf.kind) rejects any attempt to change kind on an existing PlatformApplication. To switch workload types, delete the PlatformApplication and re-create with the new kind. PVCs created by the platform have no owner reference to the application, so they survive the deletion and are re-adopted on the next apply if names match.

Existing manifests with no kind field continue to deserialize as Deployment — the field default makes the change zero-migration for any application created before this feature shipped.

volumeClaimTemplates​

spec.deployment.volumeClaimTemplates[] is a list of per-pod PVC templates. Kubernetes provisions one PVC per pod ordinal per template. A PlatformApplication StatefulSet currently runs one pod, so each template yields one PVC (<name>-<sts>-0). For more information, see Scaling.

FieldRequiredNotes
nameyesPVC name prefix and volume mount name. PVC becomes <name>-<sts>-<ordinal> (e.g., data-demo-http-echo-0).
mountPathyesPath inside the container where the volume mounts.
sizeyesRequested storage (e.g., 10Gi).
storageClassNamenoProvisioner. Defaults to the cluster default StorageClass.
accessModesnoDefaults to [ReadWriteOnce]. Per-pod PVCs are almost always RWO; multiple pods don't share a per-pod template.

Every entry in volumeClaimTemplates automatically gets a matching volumeMount on the container — you don't add a volumeMounts[*] entry separately for the same name.

When to use volumeClaimTemplates vs volumeMounts​

Both fields can coexist on a StatefulSet:

  • Use volumeClaimTemplates for storage that should be per-pod (data dirs for replicated databases, per-shard storage for sharded systems).
  • Use volumeMounts[*] with source.claimName for shared storage across all pods (RWX volumes, ReadOnlyMany configuration mounts).
  • Use volumeMounts[*] with no source for emptyDir, ConfigMap, or Secret file mounts — same as on a Deployment.

volumeMounts[*].source.size (provisioning a new shared PVC) is unusual on a StatefulSet — if you need shared storage, prefer referencing a pre-created RWX PVC by claimName.

podManagementPolicy​

Kubernetes StatefulSet.spec.podManagementPolicy controls pod creation/deletion ordering:

ValueBehavior
OrderedReady (Kubernetes default)Pods are created in order (-0 before -1 before -2), each waiting for the previous to be Ready before starting. Termination is reverse order.
ParallelAll pods are created and deleted in parallel. The StatefulSet still gives each pod its stable identity and PVC, but doesn't gate on readiness.

Use OrderedReady for replicated systems with quorum requirements (consensus protocols, leader-elected databases) where bringing up multiple peers simultaneously could cause split-brain. Use Parallel for sharded systems where pods are independent and you want faster rollouts.

The CRD enforces the enum at admission time via a CEL rule:

!has(self.podManagementPolicy) || self.podManagementPolicy == 'OrderedReady' || self.podManagementPolicy == 'Parallel'

A typo (podManagementPolicy: ordered) gets rejected immediately, not silently dropped.

Field Restrictions​

CEL rules enforce that volumeClaimTemplates and podManagementPolicy are StatefulSet-only:

self.kind == 'StatefulSet' || (!has(self.volumeClaimTemplates) || self.volumeClaimTemplates.size() == 0)
self.kind == 'StatefulSet' || !has(self.podManagementPolicy)

Setting either field on a Deployment (or omitting kind, which defaults to Deployment) is rejected at admission.

Singleton-Mode Override Skipped​

The singleton-mode override from lesson 5 is explicitly gated on kind: Deployment. StatefulSets don't trigger it, even with RWO volumeClaimTemplates, because each pod gets its own PVC.

The matching admission rejection (autoscaling + RWO PVC) is also gated on kind: Deployment. A StatefulSet with autoscaling and RWO volumeClaimTemplates passes admission, but the autoscaling settings currently don't change the pod count. For more information, see Scaling.

Scaling​

A PlatformApplication with kind: StatefulSet currently runs a single pod:

  • PlatformApplication has no replicas field, and the platform doesn't set StatefulSet.spec.replicas, so the StatefulSet starts at the Kubernetes default of one pod.
  • The platform still creates a HorizontalPodAutoscaler (CPU) or, when autoscaling.triggers is set, a KEDA ScaledObject. Both set scaleTargetRef.kind: Deployment, so neither resolves to the StatefulSet. The HorizontalPodAutoscaler reports AbleToScale=False with a FailedGetScale event, and the ScaledObject reports Ready=False with ScaledObjectCheckFailed.
  • Alongside the autoscaler, the platform creates a PodDisruptionBudget with minAvailable: 1. With only one pod, that budget can block voluntary evictions such as node drains and cluster upgrades. Setting autoscaling.enabled: false and leaving out autoscaling.triggers stops the platform from creating the budget. It doesn't remove one that already exists, so check with kubectl get pdb -n <namespace>.

Admission accepts the manifest and the PlatformApplication reports no error, so the only signal is the autoscaler's own status. Support for scaling StatefulSets is tracked as a follow-up.

To see the autoscaler status for an application:

kubectl describe hpa -n <namespace> <app>
kubectl get scaledobject -n <namespace> <app> -o jsonpath='{.status.conditions}'

Headless Service (Deferred)​

StatefulSet.spec.serviceName is required by the Kubernetes API. The platform points it at the existing primary Service so the StatefulSet is well-formed. This does not give you per-pod DNS.

Per-pod DNS records (pod-N.<svc>.<ns>.svc.cluster.local) require the named Service to be headless (spec.clusterIP: None). The platform's primary Service is not headless — it's a normal ClusterIP Service used by ingress and cluster-internal traffic.

If you need stable per-pod DNS today (peer discovery for Cassandra, etcd, ZooKeeper, Postgres replicas with built-in DNS-based discovery), create a headless Service manually alongside the PlatformApplication:

apiVersion: v1
kind: Service
metadata:
name: demo-http-echo-headless
namespace: demo-http-echo
spec:
clusterIP: None
selector:
p6m.dev/app: demo-http-echo
ports:
- name: http
port: 8080

Then your application can resolve peers at demo-http-echo-0.demo-http-echo-headless.demo-http-echo.svc.cluster.local. Operator-managed headless Service support is tracked as a follow-up; until then, hand-rolled is the workaround.

PVC Retention​

When the StatefulSet is deleted, the platform does not delete the per-pod PVCs. This matches the Kubernetes default StatefulSet PVC retention policy and prevents accidental data loss.

Re-applying the PlatformApplication re-binds to the existing PVCs by name, so data persists across rollouts and re-creations.

To free the storage explicitly:

kubectl delete pvc -n <namespace> -l app.kubernetes.io/instance=<app-name>

If you need PVC deletion tied to StatefulSet lifecycle, edit the StatefulSet directly to set spec.persistentVolumeClaimRetentionPolicy — the platform doesn't surface this field on PlatformApplication yet.

Verification Procedures​

Confirm the StatefulSet was created​

kubectl get statefulset -n <namespace>
# Should show one StatefulSet with the application name

kubectl get deployment -n <namespace>
# Should show no Deployment for the same name

Confirm the pod's PVC​

kubectl get pvc -n <namespace>
# data-<app>-0 Bound ...

Verify the stable pod identity​

kubectl exec -n <namespace> <app>-0 -- hostname
# <app>-0

Inspect podManagementPolicy​

kubectl get statefulset -n <namespace> <app> -o jsonpath='{.spec.podManagementPolicy}'
# OrderedReady (or Parallel)