Documentation / saat
Saat
A lean Helm chart for deploying scheduled jobs on Kubernetes. Saat (Arabic/Turkish for “hour” / “clock”) wraps a Kubernetes CronJob with production defaults for scheduled workloads: periodic batch tasks, scheduled report generation, housekeeping/cleanup scripts, and any container that should run on a cron schedule rather than as a long-lived workload.
Unlike Safeena (HTTP services with ingress) and Khadim (long-running workers/daemons), Saat renders a single CronJob — no Deployment, no Service, no ingress. Each fire of the schedule spawns a fresh Job and Pod.
Prerequisites
- Kubernetes 1.27+ (required for
spec.timeZonesupport onCronJob) - Helm 3.x
Architecture
┌──────────┐
│ CronJob │ (schedule + timeZone)
└────┬─────┘
│ fires on schedule
▼
┌──────────┐
│ Job │ (per-run, ephemeral)
└────┬─────┘
▼
┌──────────┐
│ Pod │ (runs your image, exits)
└──────────┘
- Each scheduled fire creates a new
Job, which spawns aPod. - The pod runs your container, exits, and is cleaned up after
ttlSecondsAfterFinished. concurrencyPolicycontrols what happens if the previous run is still active when the next fire is due.- Run history is bounded by
successfulJobsHistoryLimit/failedJobsHistoryLimit.
Installation
# Basic cron job — every hour, UTC
helm install hourly-report charts/saat \
--set image.repository=my-registry/hourly-report \
--set image.tag=v1.0.0 \
--set schedule="0 * * * *"
# With a fixed resource name, independent of the release name
helm install hourly-report charts/saat \
--set fullnameOverride=hourly-report \
--set image.repository=my-registry/hourly-report \
--set image.tag=v1.0.0
# Custom timezone (run at 02:30 every day, Istanbul time)
helm install nightly-cleanup charts/saat \
--set image.repository=my-registry/cleanup \
--set schedule="30 2 * * *" \
--set timeZone="Europe/Istanbul"
Configuration
Schedule
Set a standard 5-field cron expression. The timeZone field is rendered into CronJob.spec.timeZone (Kubernetes 1.27+):
schedule: "*/15 * * * *" # every 15 minutes
timeZone: "Europe/London"
If your cluster is older than 1.27, omit timeZone and ensure your cron expression is in UTC (the controller’s default).
Pausing without uninstalling
Set suspend: true to stop creating new runs while keeping the chart installed. Existing in-flight runs are not killed — only future fires are skipped.
suspend: true
Concurrency
Controls what happens when a previous run is still active at the next fire:
| Value | Behaviour |
|---|---|
Allow |
Run concurrently with the previous run. |
Forbid |
Skip the new run if a previous run is still active. (default) |
Replace |
Cancel the previous run and start a new one. |
concurrencyPolicy: Forbid
History limits
How many completed runs Kubernetes keeps around for inspection (kubectl get jobs). Older runs are garbage collected.
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
Starting deadline
If the controller misses a scheduled fire (e.g. controller downtime, suspended cluster), startingDeadlineSeconds says how late a missed run can be before it’s skipped. null means missed runs are caught up indefinitely until the next scheduled fire — generally not what you want for short-interval jobs.
startingDeadlineSeconds: 300 # skip any run that's more than 5 minutes late
Per-execution Job/Pod settings
These apply to each individual Job spawned by the CronJob:
job:
backoffLimit: 3 # retry the Pod up to N times on failure
activeDeadlineSeconds: 600 # kill the Job if it runs longer than N seconds
ttlSecondsAfterFinished: 3600 # delete completed Jobs after N seconds
restartPolicy: OnFailure # OnFailure | Never (Always is not valid for Jobs)
command: [] # override container CMD
args: [] # override container ARGS
Environment variables
Inject environment variables directly or from Kubernetes Secrets/ConfigMaps:
env:
- name: APP_ENV
value: production
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-db-credentials
key: password
envFrom:
- secretRef:
name: my-app-secrets
- configMapRef:
name: my-app-config
Volumes and volume mounts
Mount ConfigMaps, Secrets, PVCs, or emptyDirs into the cron container:
volumes:
- name: config
configMap:
name: my-cron-config
- name: scratch
emptyDir: {}
volumeMounts:
- name: config
mountPath: /etc/my-cron
readOnly: true
- name: scratch
mountPath: /tmp
Connecting to MySQL with TLS
When a MySQL instance deployed with the mysql chart has TLS enabled, the MySQL chart publishes the CA certificate as a ConfigMap named <mysql-release>-tls-ca. Cron jobs deployed via Saat can mount this ConfigMap to verify the MySQL server’s identity and establish encrypted connections.
volumes:
- name: mysql-ca
configMap:
name: my-db-tls-ca # <mysql-release>-tls-ca
volumeMounts:
- name: mysql-ca
mountPath: /etc/mysql-tls
readOnly: true
env:
- name: MYSQL_SSL_CA
value: /etc/mysql-tls/ca.crt
The container then reads the MYSQL_SSL_CA environment variable and passes it to its MySQL driver. See the MySQL chart README for driver-specific examples (PHP/Kipchak, Node.js, Python, Go).
Examples
Hourly report generator
fullnameOverride: "hourly-report"
image:
repository: registry.example.com/acme/reports
tag: "v1.2.0"
schedule: "0 * * * *"
timeZone: "UTC"
concurrencyPolicy: Forbid
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
envFrom:
- secretRef:
name: reports-env
Nightly cleanup with a hard timeout
fullnameOverride: "nightly-cleanup"
image:
repository: my-registry/cleanup
tag: "v0.4.1"
schedule: "30 2 * * *"
timeZone: "Europe/Istanbul"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
job:
backoffLimit: 1
activeDeadlineSeconds: 1800 # kill the run if it exceeds 30 minutes
ttlSecondsAfterFinished: 86400 # keep completed jobs for a day for debugging
restartPolicy: Never
command: ["/app/cleanup"]
args: ["--dry-run=false"]
env:
- name: DRY_RUN
value: "false"
Frequent job with MySQL TLS
fullnameOverride: "metrics-sync"
image:
repository: my-registry/metrics-sync
tag: "v2.0.0"
schedule: "*/5 * * * *"
concurrencyPolicy: Replace
job:
backoffLimit: 0
activeDeadlineSeconds: 240
env:
- name: DB_HOST
value: my-db
- name: MYSQL_SSL_CA
value: /etc/mysql-tls/ca.crt
envFrom:
- secretRef:
name: metrics-sync-env
volumes:
- name: mysql-ca
configMap:
name: my-db-tls-ca
volumeMounts:
- name: mysql-ca
mountPath: /etc/mysql-tls
readOnly: true
resources:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
Paused job (kept installed but not firing)
fullnameOverride: "weekly-billing-export"
image:
repository: registry.example.com/acme/billing-export
tag: "v1.0.0"
schedule: "0 6 * * 1"
timeZone: "UTC"
suspend: true
Parameters
Image
| Parameter | Description | Default |
|---|---|---|
image.repository |
Container image repository | nginx |
image.tag |
Container image tag (defaults to appVersion) | Set in each release |
image.pullPolicy |
Image pull policy | IfNotPresent |
imagePullSecrets |
Image pull secrets | [{name: 1x-ax}] |
Naming
| Parameter | Description | Default |
|---|---|---|
nameOverride |
Override the chart name | "" |
fullnameOverride |
Override the full release name | "" |
Schedule
| Parameter | Description | Default |
|---|---|---|
schedule |
5-field cron expression (required) | "0 * * * *" |
timeZone |
IANA timezone for the schedule (K8s ≥ 1.27) | "UTC" |
suspend |
Pause scheduling without uninstalling | false |
concurrencyPolicy |
Allow | Forbid | Replace |
Forbid |
successfulJobsHistoryLimit |
How many successful run records to keep | 3 |
failedJobsHistoryLimit |
How many failed run records to keep | 3 |
startingDeadlineSeconds |
Skip a missed run if it’s more than N seconds late | null |
Job (per-execution)
| Parameter | Description | Default |
|---|---|---|
job.backoffLimit |
Retry the Pod up to N times on failure | 3 |
job.activeDeadlineSeconds |
Hard timeout for a single run (seconds) | null |
job.ttlSecondsAfterFinished |
Delete finished Jobs after N seconds | 3600 |
job.restartPolicy |
OnFailure | Never |
OnFailure |
job.command |
Override container CMD | [] |
job.args |
Override container ARGS | [] |
Resources
| Parameter | Description | Default |
|---|---|---|
resources.limits.cpu |
CPU limit | 100m |
resources.limits.memory |
Memory limit | 256Mi |
resources.requests.cpu |
CPU request | 20m |
resources.requests.memory |
Memory request | 64Mi |
Environment
| Parameter | Description | Default |
|---|---|---|
env |
Environment variables for the container | [] |
envFrom |
Environment variable sources (Secrets, ConfigMaps) | [] |
volumes |
Additional volumes on the Pod | [] |
volumeMounts |
Additional volume mounts on the container | [] |
Service account
| Parameter | Description | Default |
|---|---|---|
serviceAccount.create |
Create a ServiceAccount | false |
serviceAccount.automount |
Auto-mount API credentials | true |
serviceAccount.annotations |
ServiceAccount annotations | {} |
serviceAccount.name |
ServiceAccount name (auto-generated if empty) | "" |
Pod metadata and security
| Parameter | Description | Default |
|---|---|---|
podAnnotations |
Additional pod annotations | {} |
podLabels |
Additional pod labels | {} |
podSecurityContext |
Pod-level security context | {} |
securityContext |
Container-level security context | {} |
Scheduling
| Parameter | Description | Default |
|---|---|---|
nodeSelector |
Node selector labels | {} |
tolerations |
Pod tolerations | [] |
affinity |
Pod affinity rules | {} |