Intro
Kubernetes CronJobs run containers on a schedule. A local lab lets you practice safely, reproduce issues, and build confidence before touching shared environments. In this guide you will set up a disposable cluster, create a dedicated namespace, and run two CronJob examples: a minutely echo and a small Python task. You will validate schedules, concurrency behavior, logs, history limits, and cleanup routines so your team can test changes quickly and repeatably.
Workflow Overview
Follow this workflow to keep your experiments clean and fast:
- Create an isolated local cluster.
- Create a namespace and service account for CronJobs.
- Apply a tiny pilot CronJob with a fast schedule.
- Observe Job and Pod creation, logs, and history limits.
- Exercise controls: suspend, run-once, replace concurrency, backoff.
- Add a realistic second example (Python) with env vars and resources.
- Troubleshoot failures by inspecting events, Jobs, and Pods.
- Add safety guards: resource limits, non-root, cleanup scripts.
- Automate common checks into one-liners or scripts.
- Reset or delete the lab quickly when done.
Local Pilot Plan
Pilot goal: a minutely CronJob that prints the timestamp and a message, keeps a short Job history, and forbids overlapping runs.
Success criteria:
- Within 2 minutes you can see at least one successful Job.
- You can read logs from the latest Job Pod.
- No overlapping runs occur.
- You can suspend it, then resume.
- You can trigger a one-off run on demand.
This narrow scope is fast to measure and easy to inspect locally before any deployment.
Set up a local cluster
Use kind for a lightweight, disposable cluster.
Create the cluster:
kind create cluster --name cronlab
Point kubectl at it:
kubectl cluster-info --context kind-cronlab
Verify nodes:
kubectl get nodes
Alternative: minikube start (if you prefer minikube).
Namespace and ServiceAccount
Scope your lab to a namespace and run pods as non-root when possible.
Create a namespace and service account (save as ns-sa.yaml):
---
apiVersion: v1
kind: Namespace
metadata:
name: cronlab
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cronjob-sa
namespace: cronlab
Apply it:
kubectl apply -f ns-sa.yaml
Example 1: minutely echo job
A tiny CronJob that runs every minute, forbids overlaps, and keeps short history. Save as cron-echo.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: echo-minutely
namespace: cronlab
spec:
schedule: '* * * * *'
concurrencyPolicy: Forbid
startingDeadlineSeconds: 30
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
metadata:
labels:
app: echo-minutely
spec:
serviceAccountName: cronjob-sa
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: echo
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ['sh','-c','date; echo Hello from CronJob; sleep 5']
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
Apply and observe:
kubectl apply -f cron-echo.yaml
kubectl get cronjob -n cronlab
kubectl get jobs -n cronlab --watch
Get logs from the most recent Job Pod:
kubectl get pods -n cronlab -l app=echo-minutely
kubectl logs -n cronlab <pod-name>
Suspend and resume using a patch file (safer across shells). Save suspend-true.yaml:
spec:
suspend: true
Then:
kubectl patch cronjob echo-minutely -n cronlab --type merge --patch-file suspend-true.yaml
Set spec.suspend to false to resume.
Run once on demand without waiting for the schedule:
kubectl create job echo-now-1 --from=cronjob/echo-minutely -n cronlab
Use a new name each time (echo-now-2, echo-now-3, ...).
Example 2: Python ETL mock
This example simulates a small data task every 5 minutes, uses env vars, scratch storage, and modest resources. Save as cron-python.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: python-etl-mock
namespace: cronlab
spec:
schedule: '*/5 * * * *'
concurrencyPolicy: Replace
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 2
jobTemplate:
spec:
backoffLimit: 2
template:
metadata:
labels:
app: python-etl-mock
spec:
serviceAccountName: cronjob-sa
restartPolicy: OnFailure
containers:
- name: worker
image: python:3.11-slim
command: ['python','-c','import os, time, random, sys; print("starting job"); time.sleep(2); n=random.randint(0,9); print("processed", n, "records from", os.getenv("SOURCE_URL")); sys.exit(0)']
env:
- name: SOURCE_URL
value: 'file:///data/input.csv'
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: scratch
mountPath: /data
volumes:
- name: scratch
emptyDir: {}
Apply and check:
kubectl apply -f cron-python.yaml
kubectl get cronjob -n cronlab
kubectl get jobs -n cronlab -l app=python-etl-mock
kubectl logs -n cronlab -l job-name -c worker --tail=50 --all-containers=false
Observability and troubleshooting
Common checks:
- List CronJobs:
kubectl get cronjob -n cronlab
- Describe a CronJob:
kubectl describe cronjob echo-minutely -n cronlab
- Watch Jobs:
kubectl get jobs -n cronlab --watch
- Show Pods for a Job:
kubectl get pods -n cronlab -l job-name=<job-name>
- Pod logs:
kubectl logs -n cronlab <pod-name>
- Events on failures:
kubectl describe job/<job-name> -n cronlab
Run a CronJob immediately without waiting:
kubectl create job echo-now-1 --from=cronjob/echo-minutely -n cronlab
Repeat with a new name each time (echo-now-2, echo-now-3, ...).
Simulate failure for testing backoff by changing the command to exit 1, then observe retries and events.
Safety and resource controls
Use these guardrails in your lab and in real workloads:
- concurrencyPolicy: Forbid or Replace to prevent overlaps.
- startingDeadlineSeconds: avoid missing runs after short outages.
- backoffLimit: cap retries for failing Jobs.
- successfulJobsHistoryLimit and failedJobsHistoryLimit: control list sizes.
- Resource requests/limits: prevent noisy neighbors locally.
- Security context: runAsNonRoot, seccompProfile=RuntimeDefault.
- Namespace scoping: keep lab artifacts contained.
Cleanup quickly:
kubectl delete ns cronlab
kind delete cluster --name cronlab
Automate and iterate locally
Package your lab steps into a simple script or Makefile so new teammates can spin up, test, and tear down with a few commands. Keep manifests in version control, add a smoke test that creates a Job from each CronJob, tails logs, and asserts a zero exit code. This separation of setup, execution, and review helps you add new examples without rework.
Conclusion
You now have a repeatable local CronJobs lab: an isolated cluster, a scoped namespace, a fast pilot CronJob, and a more realistic Python example. You can observe schedules, limit history, prevent overlaps, run on demand, and clean up safely. Next steps: add alerts for failed Jobs, parameterize environments via ConfigMaps and Secrets, and codify your checks so every change is quick to validate locally.