Chapter 36 of 47 · More workload types

Jobs and CronJobs: work that finishes

A Deployment keeps an app running forever. But some work has an end: send this week's digest email, delete old rejected comments, resize uploaded images. A Job runs a task until it has succeeded, then stops. A CronJob creates such a Job on a schedule, like an alarm clock.

Why not a Deployment?

Deployment

"Keep 3 copies running, always." If the process exits, even successfully, Kubernetes starts it again. Perfect for posts and comments, useless for a cleanup script: it would run in a loop forever.

Job

"Run this until it succeeds once." Exit code 0 means done: the Pod stays Completed and nothing restarts. A non-zero exit means "try again", up to a limit.

CronJob, Job and PodA CronJob creates a new Job at each scheduled time. Each Job creates a Pod that runs the task and ends as Completed. CronJob"every night at 3" Mon 03:00Tue 03:00 Job cleanup-monrun once to success Job cleanup-tuerun once to success Podruns the script → Completed Podruns the script → Completed
Same chain as Deployment → ReplicaSet → Pod (chapter 12), but for work that ends.

A Job, line by line

The blog app collects rejected comments. This Job deletes the ones older than 30 days. Click the parts:

apiVersion: batch/v1
kind: Job
metadata:
  name: cleanup-comments
spec:
  backoffLimit: 3
  activeDeadlineSeconds: 600
  ttlSecondsAfterFinished: 86400
  template:
    spec:
      containers:
        - name: cleanup
          image: micro-comments:1.0
          command: ["node", "scripts/delete-old-rejected.js"]
      restartPolicy: Never

template

A normal Pod template, like in a Deployment. Here: reuse the comments image and run a script instead of the server. No replicas, no Service: nobody calls this Pod.

When the task fails

  1. $ kubectl apply -f cleanup-job.yaml
    $ kubectl get pods
    NAME                     READY   STATUS    RESTARTS
    cleanup-comments-7xk2p   1/1     Running   0

    The Job creates a Pod, which runs the script.

  2. NAME                     READY   STATUS      RESTARTS
    cleanup-comments-7xk2p   0/1     Completed   0
    
    $ kubectl get job
    NAME               STATUS     COMPLETIONS   DURATION
    cleanup-comments   Complete   1/1           14s

    Exit code 0. The Pod stays Completed, so you can still read its logs. Nothing restarts.

  1. cleanup-comments-7xk2p   0/1   Error   0

    The script cannot reach the database and exits with 1.

  2. cleanup-comments-7xk2p   0/1   Error   0
    cleanup-comments-q9z3b   0/1   Error   0
    cleanup-comments-h2v8n   0/1   Error   0

    With restartPolicy: Never, each retry is a new Pod, after a growing pause.

  3. $ kubectl get job
    NAME               STATUS   COMPLETIONS
    cleanup-comments   Failed   0/1
    $ kubectl describe job cleanup-comments
      Warning  BackoffLimitExceeded  Job has reached the specified backoff limit

    After backoffLimit failures the Job gives up. Read kubectl logs cleanup-comments-7xk2p to see why.

Many Pods at once

Two numbers control how much work a Job does and how fast:

FieldMeansDefault
completionsHow many Pods must succeed before the Job is done1
parallelismHow many Pods may run at the same time1
  1. running:  [pod 1] [pod 2]
    done:     0 of 6

    Two workers start, never more than parallelism.

  2. running:  [pod 3] [pod 2]
    done:     1 of 6   ✓ pod 1

    As soon as one succeeds, the next one starts.

  3. running:  [pod 5] [pod 6]
    done:     4 of 6   ✓ 1 ✓ 2 ✓ 3 ✓ 4

    Like a checkout with two tills open: the queue moves two at a time.

  4. running:  —
    done:     6 of 6   → Job Complete

    Six successes reached. About three times faster than one worker.

Three ways to split work

spec:
  # completions and parallelism both default to 1
  template: ...

One Pod, one task: a nightly cleanup, a database migration, sending the weekly digest. The most common Job by far.

CronJob: a Job on a schedule

A CronJob is a Job template plus an alarm clock. At each scheduled time it creates a fresh Job:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: weekly-digest
spec:
  schedule: "0 9 * * 1"
  timeZone: "Asia/Kolkata"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  suspend: false
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          containers:
            - name: digest
              image: micro-query:1.0
              command: ["node", "scripts/send-weekly-digest.js"]
          restartPolicy: OnFailure

schedule

When to run, in cron format: five fields. 0 9 * * 1 = minute 0, hour 9, any day, any month, Monday. Try it in the explainer below.

Reading a schedule

Five fields: minute hour day-of-month month day-of-week. A * means "every". Type a schedule or use the buttons:

Shortcuts such as @daily, @hourly and @weekly also work in a CronJob.

When runs overlap

The digest usually takes 2 minutes. One Monday the mail server is slow and it takes 2 hours, while a CronJob runs every hour. What should happen at the next tick?

09:00  run A ███████████████████████
10:00        run B ███████████████   ← both run at the same time

The default. Fine when runs are independent; dangerous when two copies would send the same emails twice.

Safe to run twice

Kubernetes promises that a task runs at least once, not exactly once. A Pod can be killed halfway and retried, and in rare cases a scheduled run can start twice. So write tasks that do no harm if repeated (idempotent):

✗ Risky

// sends again on every retry
for (const user of users) await sendDigest(user);

✓ Safe

for (const user of users) {
  if (await alreadySent(user, thisWeek)) continue;
  await sendDigest(user);
  await markSent(user, thisWeek);
}

With kubectl

# a one-off Job without YAML
$ kubectl create job hello --image=busybox:1.36 -- echo "hi"

# test a CronJob right now instead of waiting until Monday
$ kubectl create job digest-test --from=cronjob/weekly-digest

$ kubectl get cronjobs
NAME            SCHEDULE    TIMEZONE       SUSPEND   ACTIVE   LAST SCHEDULE
weekly-digest   0 9 * * 1   Asia/Kolkata   False     0        5d

$ kubectl get jobs --watch
$ kubectl logs job/digest-test

# pause and resume the schedule
$ kubectl patch cronjob weekly-digest -p '{"spec":{"suspend":true}}'

Recap

Quick check

You put your cleanup script in a Deployment. What happens?
Show answer

It runs, exits successfully, and the Deployment immediately starts it again, forever, because a Deployment's job is to keep containers running. Use a Job or CronJob.

A CronJob with schedule: "0 9 * * *" sends emails at 14:30 in India. Why?
Show answer

No timeZone is set, so 09:00 means UTC, which is 14:30 in India. Add timeZone: "Asia/Kolkata".

Jobs and CronJobs: common questions

What is the difference between a Job and a CronJob?

A Job runs Pods until a task succeeds. A CronJob creates a new Job on a schedule, such as every night at 3.

Why does my CronJob run at the wrong hour?

Without timeZone the schedule uses the cluster time zone, usually UTC. Set timeZone, for example Asia/Kolkata.