Bahriya Tersane

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.timeZone support on CronJob)
  • 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 a Pod.
  • The pod runs your container, exits, and is cleaned up after ttlSecondsAfterFinished.
  • concurrencyPolicy controls 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 {}