Kubernetes » Workloads & Storage
Kubernetes runs applications through workload controllers: API objects that create and replace Pods to match a declared shape. This page covers the controllers beyond the Deployment (StatefulSet, DaemonSet, Job, CronJob, and native sidecar containers), gives an overview of volumes and persistent storage, and describes the machinery around a running workload: vertical and node autoscaling, Pod security, and health signals. Feature stages are given as of Kubernetes v1.37 (August 2026).
Related pages go deeper: Fundamentals covers Pods, ReplicaSets, and Deployments; Stateful Workloads & Persistence is the storage and StatefulSet reference; Health & Resource Management covers probes, requests and limits, QoS, and the Horizontal Pod Autoscaler.
Choosing a Workload Controller
Every controller manages Pods from a template; they differ in how many Pods, where, and for how long.
| Controller | Runs | Pod identity | Typical workloads |
|---|---|---|---|
| Deployment | N interchangeable replicas, indefinitely | Random names, no per-Pod storage | Web services, APIs, stateless workers |
| StatefulSet | N ordered replicas, indefinitely | Stable names (db-0, db-1) and per-Pod volumes |
Databases, brokers, consensus systems |
| DaemonSet | One Pod per (selected) node | Tied to its node | Log shippers, metrics agents, CNI and CSI node plugins |
| Job | Pods until a number of completions succeed | Optional completion index | Batch processing, migrations, ML training |
| CronJob | A Job on a schedule | Per run | Backups, reports, periodic clean-up |
flowchart TD
Q1{"Does it run to<br/>completion?"} -- yes --> Q2{"On a schedule?"}
Q2 -- yes --> CJ[CronJob]
Q2 -- no --> J[Job]
Q1 -- no --> Q3{"One copy on<br/>every node?"}
Q3 -- yes --> DS[DaemonSet]
Q3 -- no --> Q4{"Needs stable identity<br/>or per-replica storage?"}
Q4 -- yes --> Q5{"A mature operator<br/>exists for it?"}
Q5 -- yes --> OP["Operator<br/>(custom resource)"]
Q5 -- no --> SS[StatefulSet]
Q4 -- no --> D[Deployment]
A bare Pod has no controller: if its node fails, nothing recreates it. Use bare Pods only for one-off debugging.
StatefulSets
A StatefulSet gives each replica a stable name, a stable DNS entry through a headless Service, and its own PersistentVolumeClaim created from a volumeClaimTemplate. Pods are created in ordinal order and, by default, updated one at a time from the highest ordinal down. Recent additions include maxUnavailable for faster rolling updates (beta, enabled by default in v1.37), automatic PVC clean-up on scale-down or deletion (persistentVolumeClaimRetentionPolicy, stable since v1.32), and a Recreate update strategy (alpha in v1.37).
For production databases, prefer an operator (CloudNativePG, Strimzi, and similar) that understands the software’s failover and backup procedures; some of them manage Pods directly rather than through a StatefulSet. The full treatment, including ordering guarantees, stuck rollouts, headless Services, and backup, is in Stateful Workloads & Persistence.
DaemonSets
A DaemonSet runs one copy of a Pod on every node, or on every node matching a nodeSelector or node affinity. When a node joins the cluster it receives the Pod; when the node leaves, the Pod is garbage-collected. DaemonSets are how node-level infrastructure is deployed: log collectors, metrics exporters, the kube-proxy and CNI agents, CSI node plugins, and security sensors.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-agent
namespace: logging
spec:
selector:
matchLabels:
app: log-agent
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 10% # update 10% of nodes at a time
template:
metadata:
labels:
app: log-agent
spec:
priorityClassName: system-node-critical # evicted last under pressure
tolerations:
- operator: Exists # run on tainted nodes too
containers:
- name: fluent-bit
image: fluent/fluent-bit:5.1
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {memory: 256Mi}
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
Points specific to DaemonSets:
- Scheduling. DaemonSet Pods are placed by the default scheduler (with node affinity pinning each Pod to its node), and the controller automatically adds tolerations for conditions such as
not-ready,unreachable, anddisk-pressure, so agents keep running on unhealthy nodes. Tolerating all taints, as above, is appropriate for true infrastructure agents and wrong for ordinary applications. - Updates.
RollingUpdatereplaces Pods node by node, governed bymaxUnavailable. SettingmaxSurgeinstead starts the new Pod before stopping the old one on each node, which avoids a gap in coverage for agents that can briefly run twice.OnDeleteupdates a node only when its Pod is deleted manually. - Resource cost multiplies. A request of 100m CPU is 100m on every node. Keep agent requests small and set a memory limit, because a leaking agent on every node degrades the whole cluster at once.
- Priority. Give node-critical agents a high
priorityClassNameso the kubelet evicts them last under resource pressure.
Jobs
A Job runs Pods until a specified number complete successfully, retrying failures up to a limit. Where a Deployment keeps Pods running, a Job is done when its work is done.
| Field | Meaning | Default |
|---|---|---|
completions |
Successful Pods required for the Job to succeed | 1 |
parallelism |
Maximum Pods running at once | 1 |
completionMode |
NonIndexed, or Indexed: each Pod gets a completion index (0 to completions−1) in the JOB_COMPLETION_INDEX env var and a stable hostname |
NonIndexed |
backoffLimit |
Pod failures tolerated before the Job fails, with exponential back-off between retries | 6 |
backoffLimitPerIndex |
Retry budget per index in Indexed Jobs, so one bad shard cannot consume the whole budget (stable since v1.33) | unset |
podFailurePolicy |
Rules on exit codes and Pod conditions: FailJob, FailIndex, Ignore, or Count (stable since v1.31) |
unset |
successPolicy |
Declare success early, for example when index 0 (a leader) succeeds (stable since v1.33) | all completions |
podReplacementPolicy |
Failed: start a replacement only once the old Pod has fully terminated, avoiding two copies at once (stable since v1.34) |
TerminatingOrFailed |
activeDeadlineSeconds |
Wall-clock limit for the whole Job | none |
ttlSecondsAfterFinished |
Delete the finished Job and its Pods after this many seconds | none (kept forever) |
suspend |
Pause the Job (no new Pods); used by batch queueing systems such as Kueue | false |
An Indexed Job that processes 100 shards, ten at a time, fails fast on a known bad exit code, and does not count node disruptions against its retry budget:
apiVersion: batch/v1
kind: Job
metadata:
name: reindex-shards
spec:
completions: 100
parallelism: 10
completionMode: Indexed
backoffLimitPerIndex: 2 # each shard may fail twice
maxFailedIndexes: 5 # give up if more than 5 shards fail
ttlSecondsAfterFinished: 86400
podFailurePolicy:
rules:
- action: FailIndex # exit 42 = bad input; don't retry this shard
onExitCodes:
containerName: worker
operator: In
values: [42]
- action: Ignore # preemption, drain, eviction: retry for free
onPodConditions:
- type: DisruptionTarget
template:
spec:
restartPolicy: Never # required by podFailurePolicy
containers:
- name: worker
image: registry.example.com/reindexer:3.2
args: ["--shard=$(JOB_COMPLETION_INDEX)"]
resources:
requests: {cpu: "1", memory: 2Gi}
limits: {memory: 2Gi}
restartPolicy: OnFailure restarts failed containers in place instead of creating new Pods; it is simpler, but the Pod-level policies above require Never. For large-scale batch and ML training, projects such as Kueue (quota and queueing), JobSet (groups of related Jobs, e.g. leader plus workers), and the Kubeflow Trainer build on the Job API rather than replacing it.
CronJobs
A CronJob creates a Job on a cron schedule. The controller is simple, but most production incidents with CronJobs come from its concurrency and missed-run semantics.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "30 2 * * *" # 02:30 every day
timeZone: "Europe/London" # IANA zone; stable since v1.27
concurrencyPolicy: Forbid # skip a run if the previous is still going
startingDeadlineSeconds: 600 # a run more than 10 min late is skipped
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: registry.example.com/pg-backup:1.8
| Field | Options and behaviour |
|---|---|
concurrencyPolicy |
Allow (default): runs may overlap. Forbid: skip the new run while one is active. Replace: kill the running Job and start the new one |
startingDeadlineSeconds |
How late a run may start (after controller downtime, say). Without it, a controller that has missed more than 100 schedules stops scheduling and logs an error |
timeZone |
Interpret schedule in an IANA time zone instead of the controller’s local zone. Runs at times skipped or repeated by daylight-saving changes may be skipped or doubled; schedule around 01:00–03:00 local time with care |
suspend |
Stop creating new Jobs without deleting the CronJob |
CronJobs guarantee at least one Job per schedule “most of the time”: in rare cases a run can be created twice or not at all. Make scheduled work idempotent.
Sidecar Containers
A sidecar is a helper container that runs alongside the main container for the Pod’s whole life: a log shipper, a service-mesh proxy, a secrets refresher. Before native support, sidecars were ordinary containers, which caused well-known bugs: a Job never completed because its proxy sidecar never exited, and the main container started before its proxy was ready.
Native sidecars (stable since v1.33) are declared as init containers with restartPolicy: Always:
spec:
initContainers:
- name: migrate # ordinary init container: runs to completion first
image: registry.example.com/app:4.1
command: ["./migrate"]
- name: log-shipper # native sidecar
image: fluent/fluent-bit:5.1
restartPolicy: Always
startupProbe: # main containers wait until this passes
httpGet: {path: /api/v1/health, port: 2020}
containers:
- name: app
image: registry.example.com/app:4.1
sequenceDiagram
participant I as init: migrate
participant S as sidecar: log-shipper
participant A as container: app
I->>I: run to completion
Note over S: starts after earlier init containers
S->>S: startup probe passes
Note over A: main containers start only now
A->>A: runs, exits (Job) or is stopped
Note over S: sidecar stopped after main containers exit
Native sidecars start in order before the main containers, are restarted independently if they crash, do not block Job completion, and are terminated after the main containers during shutdown, so logs and proxied connections drain cleanly. Patterns built on sidecars (adapters, ambassadors, init-time configuration) are covered in Operations.
Storage at a Glance
Container filesystems are ephemeral: when a container restarts, anything it wrote to its own filesystem is gone. Volumes attach storage to a Pod with a defined lifetime.
| Volume type | Lifetime | Use for |
|---|---|---|
emptyDir |
The Pod (survives container restarts) | Scratch space, caches, sharing files between containers; medium: Memory for tmpfs |
configMap, secret, downwardAPI, projected |
The Pod; content updated from the API | Configuration files, credentials, Pod metadata, bound service-account tokens |
image |
The Pod; read-only | Mounting an OCI image or artifact (model weights, static data) without baking it into the app image (stable since v1.36) |
| Generic ephemeral volume | The Pod; provisioned through a StorageClass | Large per-Pod scratch space that the node disk cannot provide |
persistentVolumeClaim |
Independent of any Pod | Durable data: databases, uploads, queues |
hostPath |
The node | Node agents only; a security risk for ordinary workloads |
Persistent Volumes
Durable storage uses three objects. A PersistentVolumeClaim (PVC) is a namespaced request (“20 GiB, ReadWriteOnce, class fast-ssd”). A StorageClass tells a CSI driver how to create a matching PersistentVolume (PV) on demand. The PVC binds to the PV, and the Pod mounts the PVC by name.
flowchart LR
Pod[Pod] -->|mounts by name| PVC["PVC<br/>20Gi RWO fast-ssd"]
PVC -->|storageClassName| SC["StorageClass<br/>fast-ssd"]
SC -->|provisions via| CSI[CSI driver]
CSI -->|creates| PV[("PersistentVolume<br/>cloud disk")]
PVC -.-|bound| PV
| Access mode | Meaning | Backed by |
|---|---|---|
| ReadWriteOnce (RWO) | Read-write from one node | Block storage (EBS, Persistent Disk, Azure Disk) |
| ReadWriteOncePod (RWOP) | Read-write from exactly one Pod | CSI block storage |
| ReadOnlyMany (ROX) | Read-only from many nodes | File storage, cloned volumes |
| ReadWriteMany (RWX) | Read-write from many nodes | File storage (EFS, Azure Files, Filestore, NFS, CephFS) |
| CSI drivers | |
|---|---|
| AWS | EBS CSI (ebs.csi.aws.com), EFS CSI, FSx CSI |
| Google Cloud | Persistent Disk CSI (pd.csi.storage.gke.io), Filestore CSI |
| Azure | Azure Disk CSI (disk.csi.azure.com), Azure Files CSI |
| Self-managed | Rook-Ceph, Longhorn, OpenEBS, vendor arrays |
Troubleshooting Claims
| Symptom | Usual cause | Check |
|---|---|---|
PVC Pending |
No default StorageClass; class name typo; RWX requested from block storage; WaitForFirstConsumer waiting for a Pod (normal) |
kubectl describe pvc, events from the provisioner |
Pod ContainerCreating, FailedAttachVolume |
Volume still attached to a failed node; zone mismatch; node’s volume-attachment limit reached | kubectl describe pod, kubectl get volumeattachment |
PVC stuck Terminating |
kubernetes.io/pvc-protection finalizer: a Pod still uses it |
Find and delete the Pod using the claim |
PV Released |
PVC deleted under a Retain policy; data kept on purpose |
Reuse by clearing spec.claimRef, or delete deliberately |
Reclaim policies, volume expansion, VolumeAttributesClass, snapshots, and backup strategy are covered in Stateful Workloads & Persistence.
Scaling Workloads
Kubernetes scales at three layers, and production clusters usually combine them: more Pods, bigger Pods, and more nodes to put them on.
flowchart LR
M[Metrics:<br/>CPU, memory, custom, events] --> HPA["HPA / KEDA<br/>changes replica count"]
M --> VPA["VPA<br/>changes Pod requests"]
HPA --> P["Pending Pods<br/>(no node fits)"]
VPA --> P
P --> NA["Cluster Autoscaler / Karpenter<br/>adds nodes"]
NA -.->|consolidation removes under-used nodes| NA
Horizontal: More Replicas
The HorizontalPodAutoscaler adjusts replicas on a Deployment or StatefulSet from observed metrics; its algorithm, metric types, and stabilisation behaviour are covered in Health & Resource Management. Two recent changes: a per-HPA tolerance (behavior.scaleUp.tolerance, behavior.scaleDown.tolerance) replaces the fixed cluster-wide 10% dead band (stable in v1.37), and minReplicas: 0 for HPAs driven by custom or external metrics is beta and enabled by default in v1.37. KEDA extends horizontal scaling to event sources such as queue depth, Kafka lag, or cron windows, and has long supported scale-to-zero.
Vertical: Bigger Pods
The Vertical Pod Autoscaler (an add-on from the Kubernetes autoscaler project, not part of the core) watches actual usage and recommends or applies CPU and memory requests.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: InPlaceOrRecreate # resize running Pods; evict only if that fails
resourcePolicy:
containerPolicies:
- containerName: "*"
controlledResources: ["memory"] # leave CPU to the HPA
minAllowed: {memory: 128Mi}
maxAllowed: {memory: 4Gi}
updateMode |
Behaviour |
|---|---|
Off |
Recommendations only, in the VPA’s status. A safe way to right-size by hand |
Initial |
Apply requests only when Pods are created |
Recreate |
Evict Pods whose requests differ significantly from the recommendation (respecting PodDisruptionBudgets) |
InPlaceOrRecreate |
Resize running Pods in place, falling back to eviction (generally available since VPA 1.6) |
Auto |
Deprecated; currently equivalent to Recreate. Use an explicit mode |
Do not let an HPA and a VPA act on the same resource: the VPA raises CPU requests, which lowers utilisation, which makes the HPA scale in. A common split is HPA on CPU (or a custom metric) and VPA on memory.
In-Place Pod Resize
Since v1.35, CPU and memory requests and limits of a running Pod can be changed without recreating it (in-place Pod vertical scaling, stable). The change is made through the Pod’s resize subresource, and each container’s resizePolicy states whether a change needs a container restart:
spec:
containers:
- name: api
image: registry.example.com/api:2.7
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # CPU changes apply live via cgroups
- resourceName: memory
restartPolicy: RestartContainer # e.g. a JVM that sizes its heap at start
resources:
requests: {cpu: 500m, memory: 1Gi}
limits: {memory: 1Gi}
kubectl patch pod api-7d9c5 --subresource resize --patch \
'{"spec":{"containers":[{"name":"api","resources":{"requests":{"cpu":"1"}}}]}}'
kubectl get pod api-7d9c5 -o jsonpath='{.status.containerStatuses[0].resources}'
If the node lacks room, the resize is reported as PodResizePending (deferred or infeasible) rather than evicting anything. A resize cannot change the Pod’s QoS class. Editing a Deployment’s template still triggers a normal rollout; in-place resize acts on individual Pods and is mainly used by the VPA and by platform tooling. Pod-level resources (a budget shared by all containers in the Pod) is beta and enabled by default since v1.34.
Nodes: Cluster Autoscaler and Karpenter
When Pods are Pending because no node has room, a node autoscaler adds capacity; when nodes are under-used, it drains and removes them.
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| Model | Resizes predefined node groups (ASGs, managed node pools) | Launches individual instances chosen to fit the pending Pods |
| Instance choice | Fixed per node group | Any type allowed by a NodePool (families, sizes, Spot or On-Demand, architectures) |
| Scale-down | Removes nodes below a utilisation threshold | Consolidation: replaces or removes nodes to pack workloads more cheaply |
| Availability | All major clouds | AWS (open source, v1 API since 2024); Azure as AKS node auto-provisioning; other providers through community integrations |
Both honour PodDisruptionBudgets when removing nodes, so every production workload that must stay available during node churn needs a PDB.
Workload Security
Pod Security Standards
The built-in Pod Security Admission controller (stable since v1.25, replacing the removed PodSecurityPolicy) enforces three profiles per namespace:
| Profile | Allows | Intended for |
|---|---|---|
| Privileged | Everything | System components, node agents, CNI and CSI plugins |
| Baseline | Blocks known privilege escalations: host namespaces, privileged containers, hostPath, added dangerous capabilities |
Most applications with little change |
| Restricted | Baseline plus: must run as non-root, drop all capabilities, no privilege escalation, seccomp RuntimeDefault or Localhost |
Security-sensitive and multi-tenant workloads |
Each namespace can set a profile in three modes: enforce rejects violating Pods, audit records them in the audit log, and warn returns a warning to the client. Rolling out warn and audit first reveals offenders without breaking deployments.
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.37 # pin the policy version
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
A container spec that passes the restricted profile:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: registry.example.com/api:2.7
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # not required by the profile, but recommended
capabilities:
drop: ["ALL"]
For stronger isolation, user namespaces (hostUsers: false in the Pod spec, stable since v1.36) map the container’s root user to an unprivileged user on the host, so a container escape does not yield host root. Policy beyond the three profiles, such as required labels, allowed registries, or signed images, is enforced with ValidatingAdmissionPolicy (CEL expressions, built in since v1.30) or with Kyverno or OPA Gatekeeper.
Service Accounts and API Access
Every Pod runs as a ServiceAccount. Give each application its own, grant it the narrowest RBAC Role it needs, and turn off the API token for Pods that never call the Kubernetes API:
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: payments
automountServiceAccountToken: false
Mounted tokens are short-lived, audience-bound, and rotated by the kubelet; long-lived Secret-based tokens are no longer created automatically. Cloud workload-identity features (EKS Pod Identity and IRSA, GKE Workload Identity Federation, AKS Workload Identity) exchange these tokens for cloud credentials, so no static cloud keys need to be stored in Secrets. Roles, bindings, and least-privilege design are covered in Networking & Configuration.
Secrets are not encrypted by default. Base64 in a Secret manifest is an encoding, not encryption, and etcd stores Secrets in plain text unless encryption at rest is configured (KMS v2, stable since v1.29, or the managed equivalent enabled on the cluster). Restrict get/list on Secrets through RBAC, and consider an external store (a cloud secrets manager via the External Secrets Operator or the Secrets Store CSI driver) for high-value credentials.
Observability: Understanding Application Health
Kubernetes acts on three health signals that each container can expose:
| Probe | Question | On failure |
|---|---|---|
| Startup | Has the application finished starting? | Other probes wait; after failureThreshold the container is restarted |
| Liveness | Is the process stuck beyond recovery? | Container restarted |
| Readiness | Can it serve traffic right now? | Removed from Service endpoints; not restarted |
startupProbe:
httpGet: {path: /healthz, port: 8080}
periodSeconds: 5
failureThreshold: 30 # allow up to 150 s to boot
livenessProbe:
httpGet: {path: /healthz, port: 8080}
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet: {path: /ready, port: 8080}
periodSeconds: 5
Keep liveness checks shallow (the process itself, not its database) so that a slow dependency does not trigger restarts across every replica; express dependency health through readiness instead. Probe mechanics and design guidance are covered in Health & Resource Management.
Beyond probes, workloads expose metrics for Prometheus. With the Prometheus Operator, a ServiceMonitor declares what to scrape:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: api
labels:
release: prometheus # must match the Prometheus instance's selector
spec:
selector:
matchLabels:
app: api
endpoints:
- port: metrics # a named port on the Service
interval: 30s
For request-driven services, alert on the RED signals (request rate, error rate, duration); for resources such as nodes and queues, on the USE signals (utilisation, saturation, errors). Metrics pipelines, logging, tracing with OpenTelemetry, and alerting are covered in Operations.
Common Pitfalls
| Pitfall | Consequence | Remedy |
|---|---|---|
| No memory limit | One leaking Pod pushes the node into memory pressure and evictions | Set memory limits (commonly equal to requests) |
| Tight CPU limits on latency-sensitive services | Throttling even when the node is idle | Set CPU requests; use CPU limits sparingly |
| CronJob work that is not idempotent | Duplicate or missed runs corrupt data | Design for at-least-once; use concurrencyPolicy: Forbid |
Jobs without ttlSecondsAfterFinished |
Thousands of finished Jobs and Pods clutter the API | Set a TTL or history limits |
| Proxy sidecar as a regular container in a Job | Job never completes | Use a native sidecar (restartPolicy: Always init container) |
| HPA and VPA on the same resource | Autoscalers fight | Split resources, or use VPA in Off mode for recommendations |
| No PodDisruptionBudget | Node drains and autoscaler consolidation take down all replicas | Define a PDB for every service that must stay available |
| Assuming Secrets are encrypted | Credentials readable from etcd or backups | Enable encryption at rest; restrict RBAC; use an external store |
| Deep liveness checks | Dependency outage restarts every replica | Liveness checks the process only; readiness checks dependencies |
See Also
- Fundamentals: Pods, ReplicaSets, Deployments, and cluster architecture
- Networking & Configuration: Services, Ingress, network policies, ConfigMaps, Secrets, and RBAC
- Health & Resource Management: probes, requests and limits, QoS, scheduling, and the HPA
- Stateful Workloads & Persistence: storage in depth, StatefulSets, and backup
- Operations: kubectl, Helm, observability, and troubleshooting
- Advanced Topics: CRDs, operators, service mesh, and GitOps
- Docker Storage & Security: volumes and hardening at the container level
- AWS Compute: EKS and managed node groups