12

DaemonSet, Job & CronJob

Run it in the public monorepo

This course is built around the public HelixWorks Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.

Source: terraform/main.tf

resource "kind_cluster" "helixworks" {
  name            = var.cluster_name
  node_image      = var.kind_node_image
  wait_for_ready  = true
  kubeconfig_path = local.kubeconfig_path

  kind_config {
    kind        = "Cluster"
    api_version = "kind.x-k8s.io/v1alpha4"

Code to reality

Declared intent
Create the local Kubernetes substrate from versioned infrastructure code.
Interpreter
Terraform builds a dependency graph and the Kind provider calls the Docker and Kind APIs.
Software effect
Terraform records a cluster resource and Kind writes a kubeconfig for its Kubernetes API.
Hardware effect
Docker starts containers that consume the laptop resources used as control-plane and worker nodes.
Observable evidence
terraform state show and kubectl get nodes must agree that the declared cluster is ready.

Key terms

TermMeaning
DaemonSetRuns one pod per (matching) node
JobRuns pods until successful completion
CronJobCreates Jobs on a schedule
completions/parallelismHow many / how concurrently a Job runs
concurrencyPolicyWhether CronJob runs may overlap
ScheduleCron expression driving a CronJob

Problem & solution

Deployments assume long-running, horizontally scalable services, but some workloads don't fit: an agent that must run on every node, a one-off batch task that should finish and stop, or a recurring scheduled job.

Solution: Pick the right controller for the lifecycle: DaemonSet (one pod per node), Job (run to completion), CronJob (scheduled).

The analogy

Think of three different port jobs. A safety inspector stationed on every berth watches each dock all day, and the moment a brand-new berth opens the port posts one there too. A one-off cargo haul is dispatched, runs until the load is delivered, then stops for good. A scheduled nightly haul goes out automatically every night on a fixed timetable. These map to a DaemonSet that runs one pod per node, a Job that runs to completion, and a CronJob that creates Jobs on a schedule.

Where this fits in the cluster

These are controllers that live in the control plane and decide which pods land on which nodes. DaemonSet works at the node layer, Job/CronJob at the pod layer.

Three special workload controllers

Beyond Deployments, Kubernetes ships controllers for non-standard workloads: node agents, one-off batch tasks, and scheduled tasks.

   DaemonSet  -> one pod on EVERY node           (agents)
   Job        -> run to completion, then stop     (batch task)
   CronJob    -> Job on a schedule                (cron)

Where they sit in the controller family

Every controller manages pods, but each answers a different question: how many, where, and for how long?

   CONTROLLER     "how many / where"            "how long does a pod live"
   -----------    --------------------------     --------------------------
   Deployment     N replicas, scheduler picks    forever (restarted on exit)
   DaemonSet      exactly 1 per matching node     forever (restarted on exit)
   Job            N completions, anywhere         until task succeeds, then stop
   CronJob        spawns a Job each tick          each Job stops when done

Rule of thumb: long-running = Deployment/DaemonSet, runs-then-exits = Job/CronJob. The restart expectation is the key difference.

DaemonSet — one pod per node

A DaemonSet guarantees a copy of a pod runs on every (matching) node, and automatically places one on any new node that joins the cluster.

Use for node-level agents: log collectors (fluentd), monitoring (node-exporter), CNI plugins, kube-proxy.

Graph legend — node-exporter as a real per-node DaemonSet:

Graph nodeMaps toWhat it does
node-exporter pod (node1-3)a DaemonSet pod, image quay.io/prometheus/node-exporter:v1.8.2One metrics agent per node, scraped by Prometheus
New node joinsthe DaemonSet controller reconcilingAuto-schedules a node-exporter onto any new node

DaemonSet vs Deployment scaling

A Deployment's count is something you choose; a DaemonSet's count is dictated by the cluster size. You never set replicas on a DaemonSet.

Graph legend — DaemonSet count is dictated by node count, not replicas:

Graph nodeMaps toWhat it does
Deployment replicas 2Deployment.spec.replicasA fixed number you choose; may leave nodes empty
DaemonSet, no replicas fieldDaemonSet (no replicas)Always one node-exporter per matching node

Targeting a subset of nodes

Pair a nodeSelector (Day 15) or taints/tolerations (Day 14) with a DaemonSet to run the agent only where it belongs (e.g. only GPU nodes).

Graph legend — scoping a DaemonSet to a node subset:

Graph nodeMaps toWhat it does
dcgm-exporter poda DaemonSet pod, image nvcr.io/nvidia/k8s/dcgm-exporterNVIDIA GPU metrics agent, GPU nodes only
nodeSelector gpu=trueDaemonSet.spec.template.spec.nodeSelectorRestricts the agent to nodes labeled gpu=true
cpu node - skippeda node missing the labelGets no DaemonSet pod

The manifest below is a minimal DaemonSet; notice it has no replicas field, because the controller decides the count by running one pod on every matching node.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
spec:
  selector:
    matchLabels: { app: node-exporter }
  template:
    metadata:
      labels: { app: node-exporter }
    spec:
      containers:
        - name: node-exporter
          image: quay.io/prometheus/node-exporter:v1.8.2
          args: ['--path.rootfs=/host']
          ports:
            - { name: metrics, containerPort: 9100 }

Job — run once to completion

A Job runs a pod until its task finishes successfully, then stops — unlike a Deployment, it is not meant to stay running.

Graph legend — a Job runs once to completion:

Graph nodeMaps toWhat it does
pg_dump poda Job pod, image postgres:16Runs pg_dump once, then exits
Completed, exit 0the pod's terminal phaseSuccess; the Job is not restarted
  • Not restarted on success.
  • Retries on failure up to backoffLimit.

completions x parallelism

These two knobs control batch shape: how many successes you need, and how many pods run at once toward that goal.

Graph legend — a parallel image-resize batch Job:

Graph nodeMaps toWhat it does
completions 4, parallelism 2Job.spec.completions / .parallelismNeed 4 successes, run 2 at a time
resize-worker pod1-4the Job's podsEach processes one shard of the batch
Done when 4 pods have exited 0the Job reaching completionsMarks the Job complete

completions: 1, parallelism: 1 runs a single one-shot task (the default).

backoffLimit and retries

On failure a Job retries with exponential backoff until backoffLimit is hit, then it's marked Failed and stops trying.

Graph legend — retries with exponential backoff up to backoffLimit:

Graph nodeMaps toWhat it does
pg_dump FAILa Job pod exiting non-zeroA failed backup attempt
wait 10s / 20sthe kubelet's exponential backoffGrowing delay between retries
backoffLimit reached - Job FailedJob.spec.backoffLimitStops retrying and marks the Job Failed

activeDeadlineSeconds: hard cap on total runtime regardless of retries.

apiVersion: batch/v1
kind: Job
metadata:
  name: db-backup
spec:
  completions: 1          # how many successful pods needed
  parallelism: 1          # how many run at once
  backoffLimit: 4         # retries before marking failed
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: backup
          image: postgres:16
          command: ['sh','-c','pg_dump "$DB_URL" > /tmp/dump.sql && echo done']
          env:
            - name: DB_URL
              valueFrom:
                secretKeyRef: { name: db-credentials, key: url }

CronJob — Jobs on a schedule

A CronJob creates a Job automatically on a recurring cron schedule — the Kubernetes equivalent of a crontab entry.

This manifest defines a CronJob that backs up at 02:00 every night; the extra fields control overlap and how much run history Kubernetes keeps around.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup
spec:
  schedule: "0 2 * * *"            # daily at 02:00
  concurrencyPolicy: Forbid        # don't start a new run if one is still going
  startingDeadlineSeconds: 120     # skip a missed run if >2 min late
  successfulJobsHistoryLimit: 3    # keep last 3 successful Jobs
  failedJobsHistoryLimit: 1        # keep last 1 failed Job
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: busybox
              command: ['sh','-c','echo backing up...']

concurrencyPolicy — what if a run overlaps?

If a Job is still running when the next tick fires, this policy decides what happens. Critical for long backups or jobs that must not double-run.

   Allow   (default): start the new run anyway -> two Jobs run at once
   Forbid           : skip the new run while the old one is still going
   Replace          : kill the old run, start the new one

Object chain: CronJob -> Job -> Pod

A CronJob doesn't run containers directly — it creates Jobs, which create Pods. History limits control how many old Jobs are kept around.

Graph legend — the CronJob to Job to Pod chain for nightly pg_dump:

Graph nodeMaps toWhat it does
CronJob db-backupkind: CronJob with schedule: "0 2 * * *"Creates a Job on each tick
Job db-backup-28...a Job created per tickOwns one backup run
Pod ... runs pg_dumpthe Job's postgres:16 podExecutes the actual pg_dump
old Jobs prunedsuccessfulJobsHistoryLimit / failedJobsHistoryLimitCaps how many old Jobs are retained

Commands

Everyday commands for inspecting and triggering these controllers.

kubectl get daemonset -A
kubectl get jobs
kubectl get cronjobs                               # shows SCHEDULE, LAST SCHEDULE
kubectl logs job/pi                                # logs from a Job's pod
kubectl create job manual --from=cronjob/backup    # trigger a CronJob now
kubectl get pods --selector=job-name=pi            # find a Job's pods
kubectl delete job pi                              # also deletes its pods

Reading the STATUS / COMPLETIONS columns

The list output tells you batch progress at a glance.

   $ kubectl get jobs
   NAME   COMPLETIONS   DURATION   AGE
   pi     1/1           5s         1m      <- done: 1 of 1 succeeded
   etl    2/4           30s        30s     <- in progress: 2 of 4 done

   $ kubectl get pods
   pi-xxxxx   0/1   Completed   0   1m      <- batch pods end "Completed", not Running

When to use which

A quick decision guide for picking the right controller for the job.

   Need it on every node?           -> DaemonSet
   One-off batch task?              -> Job
   Recurring scheduled task?        -> CronJob
   Long-running stateless service?  -> Deployment (Day 8)

End-to-end example: nightly DB backup

A realistic pipeline: a DaemonSet log agent on every node, plus a CronJob that backs up a database every night and a way to trigger it on demand.

Graph legend — a real per-node log agent plus a nightly Postgres backup:

Graph nodeMaps toWhat it does
DaemonSet fluent-bita DaemonSet, image cr.fluentbit.io/fluent/fluent-bit:3.0Ships logs from every node, always running
CronJob db-backupkind: CronJob (0 2 * * *)Spawns the nightly backup Job
Job db-backup-28...a Job per tickOwns one backup run
Pod runs pg_dumpthe Job's postgres:16 podDumps the database to disk
# 1) node-level log agent on EVERY node
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: fluent-bit }
spec:
  selector: { matchLabels: { app: fluent-bit } }
  template:
    metadata: { labels: { app: fluent-bit } }
    spec:
      containers:
        - name: fluent-bit
          image: cr.fluentbit.io/fluent/fluent-bit:3.0
          ports: [{ name: metrics, containerPort: 2020 }]
---
# 2) scheduled backup that must not overlap
apiVersion: batch/v1
kind: CronJob
metadata: { name: db-backup }
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 600
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: postgres:16
              command: ['sh','-c','pg_dump "$DB_URL" > /tmp/dump.sql && echo done']
              env:
                - name: DB_URL
                  valueFrom:
                    secretKeyRef:
                      name: db-credentials
                      key: url
kubectl apply -f backup.yaml
kubectl get daemonset fluent-bit -o wide       # one pod per node
kubectl get cronjob db-backup                  # SCHEDULE + LAST SCHEDULE
kubectl create job test-now --from=cronjob/db-backup   # trigger immediately
kubectl logs job/test-now                      # watch the backup run to Completed

Common pitfalls

The mistakes that surprise people first.

   * restartPolicy: Always on a Job  -> rejected; Jobs need Never or OnFailure
   * Pod stuck Completed, not Running -> that's correct for Jobs; don't "fix" it
   * CronJob never fires              -> check timezone, schedule syntax, and
                                         startingDeadlineSeconds skipping runs
   * Overlapping CronJob runs         -> set concurrencyPolicy: Forbid/Replace
   * DaemonSet skips a node           -> node has a taint; add a toleration (Day 14)
   * Job retries forever             -> set backoffLimit + activeDeadlineSeconds

End-to-end flow

Each controller drives a different pod lifecycle: scheduled by a CronJob, one per node by a DaemonSet, or run-to-completion by a Job.

Graph legend — each controller's distinct pod lifecycle:

Graph nodeMaps toWhat it does
CronJob db-backup tick 0200CronJob.spec.scheduleFires on schedule and creates a Job
Creates Pod running pg_dumpthe Job's postgres:16 podRuns the backup to completion
DaemonSet fluent-bita DaemonSetKeeps one log agent on every node
Job resize-workera standalone JobRuns a batch task until it succeeds, then stops

Key takeaways

  • DaemonSet = exactly one pod per (matching) node, auto-scales with nodes; no replicas field.
  • Job runs to completion (completions, parallelism, backoffLimit); use restartPolicy: Never or OnFailure.
  • CronJob spawns Jobs on a cron schedule; control overlap with concurrencyPolicy and clutter with history limits.
  • Batch pods end as Completed, not Running — that's expected.

Checklist

  • [ ] Deployed a DaemonSet and saw one pod per node
  • [ ] Scoped a DaemonSet to a subset of nodes with a nodeSelector
  • [ ] Ran a Job to completion and read its logs
  • [ ] Tuned a Job with completions + parallelism
  • [ ] Created a CronJob and manually triggered it with --from
  • [ ] Set a concurrencyPolicy and watched history limits prune old Jobs