Ravindra BagaleCourses & study guides मराठी

Chapter 60: Kubernetes Workload Controllers in Depth: ReplicaSet, Deployment, StatefulSet, DaemonSet, Jobs and CronJobs

60.7 CronJobs: schedule, concurrencyPolicy, history limits and time zones

Why: the problem

Suppose HDFC Bank must send a daily statement report at 6 AM India time, clear expired OTP records every 15 minutes, and back up a database every night at 2 AM. On a normal server you would write a crontab line. But in Kubernetes, pods move between nodes, and a crontab on one node dies with that node. A CronJob is cron for the whole cluster. At each scheduled time it creates a Job, and the Job creates pods. If a node dies, the next run simply lands on another node. The hard parts are not the schedule. They are: what if the last run is still going when the next one starts? What if the cluster was down at 6 AM? Which time zone is 6 AM? How many old Jobs do we keep? In this lesson we write CronJobs with every important field, run one by hand, stop overlaps with concurrencyPolicy, set the Asia/Kolkata time zone, and suspend a CronJob during maintenance. Then we break it with a bad schedule, overlapping runs and missed schedules.

What: every term in plain English

  • CronJob — A batch/v1 controller that creates a Job on a cron schedule.
  • schedule — Five fields: minute, hour, day of month, month, day of week. */15 * * * * means every 15 minutes.
  • timeZone — An IANA zone like Asia/Kolkata. Without it, the schedule uses the controller's zone, usually UTC.
  • concurrencyPolicy — Allow (default), Forbid (skip the new run if the old one is running), or Replace (kill the old one).
  • startingDeadlineSeconds — If a run is missed by more than this, skip it.
  • successfulJobsHistoryLimit — How many finished Jobs to keep. Default 3.
  • failedJobsHistoryLimit — How many failed Jobs to keep. Default 1.
  • suspend — true stops new runs; running Jobs continue.
  • jobTemplate — The full Job spec that each run copies.
  • lastScheduleTime — Status field showing when the CronJob last created a Job.
Write the schedule. Set timeZone. Pick a concurrencyPolicy. Add limits. Apply and trigger once by hand. Watch runs. Suspend for maintenance.
CronJob: schedule, then Job, then Pod

How: step by step

  1. Write the schedule. Test it with a cron helper; remember the five fields.
  2. Set timeZone. Asia/Kolkata for Indian business times.
  3. Pick a concurrencyPolicy. Forbid for backups and reports, Replace for 'latest wins' syncs.
  4. Add limits. backoffLimit, activeDeadlineSeconds and history limits.
  5. Apply and trigger once by hand. kubectl create job --from=cronjob/NAME.
  6. Watch runs. kubectl get cronjob, jobs and pods.
  7. Suspend for maintenance. Patch suspend: true, then false.

Example 1: Daily report at 6 AM India time

Goal. Daily report at 6 AM India time.

daily-report-cron.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
  namespace: shop
spec:
  schedule: "0 6 * * *"
  timeZone: "Asia/Kolkata"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 2
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 900
      template:
        metadata:
          labels:
            app: daily-report
        spec:
          restartPolicy: Never
          containers:
            - name: report
              image: busybox:1.36
              command: ["sh", "-c"]
              args:
                - echo "statement report for $(date +%F) started";
                  sleep 5;
                  echo "report sent"
              resources:
                requests: {cpu: 50m, memory: 32Mi}
                limits: {memory: 64Mi}

What each line does

  • schedule 0 6 * * * — Minute 0, hour 6, every day.
  • timeZone Asia/Kolkata — 6 AM IST, not 6 AM UTC (11:30 AM IST).
  • concurrencyPolicy: Forbid — If yesterday's run is somehow still running, skip today's.
  • startingDeadlineSeconds: 600 — If the controller was down at 6:00 and comes back by 6:10, it still runs; later, it skips.
  • History limits — Keep 3 good and 2 failed Jobs for logs.
  • jobTemplate — Everything from 60.6 applies inside.

Green output

$ kubectl apply -f daily-report-cron.yaml
cronjob.batch/daily-report created
$ kubectl get cronjob daily-report -n shop
NAME           SCHEDULE    TIMEZONE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
daily-report   0 6 * * *   Asia/Kolkata   False     0        <none>          5s
$ kubectl create job manual-report -n shop --from=cronjob/daily-report
job.batch/manual-report created
$ kubectl logs -n shop job/manual-report
statement report for 2026-10-25 started
report sent

Expected result. The CronJob waits for 6 AM IST. We did not wait: --from=cronjob ran the same template now. Always test this way before trusting a schedule.

Example 2: Every minute, with Forbid vs Allow overlaps

Goal. Every minute, with Forbid vs Allow overlaps.

otp-cleanup-cron.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: otp-cleanup
  namespace: shop
spec:
  schedule: "* * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: cleanup
              image: busybox:1.36
              command: ["sh", "-c", "echo cleanup start $(date +%T); sleep 90; echo cleanup end $(date +%T)"]

What each line does

  • schedule * * * * * — Every minute, the fastest cron allows.
  • sleep 90 — Each run takes longer than the gap, on purpose.
  • Forbid — The run at minute 2 is skipped because minute 1 is still active.
  • Allow (default) — Would start a second Job, and two cleanups would fight over the same rows.
  • History limit 2 — Only the last 2 finished Jobs stay.

Green output

$ kubectl apply -f otp-cleanup-cron.yaml
$ kubectl get jobs -n shop -l batch.kubernetes.io/cronjob-name=otp-cleanup -w
NAME                   STATUS    COMPLETIONS   DURATION   AGE
otp-cleanup-29358421   Running   0/1           30s        30s
otp-cleanup-29358421   Complete  1/1           93s        93s
otp-cleanup-29358423   Running   0/1           2s         2s
$ kubectl get cronjob otp-cleanup -n shop
NAME          SCHEDULE    SUSPEND   ACTIVE   LAST SCHEDULE
otp-cleanup   * * * * *   False     1        2s

Expected result. There is no Job for minute 22. Forbid skipped it. The number in the Job name is the scheduled time in minutes since 1970.

Example 3: Nightly backup with Replace, suspend and a real-looking command

Goal. Nightly backup with Replace, suspend and a real-looking command.

backup-cron.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
  namespace: shop
spec:
  schedule: "0 2 * * *"
  timeZone: "Asia/Kolkata"
  concurrencyPolicy: Replace
  suspend: false
  jobTemplate:
    spec:
      backoffLimit: 1
      ttlSecondsAfterFinished: 86400
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: backup
              image: mysql:8.4
              envFrom:
                - secretRef:
                    name: mysql-auth
              command: ["sh", "-c"]
              args:
                - mysqldump -h mysql-0.mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases > /backup/all-$(date +%F).sql &&
                  ls -lh /backup
              volumeMounts:
                - name: backup
                  mountPath: /backup
          volumes:
            - name: backup
              emptyDir: {}

What each line does

  • 0 2 * * * — 2 AM IST every night.
  • Replace — If last night's backup is stuck, kill it and start fresh.
  • secretRef mysql-auth — Same Secret as the StatefulSet lab in 60.4; apply mysql-secret.yaml, mysql-headless.yaml and mysql-sts.yaml from there first.
  • mysql-0.mysql — Stable DNS name from the headless Service.
  • emptyDir /backup — Lab only: the file vanishes with the pod. In production write to a PVC or upload to S3 (chapter 61).
  • ttlSecondsAfterFinished: 86400 — Each Job is removed a day later.

Green output

$ kubectl apply -f backup-cron.yaml
$ kubectl create job backup-now -n shop --from=cronjob/db-backup
$ kubectl logs -n shop job/backup-now
-rw-r--r-- 1 root root 3.1M Oct 25 06:41 all-2026-10-25.sql
$ kubectl patch cronjob db-backup -n shop -p '{"spec":{"suspend":true}}'
cronjob.batch/db-backup patched
$ kubectl get cronjob db-backup -n shop
NAME        SCHEDULE    TIMEZONE       SUSPEND   ACTIVE
db-backup   0 2 * * *   Asia/Kolkata   True      0

Expected result. The dump worked. During a planned database upgrade we suspend the backup so it does not run against a half-upgraded server.

Every command, step by step (kind lab with kubectl)

  1. Explain the CronJob spec

    kubectl explain cronjob.spec | grep -E 'schedule|timeZone|concurrency|suspend|History'
    

    What it does: All the fields we use.

    concurrencyPolicy   <string>
    failedJobsHistoryLimit  <integer>
    schedule    <string> -required-
    successfulJobsHistoryLimit  <integer>
    suspend <boolean>
    timeZone    <string>
    
  2. Create one from the CLI

    kubectl create cronjob ping -n shop --image=busybox:1.36 --schedule='*/5 * * * *' -- echo ping
    

    What it does: Quick CronJob without YAML.

    cronjob.batch/ping created
    
  3. Apply the daily report

    kubectl apply -f daily-report-cron.yaml
    kubectl get cronjob -n shop
    

    What it does: Shows SCHEDULE and TIMEZONE.

    daily-report   0 6 * * *   Asia/Kolkata   False   0   <none>
    
  4. Run it now

    kubectl create job manual-report -n shop --from=cronjob/daily-report
    kubectl wait -n shop --for=condition=complete job/manual-report --timeout=60s
    

    What it does: The standard way to test a CronJob.

    job.batch/manual-report condition met
    
  5. Watch overlap handling

    kubectl apply -f otp-cleanup-cron.yaml
    kubectl get jobs -n shop -w
    

    What it does: Leave it for 3 minutes and count Jobs.

    otp-cleanup-29358421   Running   0/1
    
  6. Read events for skips

    kubectl describe cronjob otp-cleanup -n shop | tail -5
    

    What it does: Shows SuccessfulCreate and, with Forbid, no Job for busy minutes.

    Normal  SuccessfulCreate  cronjob-controller  Created job otp-cleanup-29358421
    Normal  SawCompletedJob   cronjob-controller  Saw completed job: otp-cleanup-29358421, status: Complete
    
  7. Change the schedule

    kubectl patch cronjob otp-cleanup -n shop -p '{"spec":{"schedule":"*/15 * * * *"}}'
    

    What it does: Unlike Jobs, CronJob fields can be edited; the next Job uses them.

    cronjob.batch/otp-cleanup patched
    
  8. Suspend everything in a namespace

    for c in $(kubectl get cronjob -n shop -o name); do kubectl patch $c -n shop -p '{"spec":{"suspend":true}}'; done
    

    What it does: Handy before maintenance.

    cronjob.batch/daily-report patched
    cronjob.batch/otp-cleanup patched
    
  9. Clean up

    kubectl delete cronjob -n shop daily-report otp-cleanup db-backup ping
    kubectl delete job -n shop manual-report backup-now --ignore-not-found
    

    What it does: Deleting a CronJob deletes its Jobs too.

    cronjob.batch "daily-report" deleted
    

When it is red 1

The CronJob "daily-report" is invalid: spec.schedule: Invalid value: "0 6 * *": expected exactly 5 fields, found 4

Cause. The schedule has the wrong number of fields.

Fix, step by step

  1. Use 5 fields: minute hour day month weekday.
  2. Check with kubectl apply --dry-run=server.

When it is red 2

spec.timeZone: Invalid value: "IST": unknown time zone IST

Cause. Short names like IST are not valid IANA zones.

Fix, step by step

  1. Use Asia/Kolkata.

When it is red 3

Warning  TooManyMissedTimes  Cannot determine if job needs to be started: too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew

Cause. The CronJob was suspended or the controller was down for a long time with a frequent schedule.

Fix, step by step

  1. Set startingDeadlineSeconds, for example 300.
  2. Re-apply; future runs work normally.

When it is red 4

two report emails arrived at 6:00

Cause. concurrencyPolicy Allow and a slow previous run, or two CronJobs with the same work.

Fix, step by step

  1. Set concurrencyPolicy: Forbid.
  2. Make the job idempotent: check if today's report is already sent.
  3. kubectl get cronjob -A | grep report to find duplicates.

Practice tasks

Easy Task 1: Create

Why: First CronJob.

What to do: Create ping with kubectl create cronjob.

How:
  1. --schedule.
Solution
kubectl create cronjob ping -n shop --image=busybox:1.36 --schedule='*/5 * * * *' -- echo ping

Easy Task 2: Run now

Why: Testing.

What to do: Trigger daily-report by hand.

How:
  1. --from=cronjob.
Solution
kubectl create job manual-report -n shop --from=cronjob/daily-report

Easy Task 3: Read

Why: Status.

What to do: Print lastScheduleTime of ping after 5 minutes.

How:
  1. jsonpath status.
Solution
kubectl get cronjob ping -n shop -o jsonpath='{.status.lastScheduleTime}'

Medium Task 4: Time zone

Why: IST.

What to do: Change ping to timeZone Asia/Kolkata and a 9:30 schedule.

How:
  1. 30 9 * * *.
Solution
kubectl patch cronjob ping -n shop -p '{"spec":{"timeZone":"Asia/Kolkata","schedule":"30 9 * * *"}}'

Medium Task 5: Forbid

Why: Overlap.

What to do: Run otp-cleanup for 3 minutes and count Jobs.

How:
  1. List jobs.
Solution
kubectl get jobs -n shop -l batch.kubernetes.io/cronjob-name=otp-cleanup

Medium Task 6: Replace

Why: Overlap 2.

What to do: Change otp-cleanup to Replace and watch the old Job get killed.

How:
  1. Events.
Solution
kubectl patch cronjob otp-cleanup -n shop -p '{"spec":{"concurrencyPolicy":"Replace"}}'

Medium Task 7: History

Why: Limits.

What to do: Set successful limit 1 and see older Jobs go.

How:
  1. patch.
Solution
kubectl patch cronjob otp-cleanup -n shop -p '{"spec":{"successfulJobsHistoryLimit":1}}'

Medium Task 8: Suspend

Why: Maintenance.

What to do: Suspend db-backup and confirm SUSPEND True.

How:
  1. patch suspend.
Solution
kubectl patch cronjob db-backup -n shop -p '{"spec":{"suspend":true}}'

Hard Task 9: Weekdays

Why: Real schedule.

What to do: Do the main task schedule.

How:
  1. */10 9-17 * * 1-5.
Solution
schedule: "*/10 9-17 * * 1-5"

Hard Task 10: Backup

Why: Real work.

What to do: Run backup-now against the 60.4 MySQL and print the file size.

How:
  1. Needs mysql-0 running.
Solution
kubectl logs -n shop job/backup-now

Homework: try at home

Write a CronJob that runs every 10 minutes from 9 AM to 6 PM IST on weekdays only, uses Forbid, keeps 5 successful Jobs and deletes each Job after 1 hour. Prove it with kubectl create job --from.

Recap

  • A CronJob creates a Job on a cron schedule, cluster-wide.
  • Set timeZone, for example Asia/Kolkata, or schedules run in UTC.
  • concurrencyPolicy Forbid or Replace prevents overlapping runs.
  • startingDeadlineSeconds and history limits keep it healthy and tidy.
  • Test with kubectl create job --from=cronjob; suspend during maintenance.
  • Next, 60.8 compares every controller in one lab.

FAQ

Q. Is a CronJob exact to the second?

No. It starts within a few seconds of the minute.

Q. Can a schedule run every 30 seconds?

No. One minute is the smallest step. Use a Deployment with a loop for faster work.

Q. What happens to running Jobs if I delete the CronJob?

They are deleted too, by cascade, unless you use --cascade=orphan.

Q. Does daylight saving matter in India?

Not for Asia/Kolkata, but it does for zones like Europe/London.

Q. Can I use @daily or @hourly?

Yes, those macros are supported.

Q. Where does the CronJob loop run?

In the cronjob controller in kube-controller-manager.

Interview questions

  1. How does a CronJob work?

    Model answer: The cronjob controller checks schedules and, at each due time, creates a Job from jobTemplate; the Job creates pods.

  2. Explain concurrencyPolicy.

    Model answer: Allow runs Jobs in parallel, Forbid skips a new run while one is active, Replace cancels the running Job and starts the new one.

  3. How do you make a CronJob use IST?

    Model answer: Set spec.timeZone: Asia/Kolkata (stable since Kubernetes 1.27).

  4. How do you test a CronJob without waiting?

    Model answer: kubectl create job test --from=cronjob/NAME.

  5. A CronJob stopped running. What do you check?

    Model answer: suspend, lastScheduleTime, events like TooManyMissedTimes, startingDeadlineSeconds, failed Jobs and their pod logs.

Quick quiz

Q1. What does */15 * * * * mean?

Every 15 minutes.

Q2. Which time zone is used without timeZone?

The controller manager's, usually UTC.

Q3. Which policy skips a run if the last is still active?

Forbid.

Q4. How do you run a CronJob right now?

kubectl create job NAME --from=cronjob/CRON.

Q5. Default successful history limit?

3.

Samajla ka?