Skip to main content

Kubernetes Workloads — Jobs, CronJobs, DaemonSets, and StatefulSets

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

Deployments are the workhorse of Kubernetes, but not every workload is a long-running web server. You need to run a database migration once, process a queue of images every night, collect logs from every node, or deploy a database cluster with stable identities. Kubernetes has a dedicated workload type for each of these patterns.

Workload Selection Guide

Before diving into each type, here is when to use what:

Workload TypePatternExample
DeploymentStateless, long-running, replaceableWeb servers, APIs, microservices
JobRun-to-completion, one-time taskDatabase migration, data export, ML training
CronJobScheduled recurring taskNightly reports, log cleanup, backups
DaemonSetOne pod per nodeLog collectors, monitoring agents, CNI plugins
StatefulSetStateful, ordered, stable identityDatabases, Kafka, ZooKeeper, Elasticsearch

Jobs — Run to Completion

A Job creates one or more pods and ensures they run to successful completion. Unlike Deployments, Jobs are not meant to run forever — they finish and stop.

Basic Job

apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
namespace: production
spec:
backoffLimit: 3 # Retry up to 3 times on failure
activeDeadlineSeconds: 600 # Kill the job after 10 minutes
ttlSecondsAfterFinished: 300 # Auto-delete 5 minutes after completion
template:
spec:
restartPolicy: Never # Jobs require Never or OnFailure
containers:
- name: migrate
image: my-app:1.0
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
# Run and monitor
kubectl apply -f db-migration.yaml
kubectl get jobs
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 45s 2m

# Check pod logs
kubectl logs job/db-migration

# List pods created by the job
kubectl get pods -l job-name=db-migration

Parallel Jobs

Process multiple items concurrently with completions and parallelism:

apiVersion: batch/v1
kind: Job
metadata:
name: image-processor
spec:
completions: 10 # Total number of pods that must complete
parallelism: 3 # Run 3 pods at a time
backoffLimit: 5
template:
spec:
restartPolicy: OnFailure
containers:
- name: processor
image: image-processor:1.0
command: ["./process-batch.sh"]
# Watch parallel execution
kubectl get pods -l job-name=image-processor -w
# NAME READY STATUS RESTARTS AGE
# image-processor-abc12 1/1 Running 0 10s
# image-processor-def34 1/1 Running 0 10s
# image-processor-ghi56 1/1 Running 0 10s
# image-processor-abc12 0/1 Completed 0 35s
# image-processor-jkl78 1/1 Running 0 2s # Next one starts

Indexed Jobs (Kubernetes 1.21+)

Each pod gets an index, useful for processing partitioned data:

apiVersion: batch/v1
kind: Job
metadata:
name: indexed-processor
spec:
completions: 5
parallelism: 5
completionMode: Indexed # Each pod gets JOB_COMPLETION_INDEX env var
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: worker:1.0
command: ["./process.sh"]
# JOB_COMPLETION_INDEX is automatically set (0, 1, 2, 3, 4)

CronJobs — Scheduled Tasks

CronJobs create Jobs on a repeating schedule using standard cron syntax.

apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
namespace: production
spec:
schedule: "0 2 * * *" # Every day at 2:00 AM UTC
timeZone: "America/New_York" # Timezone support (K8s 1.27+)
concurrencyPolicy: Forbid # Do not start new if previous is still running
successfulJobsHistoryLimit: 3 # Keep last 3 successful job records
failedJobsHistoryLimit: 3 # Keep last 3 failed job records
startingDeadlineSeconds: 300 # Skip if more than 5 minutes late
suspend: false # Set to true to pause scheduling
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600 # Kill if running longer than 1 hour
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: backup-tool:1.0
command: ["./backup.sh"]
volumeMounts:
- name: backup-storage
mountPath: /backups
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: backup-pvc

Cron Schedule Reference

ExpressionSchedule
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 2 * * *Daily at 2 AM
0 0 * * 0Every Sunday at midnight
0 0 1 * *First day of every month

Concurrency Policies

PolicyBehavior
Allow (default)Multiple jobs can run simultaneously
ForbidSkip new job if previous is still running
ReplaceCancel running job and start new one
# List CronJobs and their schedules
kubectl get cronjobs -n production
# NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE
# nightly-backup 0 2 * * * False 0 6h

# Manually trigger a CronJob (create a Job from it)
kubectl create job manual-backup --from=cronjob/nightly-backup -n production

# Suspend a CronJob temporarily
kubectl patch cronjob nightly-backup -n production -p '{"spec":{"suspend":true}}'

DaemonSets — One Pod Per Node

A DaemonSet ensures that a copy of a pod runs on every node (or a subset of nodes). When nodes are added to the cluster, DaemonSet pods are automatically scheduled on them. When nodes are removed, the pods are garbage collected.

Log Collector DaemonSet

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentbit
namespace: logging
spec:
selector:
matchLabels:
app: fluentbit
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # Update one node at a time
template:
metadata:
labels:
app: fluentbit
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule # Run on control plane nodes too
containers:
- name: fluentbit
image: fluent/fluent-bit:2.2
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: containers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: containers
hostPath:
path: /var/lib/docker/containers

Running on Specific Nodes Only

Use nodeSelector or affinity to target a subset of nodes:

spec:
template:
spec:
nodeSelector:
node-type: gpu # Only GPU nodes
# OR use affinity for more complex rules:
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.io/os
# operator: In
# values: ["linux"]
# Check DaemonSet status
kubectl get daemonset -n logging
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR
# fluentbit 5 5 5 5 5 <none>

# Verify it runs on every node
kubectl get pods -n logging -o wide -l app=fluentbit
# NAME READY STATUS NODE
# fluentbit-abc12 1/1 Running node-1
# fluentbit-def34 1/1 Running node-2
# fluentbit-ghi56 1/1 Running node-3

StatefulSets — Stable Identity and Ordered Deployment

StatefulSets are designed for applications that need one or more of:

  • Stable network identity — each pod gets a predictable hostname (pod-0, pod-1, pod-2)
  • Stable persistent storage — each pod gets its own PVC that follows it across rescheduling
  • Ordered deployment and scaling — pods are created in order (0, 1, 2) and deleted in reverse (2, 1, 0)

PostgreSQL StatefulSet

apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: production
spec:
clusterIP: None # Headless service — required for StatefulSets
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: postgres-headless # Must match the headless service
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeClaimTemplates: # Each pod gets its own PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
# StatefulSet pods get predictable names
kubectl get pods -n production -l app=postgres
# NAME READY STATUS AGE
# postgres-0 1/1 Running 5m # Created first
# postgres-1 1/1 Running 4m # Created second
# postgres-2 1/1 Running 3m # Created third

# Each pod has its own PVC
kubectl get pvc -n production
# NAME STATUS VOLUME CAPACITY
# data-postgres-0 Bound pvc-abc123 50Gi
# data-postgres-1 Bound pvc-def456 50Gi
# data-postgres-2 Bound pvc-ghi789 50Gi

# DNS resolution: each pod has a stable DNS name
# postgres-0.postgres-headless.production.svc.cluster.local
# postgres-1.postgres-headless.production.svc.cluster.local
# postgres-2.postgres-headless.production.svc.cluster.local

# If postgres-1 is rescheduled, it keeps the same name and PVC
kubectl delete pod postgres-1 -n production
kubectl get pods -n production -l app=postgres -w
# postgres-1 recreated with the same PVC attached

StatefulSet Update Strategies

StrategyBehavior
RollingUpdate (default)Updates pods in reverse order (N-1 → 0), one at a time
OnDeleteOnly updates a pod when you manually delete it
partitionOnly updates pods with index >= partition value (canary rollouts)
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # Only update pod-2 and above, leave 0 and 1 alone

Next, we will cover Horizontal and Vertical Pod Autoscalers — how to automatically scale your workloads based on CPU, memory, and custom metrics to handle traffic spikes without manual intervention.