Jobs and CronJobs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A Deployment keeps a Pod running forever and restarts it
whenever it exits, which is exactly wrong for work that is supposed to finish: a database migration, a report
export, a one-off batch import. A Job (batch/v1) is the controller for that shape of work instead — it creates
one or more Pods, tracks how many completed successfully, and stops once enough of them have, retrying failures
according to its own policy rather than forever. A CronJob wraps a Job template with a schedule, creating a fresh
Job at each tick. This page covers both controllers as they stand in Kubernetes 1.37, and rebuilds, one by one, the
three Job patterns that Hightower, Burns & Beda’s Kubernetes: Up and Running (1st ed., 2017 sampler), ch. 3 (book
ch. 10), "Jobs", walked through against the Bookshelf notifier — a small worker that sends catalog-update
notifications — correcting the two kubectl invocations that no longer work as printed.
The Job spec
apiVersion: batch/v1
kind: Job
metadata:
name: notifier-once
namespace: bookshelf
labels:
app.kubernetes.io/name: notifier
app.kubernetes.io/part-of: bookshelf
spec:
completions: 1
parallelism: 1
backoffLimit: 3
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: notifier
spec:
restartPolicy: OnFailure
containers:
- name: notifier
image: ghcr.io/example/bookshelf-notifier:1.0.0
args: ["--mode=once"]
Source: Kubernetes Docs — Job.
| Field | Meaning |
|---|---|
|
How many Pods must complete successfully before the Job itself is considered |
|
How many Pods the Job controller tries to keep running concurrently while completions remain outstanding. Never
exceeds |
|
How many Pod failures the Job tolerates, in total, before giving up and marking itself |
|
A wall-clock ceiling on the whole Job, independent of |
|
How long a |
|
GA since Kubernetes 1.31; requires the Pod template’s |
|
GA since Kubernetes 1.24 (default remains |
|
GA since Kubernetes 1.33, only meaningful with |
|
GA since Kubernetes 1.33, only meaningful with |
|
GA since Kubernetes 1.24. |
Source: Kubernetes Docs — Job.
A Job whose process hangs instead of exiting never finishes, so the Job controller cannot react. Give the Job’s
container a liveness probe so the kubelet restarts a hung process, and bound the whole run with
activeDeadlineSeconds.
restartPolicy: OnFailure vs Never
A Job’s Pod template may only set restartPolicy to OnFailure or Never — Always, the Deployment default, is
rejected by the API server, because a Pod that always restarts can never reach a terminal state for the Job
controller to count. Both retry a failure, but at a different layer, and Book 2’s ch. 3 (book ch. 10) treats the
choice as a small but consequential one:
restartPolicy |
Behaviour |
|---|---|
|
The kubelet restarts the failed container in place, inside the same Pod, the same way it would under
Pods' ordinary |
|
The container is left |
Book 2’s sampler default was OnFailure for the in-place-retry case, reserving Never for a Job whose failed
attempts are worth keeping around individually — both patterns below use OnFailure, since neither needs to
inspect an old failed attempt once a new Pod has already retried it.
Source: Kubernetes Docs — Job.
Auto-generated labels and manualSelector
As kubectl Essentials already introduced, the Job controller
stamps every Pod it creates with two labels of its own: job-name (the owning Job’s name) and controller-uid (a
value unique to this particular Job object, so a second Job later created with the same name never accidentally
adopts Pods left over from the first one). spec.selector is filled in automatically from these at creation time
and, unlike a ReplicaSet's selector, is immutable once
set. manualSelector: true opts out of the auto-generated selector in favour of a hand-written one, which is
possible but discouraged — it is easy to write a selector that a second Job also matches, and nothing in
Bookshelf needs it.
The three Book 2 patterns, rebuilt for the notifier
Book 2’s ch. 3 (book ch. 10) worked through three shapes of Job in turn: a one-shot Job, a fixed-completion
parallel Job, and a work-queue Job consuming from a shared queue. All three are rebuilt below against the
Bookshelf notifier, which sends a notification per catalog update.
One-shot
The simplest shape: completions: 1, parallelism: 1, exactly the manifest shown at the top of this page. One
Pod runs, and the Job is Complete the moment it exits 0.
kubectl apply -f notifier-once.yaml
kubectl wait --for=condition=complete --timeout=120s job/notifier-once
kubectl logs job/notifier-once
job.batch/notifier-once condition met
sent 42 notifications in 3.1s
Book 2’s own way of creating this same one-shot Job was kubectl run notifier --image=ghcr.io/example/bookshelf-notifier:1.0.0 --restart=OnFailure — --mode=once — as kubectl Essentials covers in full, kubectl run’s Job/Deployment
generators were removed in Kubernetes 1.18, and `kubectl run today always creates a bare Pod regardless of
--restart. The direct replacement is an imperative kubectl create job, or the declarative manifest above:
kubectl create job notifier-once --image=ghcr.io/example/bookshelf-notifier:1.0.0 -- --mode=once
Source: Kubernetes Docs — Job and Kubernetes Docs — Running Jobs.
Fixed-completion parallel (Indexed)
Book 2’s second pattern split a fixed amount of work into completions equal-sized pieces, run up to
parallelism at a time. On a 1.37 cluster the same pattern is written with completionMode: Indexed, so each Pod
can read its own index and process only its slice instead of every Pod polling a shared queue for work:
apiVersion: batch/v1
kind: Job
metadata:
name: notifier-batch
namespace: bookshelf
labels:
app.kubernetes.io/name: notifier
app.kubernetes.io/part-of: bookshelf
spec:
completions: 5
parallelism: 3
completionMode: Indexed
backoffLimitPerIndex: 2
maxFailedIndexes: 1
template:
metadata:
labels:
app.kubernetes.io/name: notifier
spec:
restartPolicy: OnFailure
containers:
- name: notifier
image: ghcr.io/example/bookshelf-notifier:1.0.0
args: ["--mode=batch", "--shard-count=5"]
env:
- name: SHARD_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
Five completions, three Pods running at once: each Pod’s SHARD_INDEX (0-4) picks a disjoint slice of the
catalog to notify about, with no coordination needed between the Pods — the split is entirely static, decided by
completions and the shard count baked into the image’s arguments, unlike the work-queue pattern below where the
split happens at runtime against shared state.
kubectl create -f notifier-batch.yaml
kubectl get pods -l job-name=notifier-batch \
-o custom-columns=NAME:.metadata.name,INDEX:.metadata.annotations.batch\\.kubernetes\\.io/job-completion-index,STATUS:.status.phase
NAME INDEX STATUS
notifier-batch-0-x7f2 0 Succeeded
notifier-batch-1-k9m4 1 Succeeded
notifier-batch-2-q2p8 2 Running
notifier-batch-3-z4n1 3 Succeeded
notifier-batch-4-b8t6 4 Succeeded
backoffLimitPerIndex: 2 and maxFailedIndexes: 1 together mean any single shard may retry up to twice on its own
before that shard is given up on, and a persistently broken shard 2 does not consume retry budget that shards 0, 1,
3 and 4 never needed — they keep running to completion. Once every index has finished, the Job is still marked
Failed if any index failed; maxFailedIndexes only ends it early, terminating the remaining Pods as soon as
more than one shard has exhausted its own retries.
Work queue (Redis list plus a consumer Job)
Book 2’s third pattern had producers push work items onto a queue Service and a pool of consumer Pods — created as
a single Job with parallelism set but no fixed completions — pop items off it until the queue is empty, each
Pod deciding for itself when there is no more work left rather than being handed a pre-assigned share. Rebuilt here
with Redis as the queue, matching the Bookshelf app’s existing Redis instance
(Persistent Volumes and Storage Classes covers
how it is deployed):
apiVersion: batch/v1
kind: Job
metadata:
name: notifier-queue-consumer
namespace: bookshelf
labels:
app.kubernetes.io/name: notifier
app.kubernetes.io/part-of: bookshelf
spec:
parallelism: 4
backoffLimit: 6
template:
metadata:
labels:
app.kubernetes.io/name: notifier
spec:
restartPolicy: OnFailure
containers:
- name: notifier
image: ghcr.io/example/bookshelf-notifier:1.0.0
args: ["--mode=queue-worker"]
env:
- name: REDIS_ADDR
value: "redis.bookshelf.svc.cluster.local:6379"
- name: REDIS_QUEUE_KEY
value: "notifier:pending"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: bookshelf-redis
key: password
Each of the four consumer Pods loops on a blocking BRPOP notifier:pending 5 against Redis: whichever Pod happens
to pop an item processes it, and a Pod that gets an empty result after its own timeout budget simply exits 0,
counting as a success. Because completions is left unset, no new Pods are created once any consumer has
exited successfully, and the Job is Complete once at least one Pod has succeeded and all Pods have terminated — there is no fixed total to reach, only "every consumer eventually found the queue empty and stopped". Producers (the catalog-api and reviews-api Deployments, on a
catalog update) push with LPUSH notifier:pending <payload>, entirely decoupled from how many consumer Pods happen
to be running at that moment.
kubectl exec deploy/catalog-api -- redis-cli -h redis.bookshelf.svc.cluster.local LPUSH notifier:pending '{"book_id":"42"}'
kubectl create -f notifier-queue-consumer.yaml
kubectl logs -l job-name=notifier-queue-consumer -f --prefix
[pod/notifier-queue-consumer-4kxqp/notifier] popped {"book_id":"42"}, notified 3 subscribers
[pod/notifier-queue-consumer-9mzr2/notifier] queue empty after 5s, exiting
[pod/notifier-queue-consumer-p7wfl/notifier] queue empty after 5s, exiting
[pod/notifier-queue-consumer-t2blq/notifier] queue empty after 5s, exiting
Book 2’s own queue was a small HTTP work-queue Deployment reached through kubectl port-forward from the consumer
Job’s Pods; the shape above is functionally identical — a shared, external store that decouples producers from
however many consumers currently exist — swapped for Redis, since Bookshelf already runs one, and using a
cluster-internal Service address instead of port-forward, which is a debugging tool
(kubectl Essentials) rather than how Pods reach each other in
production. The REDIS_PASSWORD above is never a literal in the manifest: it comes from a Secret created out of
band, e.g. kubectl create secret generic bookshelf-redis --from-literal=password="$(openssl rand -base64 24)",
exactly as ConfigMaps describes for Secret values in general.
Finally, once the consumer Job has run, Book 2’s way of checking on it was kubectl get jobs -a (--show-all) to
see completed Jobs alongside active ones — as kubectl Essentials
already covers, that flag was removed once kubectl get stopped hiding completed and failed objects by default, so
a plain kubectl get jobs on 1.37 already shows notifier-queue-consumer once it finishes.
📊 The work-queue pattern
CronJobs
A CronJob (batch/v1, GA since Kubernetes 1.21, promoted from the batch/v1beta1 API Book 2’s era predates) does
not itself run anything — it holds a jobTemplate and a schedule, and at each tick the CronJob controller creates
a brand-new Job object from that template, which then runs exactly like any Job above.
For in-process scheduling inside a Spring Boot service, and how its cron syntax differs from a CronJob’s, see
Standard Scheduling vs Quartz.
apiVersion: batch/v1
kind: CronJob
metadata:
name: notifier-nightly-digest
namespace: bookshelf
labels:
app.kubernetes.io/name: notifier
app.kubernetes.io/part-of: bookshelf
spec:
schedule: "0 2 * * *"
timeZone: "Europe/Madrid"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
template:
metadata:
labels:
app.kubernetes.io/name: notifier
spec:
restartPolicy: OnFailure
containers:
- name: notifier
image: ghcr.io/example/bookshelf-notifier:1.0.0
args: ["--mode=digest"]
Source: Kubernetes Docs — CronJob.
| Field | Meaning |
|---|---|
|
A standard five-field crontab expression (minute, hour, day-of-month, month, day-of-week), plus the
non-standard |
|
GA since Kubernetes 1.27. An IANA time zone name ( |
|
|
|
How late a missed tick may still be started. If the CronJob controller was down, or the cluster was overloaded, for longer than this many seconds past a scheduled time, that tick is skipped entirely rather than started late; left unset, a missed tick may still be started arbitrarily late (subject to a 100-missed-tick safety cap). |
|
How many completed Job objects (and their Pods) are kept around after a run, split by outcome, purely for
|
Source: Kubernetes Docs — CronJob.
A CronJob’s jobTemplate.spec accepts every field the Job spec above does, including completionMode: Indexed or
a work-queue-style parallelism, so the nightly digest above could equally well be the fixed-completion or
work-queue pattern instead of the one-shot shown — CronJob only adds the schedule on top, it does not change what
kind of Job runs at each tick.
Running a Job from a CronJob’s template on demand, without waiting for the next scheduled tick, is a common enough
debugging need that kubectl has a dedicated verb for it rather than requiring the whole manifest to be copied by
hand:
kubectl create job notifier-digest-manual --from=cronjob/notifier-nightly-digest
kubectl get jobs -l app.kubernetes.io/name=notifier
job.batch/notifier-digest-manual created
NAME COMPLETIONS DURATION AGE
notifier-digest-manual 1/1 4s 12s
Source: Kubernetes Docs — Running Jobs.
References
-
Kubernetes Docs — Coarse Parallel Processing Using a Work Queue
-
Kubernetes Docs — Fine Parallel Processing Using a Work Queue
-
Kubernetes Docs — Indexed Job for Parallel Processing with Static Work Assignment
-
Kubernetes Docs — Handling Pod and Container Failures with Pod Failure Policy
-
Hightower, K., Burns, B., Beda, J. (2017). Kubernetes: Up and Running (1st ed. sampler). O’Reilly. Ch. 3 (book ch. 10), "Jobs".