Skip to main content

StatefulSet Workloads

This example builds on Persistent Storage by switching the workload from a Deployment to a StatefulSet. A StatefulSet gives each pod its own PersistentVolumeClaim and a stable identity, with ordered rolling updates.

A StatefulSet runs a single pod

A PlatformApplication with kind: StatefulSet currently runs exactly one pod, and autoscaling doesn't change that. See Scaling for why, and for a side effect on node drains.

What You'll Learn​

In this lesson, you'll learn:

  • How to switch a PlatformApplication from Deployment to StatefulSet
  • volumeClaimTemplates and how per-pod PVCs are provisioned
  • podManagementPolicy (OrderedReady vs Parallel)
  • Why singleton-mode constraints don't apply to StatefulSets
  • Why a StatefulSet currently runs a single pod
  • When a StatefulSet is the right call vs a Deployment with a shared PVC

When to Use a StatefulSet​

Use a StatefulSet when...Use a Deployment + PVC when...
Storage that belongs to a specific pod identity (databases, queues)Single replica, possibly with a single PVC
Pod identity matters (leader election, peer discovery)All pods are interchangeable
You need ordered startup/shutdownRolling updates are fine
You're running Postgres/Redis/Kafka/ElasticsearchYou're running a stateless web service

If you're not sure, start with Deployment. The StatefulSet workload kind is immutable after creation — switching means deleting and recreating the PlatformApplication.

What Gets Created​

Compared to Persistent Storage, the platform now creates:

  • A StatefulSet instead of a Deployment
  • One PersistentVolumeClaim per pod, per volumeClaimTemplates[] entry — Kubernetes provisions them as the pods come up (<template>-<sts>-0, <template>-<sts>-1, ...)
  • serviceName on the StatefulSet points at the existing primary Service

What Changed​

Switched kind to StatefulSet and replaced volumeMounts.source.size with volumeClaimTemplates:

application.yaml
apiVersion: meta.p6m.dev/v1alpha1
kind: PlatformApplication
metadata:
name: demo-http-echo
namespace: demo-http-echo
labels:
p6m.dev/app: demo-http-echo
spec:
config:
LOG_LEVEL: debug
HTTP_PORT: "8080"
ENVIRONMENT: dev
APP_NAME: demo-http-echo
secrets:
- name: super-secret-data
deployment:
kind: StatefulSet
podManagementPolicy: OrderedReady
image: mendhak/http-https-echo:31
ports:
- port: 8080
protocol: http
readinessProbe:
port: 8080
path: /
volumeClaimTemplates:
- name: data
mountPath: /var/lib/app
size: 10Gi
networking:
ingress:
enabled: true
path: /
gateway: public-open

The pod demo-http-echo-0 gets its own PVC, named data-demo-http-echo-0. Kubernetes names the PVCs of any further pods the same way (data-demo-http-echo-1, and so on), but a StatefulSet currently runs one pod. For more information, see Scaling.

volumeClaimTemplates vs volumeMounts​

The two ways of attaching persistent storage look similar but mean very different things:

FieldWorkloadBehavior
spec.deployment.volumeMounts[*].source.size (lesson 5)DeploymentOne PVC, shared across replicas. Forces singleton mode for RWO.
spec.deployment.volumeClaimTemplates[] (this lesson)StatefulSetOne PVC per pod, so pods never share a volume. No singleton mode.

volumeMounts[] still works on a StatefulSet for emptyDir, ConfigMap, and Secret mounts. Only the PVC source (source.size / source.claimName) is replaced by volumeClaimTemplates on a StatefulSet.

podManagementPolicy​

Controls how Kubernetes creates and deletes pods during a rollout:

ValueBehaviorUse For
OrderedReady (default)Pod N+1 doesn't start until pod N is ready. Shutdown in reverse.Apps with leader election, replicated databases that need quorum, anything where bringing a peer up out-of-order would cause split-brain.
ParallelAll pods start (and stop) simultaneously.Sharded apps where each pod is independent. Faster rollouts when ordering doesn't matter.

Field is optional; omit it to get the Kubernetes default (OrderedReady).

No Singleton Mode Override​

The singleton-mode override from lesson 5 — which forces replicas: 1 and strategy: Recreate on a Deployment with an RWO PVC — does not apply to StatefulSets. Each StatefulSet pod gets its own PVC, so two pods never contend for the same volume, and rolling updates work the way they would for any other StatefulSet.

The autoscaling + RWO PVC admission rejection from lesson 5 is also gated on kind: Deployment, so admission accepts a StatefulSet with autoscaling settings. Those 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, so the StatefulSet starts at the Kubernetes default of one pod.
  • The platform still creates a HorizontalPodAutoscaler, or a KEDA ScaledObject when you set autoscaling.triggers. Both target a Deployment with the application's name rather than the StatefulSet, so they never change its pod count. The HorizontalPodAutoscaler reports FailedGetScale, and the ScaledObject never becomes ready.
  • 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 demo-http-echo.

Nothing reports an error for this: admission accepts the manifest and the PlatformApplication shows no failure. Support for scaling StatefulSets is tracked as a follow-up. Until it ships, plan capacity for a single pod.

Deploy Steps​

ArgoCD automatically updates to your PlatformApplication after the Platform Dispatch Action to update your .platform repository is run.

Use the Kinds filter to locate the StatefulSet (now in place of the Deployment) and the per-pod PersistentVolumeClaim resources that come up alongside it.

Common Observations​

Why is serviceName set to the same name as the primary Service?

Per Kubernetes API, StatefulSet.spec.serviceName is a required field. 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 (pod-N.<svc>.<ns>.svc.cluster.local) requires a headless Service, which the platform doesn't yet create. If you need stable per-pod DNS today (e.g., for Cassandra or etcd peer discovery), create a headless Service manually alongside the PlatformApplication. Operator-managed headless Service support is tracked as a follow-up.

My PVCs aren't deleted when I delete the StatefulSet.

Deliberate. Kubernetes' default StatefulSet PVC retention policy keeps per-pod volumes after deletion so a re-apply re-binds to the existing data. The platform doesn't override this. To actually drop the storage:

kubectl delete pvc -n demo-http-echo -l app.kubernetes.io/instance=demo-http-echo

Can I change kind: Deployment to kind: StatefulSet (or back) on an existing app?

No. The kind field is immutable after creation — admission rejects the change with a clear error. Workload migration requires deleting and recreating the PlatformApplication. PVCs survive the deletion (no owner reference to the PlatformApplication), so a fresh apply with a StatefulSet will adopt the existing data if PVC names match.

Does this still get an HPA?

The platform creates one, but it targets a Deployment and doesn't scale the StatefulSet. For more information, see Scaling.

tip

Want to dive deeper? See StatefulSet - Details for kind immutability, podManagementPolicy semantics, headless-Service workarounds, and per-pod DNS patterns.

You've Completed the Tutorial! 🎉​

Further Learning:

  • Cloud Resources with Crossplane
  • Autoscaling with KEDA
  • Service Mesh configuration
  • Observability integration

Check the Platform Documentation for advanced topics.

Troubleshooting​

For common issues and solutions, see the Troubleshooting Guide.

Specific sections that may be helpful:

Cleanup​

Check out the Cleanup Instructions from the Basic Deployment lesson to remove all resources created in this walkthrough.

Per-pod PVCs created by a StatefulSet are intentionally not deleted when the application is removed. Drop them explicitly:

kubectl delete pvc -n demo-http-echo -l app.kubernetes.io/instance=demo-http-echo