Jobs and CronJobs
A container that does a piece of work and exits does not belong in a Deployment. A Deployment restarts it forever, which is where most CrashLoopBackOff reports on a perfectly healthy container come from.
A one-off task
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
spec:
backoffLimit: 3
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: registry.itsh.dev/<your-slug>/myapp:1.0.0
command: ["./migrate"]
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256MirestartPolicy must be OnFailure or Never; Always is not valid for a Job. backoffLimit caps the retries before the Job is marked failed.
The securityContext is the same block as on any other pod, and it is there for the same reason: without it Kubernetes prints four warnings on apply. See Versions and defaults.
Scheduled work
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 3 * * *"
timeZone: "Europe/Berlin"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: registry.itsh.dev/<your-slug>/myapp:1.0.0
command: ["./report"]
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
capabilities:
drop: ["ALL"]Standard five-field cron, unlike the six-field form the database operator uses for scheduled backups.
Three fields worth setting deliberately:
timeZone. Without it the schedule runs in UTC, which is not what you meant if you wrote0 3 * * *thinking of local time.concurrencyPolicy: Forbidskips a run if the previous one is still going. The default lets them overlap, which is rarely what a report or a cleanup wants.- The history limits. Completed pods are kept so you can read their logs. Keep a few; keeping many clutters the namespace.
What they cost
A Job is billed for the requests of its pod, for as long as that pod is actually running, measured by the minute. A Job that runs for five minutes costs five minutes, not an hour.
Completed pods kept as history are not billed. They are not running, so they cost nothing, and keeping a few for their logs is free.
This makes scheduled work the cheapest thing on the platform, and it is why a CronJob is a better answer than a service that sleeps: a CronJob costs nothing between runs without any of the scale-to-zero caveats. See Scale-to-zero.
When a Job fails
kubectl get jobs shows completions. The pods stay around, so:
kubectl get pods --selector=job-name=migrate
kubectl logs job/migrateA CronJob that never seems to run is usually a schedule in the wrong timezone, or concurrencyPolicy: Forbid skipping runs because the previous one never finished.
What's next
- Secrets and configuration for passing in credentials
- Billing and quotas for how running time is counted