Everything covered so far assumes a Pod that should keep running indefinitely, restarted whenever it stops. A database migration, a report generation script, or a nightly cleanup task needs the exact opposite guarantee — run to completion, exactly once, then stop.
3 min read
A Deployment's whole design (from lesson 1's controller reconciliation loop) assumes a Pod that should always be running — if it exits, the controller restarts it, treating that exit as a failure to correct. A database migration script that's supposed to run once and exit successfully would just get restarted forever by a Deployment, since "exited" and "failed" look identical to a controller built for long-running processes.
A Job creates a Pod, waits for it to complete successfully, and then considers the work done — no restart on a clean exit, retries on failure up to a configured limit:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
containers:
- name: migrate
image: my-app:v1.2.0
command: ["python", "manage.py", "migrate"]
restartPolicy: Never
backoffLimit: 3backoffLimit caps how many times a failing Pod gets retried before the
Job itself is marked failed — a genuine safety net distinguishing "flaky,
worth retrying a few times" from "consistently broken, stop trying and
alert someone" instead of retrying forever.
A Job can also run multiple Pods to process a batch of work in parallel, rather than just one Pod once:
spec:
completions: 10 # need 10 total successful completions
parallelism: 3 # up to 3 running at onceThis fits a queue of independent work items (processing 10 uploaded files, say) where the order between items doesn't matter and several can safely run at the same time — a distinct use case from the single database migration example above, but the same underlying Job object handles both.
A CronJob creates a new Job automatically on a schedule, using the same syntax as Unix cron:
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-cleanup
spec:
schedule: "0 2 * * *" # 2:00 AM every day
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: my-app:v1.2.0
command: ["python", "manage.py", "cleanup_old_records"]
restartPolicy: NeverEach scheduled firing creates a fresh Job, which creates a fresh Pod, following exactly the run-to-completion semantics already described — a CronJob is really just "a Job, created repeatedly on a schedule," not a separate execution model.
If a scheduled task takes longer than the interval between runs (a cleanup job scheduled hourly that occasionally takes 90 minutes), multiple instances can end up running concurrently unless explicitly prevented:
spec:
concurrencyPolicy: Forbid # skip a new run if the previous one hasn't finishedForbid skips starting a new run while one is still in progress;
Replace cancels the still-running one and starts fresh instead. Leaving
this unset (Allow, the default) permits overlapping runs, which is
exactly correct for genuinely independent, order-agnostic tasks and
exactly wrong for a task that assumes only one copy of itself is ever
running against the same data at once.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is a Deployment the wrong object for running a one-off database migration script?
2. What does a Job's backoffLimit control?
3. Why does a CronJob's concurrencyPolicy setting matter for a task that takes longer than its scheduled interval?
Kubernetes