Workloads & Scheduling
Deployments, StatefulSets, DaemonSets — and how pods land on nodes
Deployment
A Deployment manages ReplicaSets, which manage Pods. It's the standard way to run stateless applications. The key value is automated rolling updates with rollback capability.
Deployment
Desired state
image, replicas, strategyReplicaSet
Ensures N replicas
one per revisionPod
Actual running
container(s)
Rolling Update Strategy
When you update a Deployment (e.g., change the image tag), it creates a new ReplicaSet, scales it up gradually while scaling the old one down.
RollingUpdate Parameters
maxSurge— how many extra pods can exist during the update (above desired count). Can be absolute number or percentage. Default: 25%.maxUnavailable— how many pods can be down during the update. Default: 25%.- With 10 replicas,
maxSurge=3, maxUnavailable=2: at any point there are 8-13 pods total, at least 8 available.
Recreate Strategy
strategy.type: Recreate- Kills all old pods first, then creates new ones
- Brief downtime — use for workloads that cannot run two versions simultaneously
- Use cases: schema-incompatible DB migrations, exclusive file locks, license servers
# Deployment YAML with key fields highlighted
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
spec:
replicas: 3 # desired pod count
revisionHistoryLimit: 10 # old ReplicaSets to keep (for rollback)
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 1 extra pod during update
maxUnavailable: 0 # no downtime — always 3 available
selector:
matchLabels:
app: web-app
template: # pod template
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: myapp:v2.1.0 # change this to trigger rollout
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"Tip
Rollback: Kubernetes keeps old ReplicaSets (controlled by revisionHistoryLimit) so you can roll back instantly. kubectl rollout undo deployment/web-app --to-revision=3 switches to the ReplicaSet from revision 3. Check history with kubectl rollout history deployment/web-app.
StatefulSet
StatefulSets are for stateful applications that need stable identities and persistent storage — databases, message queues, distributed systems like etcd itself.
What StatefulSet Guarantees
Stable Network Identity
- Pods are named deterministically:
<statefulset>-0,<statefulset>-1,<statefulset>-2 - Requires a Headless Service (
clusterIP: None) - Each pod gets a DNS record:
pod-0.svc-name.namespace.svc.cluster.local - Identity persists across rescheduling —
postgres-0is alwayspostgres-0, even if it moves to a different node
Stable Persistent Storage
volumeClaimTemplatescreate a unique PVC per pod- PVC name:
<volume-name>-<statefulset>-<ordinal> - PVCs persist even if the pod is deleted and rescheduled
- When
postgres-0is rescheduled to a new node, it reattaches to the same PVC
Ordered Operations
pod-0
Created first
Deleted lastpod-1
Created second
After pod-0 is Readypod-2
Created third
After pod-1 is Ready
- Scale up: pods created in order 0 → 1 → 2, each waiting for the previous to be Running and Ready
- Scale down: pods deleted in reverse order 2 → 1 → 0
- Opt out: set
podManagementPolicy: Parallelto create/delete all at once
Use cases: PostgreSQL (primary/replica topology), Kafka (broker IDs), etcd (peer discovery), ZooKeeper (ensemble membership), Redis Sentinel, Elasticsearch
# StatefulSet YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless # required headless service
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # one PVC per pod
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: "50Gi"
# Creates PVCs: data-postgres-0, data-postgres-1, data-postgres-2DaemonSet
A DaemonSet ensures that exactly one pod runs on every node (or every node matching a selector). When nodes are added, the DaemonSet automatically creates pods on them. When nodes are removed, those pods are garbage collected.
Characteristics
- One pod per node (or per matching node via
nodeSelector/ affinity) - Pods automatically created on new nodes
- Pods automatically deleted when nodes are removed
- DaemonSet controller sets
.spec.nodeNamedirectly (bypasses scheduler by default)
Common Use Cases
- Log collectors — Fluentd, Fluent Bit, Filebeat
- Monitoring agents — Prometheus node-exporter, Datadog agent
- Network plugins — Calico, Cilium, Weave (CNI agents)
- Storage daemons — Ceph, GlusterFS, Longhorn agents
- Security agents — Falco, Sysdig
Job & CronJob
Job — Run to Completion
Jobs create pods that run a task and then stop. Unlike Deployments, completed pods are not restarted.
| Field | Description |
|---|---|
completions |
How many successful completions needed (default: 1) |
parallelism |
How many pods run concurrently (default: 1) |
backoffLimit |
Max retries on failure (default: 6) |
activeDeadlineSeconds |
Max total runtime before forced termination |
ttlSecondsAfterFinished |
Auto-cleanup completed Job after N seconds |
CronJob — Scheduled Jobs
CronJobs create Jobs on a cron schedule. Each CronJob invocation creates a new Job object.
| Field | Description |
|---|---|
schedule |
Cron expression ("0 2 * * *" = 2 AM daily) |
concurrencyPolicy |
Allow (concurrent), Forbid (skip if running), Replace (kill old, start new) |
successfulJobsHistoryLimit |
How many completed Jobs to keep (default: 3) |
failedJobsHistoryLimit |
How many failed Jobs to keep (default: 1) |
startingDeadlineSeconds |
Max delay before a missed schedule is considered failed |
Scheduling Mechanisms
Kubernetes provides multiple mechanisms to control where pods are placed. They range from simple label matching to complex topology-aware distribution.
nodeSelector (Simple)
The simplest scheduling constraint — pod runs only on nodes with matching labels.
# Simple node selector — pod only runs on SSD nodes
spec:
nodeSelector:
disk: ssd
region: us-east-1Node Affinity (Expressive)
More powerful than nodeSelector — supports operators (In, NotIn, Exists, DoesNotExist, Gt, Lt) and soft/hard rules.
affinity:
nodeAffinity:
requiredDuringScheduling
IgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: zone
operator: In
values:
- us-east-1a
- us-east-1bPod MUST land on nodes in us-east-1a or us-east-1b. If no matching node exists, the pod stays Pending.
affinity:
nodeAffinity:
preferredDuringScheduling
IgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: gpu-type
operator: In
values:
- a100Scheduler PREFERS A100 GPU nodes (weight 80/100) but will schedule elsewhere if needed.
Pod Affinity / Anti-Affinity
Schedule pods relative to other pods, not just node labels. topologyKey defines what "near" means.
Pod Affinity — Co-locate
"Schedule me near pods with label X"
- Web pods near cache pods on the same node (
topologyKey: kubernetes.io/hostname) - App pods in the same zone as the database (
topologyKey: topology.kubernetes.io/zone)
podAffinity:
requiredDuringScheduling
IgnoredDuringExecution:
- labelSelector:
matchLabels:
app: redis-cache
topologyKey: kubernetes.io/hostnamePod Anti-Affinity — Spread
"Don't schedule me near pods with label X"
- Spread replicas across different nodes (HA within a zone)
- Spread replicas across different zones (HA across zones)
podAntiAffinity:
requiredDuringScheduling
IgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web-app
topologyKey: topology.kubernetes.io/zoneTaints and Tolerations
Taints are applied to nodes to repel pods. Tolerations are applied to pods to allow scheduling on tainted nodes. This is the inverse of affinity — instead of attracting pods, taints repel them.
| Effect | Behavior | Use Case |
|---|---|---|
NoSchedule |
New pods without toleration will NOT be scheduled here. Existing pods are unaffected. | Dedicated GPU nodes, control plane nodes |
PreferNoSchedule |
Scheduler tries to avoid, but will place if no other option. | Soft reservation for specific workloads |
NoExecute |
Existing pods without toleration are evicted. New pods won't schedule. | Node draining, marking nodes as unhealthy |
$ kubectl taint nodes gpu-node-1 \
nvidia.com/gpu=true:NoSchedule
Repels all pods that don't explicitly tolerate this taint.
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"This pod CAN be scheduled on the tainted GPU node.
Note
Built-in taints: Kubernetes automatically taints control plane nodes with node-role.kubernetes.io/control-plane:NoSchedule so user workloads don't run there. The node controller adds node.kubernetes.io/not-ready:NoExecute to unresponsive nodes to evict pods.
Topology Spread Constraints
Distribute pods evenly across failure domains (zones, nodes, racks). More granular than pod anti-affinity.
topologySpreadConstraints:
- maxSkew: 1 # max difference between domains
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # or ScheduleAnyway
labelSelector:
matchLabels:
app: web-appExample: 6 replicas across 3 zones with maxSkew=1
maxSkew=1 means the difference between the most-populated and least-populated zone is at most 1. A distribution of 3/2/1 would be allowed; 4/1/1 would not.
PriorityClass and Preemption
Assign priority values to pods. Higher-priority pods can preempt (evict) lower-priority pods when cluster resources are scarce.
# Define a PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000 # higher number = higher priority
preemptionPolicy: PreemptLowerPriority # or Never
globalDefault: false
description: "For critical production workloads"
---
# Use in a pod spec
spec:
priorityClassName: high-priority| Built-in PriorityClass | Value | Use |
|---|---|---|
system-node-critical |
2000001000 | Node-essential pods (kubelet addons, kube-proxy) |
system-cluster-critical |
2000000000 | Cluster-essential pods (CoreDNS, metrics-server) |
Warning
Preemption risk: When a high-priority pod cannot be scheduled, the scheduler will evict lower-priority pods to make room. This can cascade — be careful with priority values. Set preemptionPolicy: Never if you want priority for queue ordering only (no eviction).
Scheduling Decision Flow
Putting it all together — here is the complete flow the scheduler follows for every unscheduled pod.
Unscheduled Pod
Scheduler detects pod
with no nodeNameFilter
Eliminate nodes:
resources, selectors,
taints, affinityScore
Rank nodes 0-100:
least requested,
balanced, topologyBind
Assign pod to
highest-scoring nodeKubelet
Picks up pod,
calls CRI, starts
containers
Filtering Plugins Applied (in order)
| Plugin | What It Checks |
|---|---|
NodeUnschedulable |
Is the node cordoned (spec.unschedulable: true)? |
NodeResourcesFit |
Does the node have enough CPU, memory, ephemeral storage? |
NodeName |
Does the pod request a specific node? |
NodePorts |
Are the requested host ports available? |
NodeAffinity |
Required node affinity rules satisfied? |
TaintToleration |
Does the pod tolerate all node taints? |
PodTopologySpread |
Would placing here violate topology spread constraints? |
InterPodAffinity |
Required pod affinity/anti-affinity rules satisfied? |
VolumeBinding |
Can required PVs be bound on this node? |
Tip
Debugging scheduling: If a pod is stuck in Pending, check kubectl describe pod <name> — the Events section shows exactly which filter eliminated all nodes. Common reasons: insufficient resources, unsatisfied node affinity, no nodes tolerating required taints.