Skip to content
Menu

Virtualization8 min read

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.

  1. Deployment

    Desired state
    image, replicas, strategy

  2. ReplicaSet

    Ensures N replicas
    one per revision

  3. Pod

    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
console
# 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-0 is always postgres-0, even if it moves to a different node

Stable Persistent Storage

  • volumeClaimTemplates create a unique PVC per pod
  • PVC name: <volume-name>-<statefulset>-<ordinal>
  • PVCs persist even if the pod is deleted and rescheduled
  • When postgres-0 is rescheduled to a new node, it reattaches to the same PVC

Ordered Operations

  1. pod-0

    Created first
    Deleted last

  2. pod-1

    Created second
    After pod-0 is Ready

  3. pod-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: Parallel to create/delete all at once

Use cases: PostgreSQL (primary/replica topology), Kafka (broker IDs), etcd (peer discovery), ZooKeeper (ensemble membership), Redis Sentinel, Elasticsearch

console
# 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-2

DaemonSet

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.nodeName directly (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
DaemonSet: fluentd
Node 1 fluentd pod
Node 2 fluentd pod
Node 3 fluentd pod
New Node auto-scheduled

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.

console
# Simple node selector — pod only runs on SSD nodes
spec:
  nodeSelector:
    disk: ssd
    region: us-east-1

Node Affinity (Expressive)

More powerful than nodeSelector — supports operators (In, NotIn, Exists, DoesNotExist, Gt, Lt) and soft/hard rules.

Hard Requirement (required)
yaml
affinity:
  nodeAffinity:
    requiredDuringScheduling
    IgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: zone
              operator: In
              values:
                - us-east-1a
                - us-east-1b

Pod MUST land on nodes in us-east-1a or us-east-1b. If no matching node exists, the pod stays Pending.

Soft Preference (preferred)
yaml
affinity:
  nodeAffinity:
    preferredDuringScheduling
    IgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
            - key: gpu-type
              operator: In
              values:
                - a100

Scheduler 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)
yaml
podAffinity:
  requiredDuringScheduling
  IgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: redis-cache
      topologyKey: kubernetes.io/hostname

Pod 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)
yaml
podAntiAffinity:
  requiredDuringScheduling
  IgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: web-app
      topologyKey: topology.kubernetes.io/zone

Taints 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
Taint (on Node)
console
console

          $ kubectl taint nodes gpu-node-1 \

            nvidia.com/gpu=true:NoSchedule
        

Repels all pods that don't explicitly tolerate this taint.

Toleration (on Pod)
yaml
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.

yaml
topologySpreadConstraints:
  - maxSkew: 1                            # max difference between domains
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule       # or ScheduleAnyway
    labelSelector:
      matchLabels:
        app: web-app

Example: 6 replicas across 3 zones with maxSkew=1

Zone A
2 pods
Zone B
2 pods
Zone C
2 pods

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.

console
# 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.

  1. Unscheduled Pod

    Scheduler detects pod
    with no nodeName

  2. Filter

    Eliminate nodes:
    resources, selectors,
    taints, affinity

  3. Score

    Rank nodes 0-100:
    least requested,
    balanced, topology

  4. Bind

    Assign pod to
    highest-scoring node

  5. Kubelet

    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.

Solidnines — solidnines.com