Schedule icon
Commands icon
Docker icon
Script icon
If icon
SlackIncomingWebhook icon

JobJanitor - Kubernetes completed Job cleaner

A Kestra blueprint that cleans up old completed Kubernetes Jobs and posts a Slack summary, with a safe dry-run default.

Categories
Infrastructure

How it works

  1. list_jobs (io.kestra.plugin.scripts.shell.Commands) runs kubectl get jobs in the target namespace and saves the full job list as JSON. It authenticates with a client certificate and key passed as Kestra secrets, the same pattern the Kubernetes gallery blueprints use.
  2. find_stale (io.kestra.plugin.scripts.python.Script) checks every job: only jobs with a Complete=True condition and a completionTime older than retention_days count as stale. Failed jobs are never touched, they stay for investigation. It also writes the stale names to stale.txt and builds the Slack summary.
  3. maybe_clean (io.kestra.plugin.core.flow.If) runs only when something is stale. Inside it, do_clean runs only in live mode (dry_run: false) and deletes each stale job with kubectl. send_summary always posts the report to Slack: what would be deleted in dry-run mode, what was deleted in live mode.
  4. There is no KV state in this flow, and that is deliberate. A janitor does not need memory: once a job is deleted it is gone, and dry-run mode is a report, not an alert.

What you get

  • Automatic cleanup of completed Jobs past the retention window, no more manual kubectl delete.
  • A Slack report every run that found something: which jobs, how old, and whether it was a dry run or a real cleanup.
  • Safety by default: dry_run is true unless you explicitly turn it off, failed jobs are never deleted, and the retention window guards fresh jobs.
  • Quiet runs when there is nothing stale to clean.

Who it's for

Anyone running batch Jobs on Kubernetes: migrations, backups, report generators, data loads. Completed jobs pile up forever by default and clutter kubectl get jobs until nobody can find the real failures. Works on any cluster you can reach with a client certificate: minikube, kind, managed clusters.

Why orchestrate this with Kestra

A shell script on a cron server can delete old jobs, but then you need somewhere to hold the secrets, a place to see the run history when something got deleted that should not have been, and a safe dry-run mode you can trust. Kestra gives you all three: secrets stay out of the YAML, every run keeps its logs and outputs where you can audit them, and the dry-run default means the first run can never surprise you.

Prerequisites

  • A Kubernetes cluster reachable from where Kestra runs, with a client certificate and key for authentication.
  • Kestra with the Docker task runner available (used for the kubectl and Python steps).
  • A Slack Incoming Webhook URL.

Secrets

Secret What it is
CLIENT_CRT base64-encoded client certificate for the cluster
CLIENT_KEY base64-encoded client key for the cluster
MASTERURL base64-encoded Kubernetes API server URL, reachable from inside the Kestra container
SLACK_WEBHOOK base64-encoded Slack Incoming Webhook URL

In Kestra OSS, secrets come from environment variables prefixed with SECRET_ and must be base64-encoded.

Quick start

  1. Start Kestra with the four secrets above as SECRET_ environment variables.
  2. Import this flow and run it with retention_days: 0 and dry_run: true: the Slack report lists what would be deleted, and nothing is touched.
  3. Create a test job that completes (kubectl create job jj-clean --image=busybox -- sh -c 'echo done'), wait for it to complete, run again in dry-run mode: it shows up in the report.
  4. Run once with dry_run: false: the job is deleted and the Slack report says so. Run again: quiet, nothing stale left.

How to extend

  • Clean up completed Pods too by listing pods with phase Succeeded and deleting the old ones the same way.
  • Add failed jobs to the cleanup with a longer retention window, keeping the safety split between completed and failed.
  • Lower the schedule to daily for busy namespaces by changing the cron trigger.
  • Route reports to PagerDuty or Opsgenie by swapping the Slack task for another notification task.

Inputs

Input Type Default What it does
namespace STRING default the namespace whose jobs to clean
retention_days INT 7 completed jobs older than this many days get deleted
dry_run BOOL true when true, only reports what would be deleted without deleting anything
slack_webhook STRING (required) Incoming webhook URL of the Slack channel for the cleanup report

Links

Pitfalls

  • dry_run defaults to true for a reason. Flip it to false only after you have seen a dry-run report and you trust the list.
  • Only jobs with Complete=True are ever deleted. Failed jobs are left alone because they might be evidence for an ongoing investigation.
  • A job without a completionTime is skipped. This should not happen for completed jobs, but the guard is there.
  • Make sure the MASTERURL is reachable from inside the Kestra container. localhost inside the container is not your host: use host.docker.internal when Kestra runs in Docker on the same machine as the cluster.
See How

New to Kestra?

Use blueprints to kickstart your first workflows.