Partition Creation with Kubernetes CronJobs
When the application already runs on Kubernetes, the scheduler is right there — and the failure modes are different from cron on a host. This guide runs partition maintenance as one CronJob per shard, with concurrency policy, history limits, credentials handled properly and, most importantly, alerting that does not depend on the job’s own exit status. It is the container-native counterpart to the approaches in Automated Partition Creation Workflows, part of Partitioning Implementation Patterns & Routing.
Prerequisites
Step 1 — One CronJob per shard, generated from a template
# partition-maintenance.yaml — rendered once per shard by kustomize or helm
apiVersion: batch/v1
kind: CronJob
metadata:
name: partition-maintenance-shard3
labels: {app: partition-maintenance, shard: shard3}
spec:
schedule: "17 * * * *" # hourly, off the hour
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid # never two runs against one shard
startingDeadlineSeconds: 900 # skip rather than pile up after an outage
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 10
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
serviceAccountName: partition-maintenance
containers:
- name: maintenance
image: registry.internal/db-maintenance:2026.08.1
args: ["--shard", "shard3", "--tables", "events,metrics"]
env:
- name: PGHOST
value: shard3.internal
- name: PGPASSWORD
valueFrom:
secretKeyRef: {name: shard3-maintenance, key: password}
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {cpu: 500m, memory: 256Mi}
Operational note: concurrencyPolicy: Forbid plus the advisory lock inside the procedure gives two independent guarantees. Either alone is adequate; both together mean a change to one does not silently remove the protection.
DBA tip: startingDeadlineSeconds matters after a control-plane outage. Without it, Kubernetes may try to run every missed schedule at once, sending a burst of DDL at a shard that has just come back.
Step 2 — Make the container do one thing, idempotently
#!/usr/bin/env python3
# maintain.py — the whole container entrypoint
import argparse, os, sys, psycopg
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--shard", required=True)
ap.add_argument("--tables", required=True)
args = ap.parse_args()
with psycopg.connect(autocommit=False) as conn:
with conn.cursor() as cur:
cur.execute("CALL maintain_partitions()") # idempotent, advisory-locked
cur.execute("""SELECT table_name,
(newest_bound - current_date) AS days_ahead,
pending_retire
FROM v_partition_health""")
rows = cur.fetchall()
conn.commit()
push_metrics(args.shard, rows) # to the push gateway
for table, days_ahead, pending in rows:
print(f"{args.shard} {table}: {days_ahead}d ahead, {pending} pending retire")
if days_ahead < 21:
print(f"WARNING {table} horizon is only {days_ahead} days", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
Operational note: The job pushes the health metrics itself. That is what makes an absent job detectable — the metric goes stale, and staleness is alertable, whereas a job that never ran produces no event at all.
SRE tip: Keep the image small and the entrypoint single-purpose. A maintenance container that also runs migrations, backups and reports becomes a thing nobody dares to schedule hourly.
Step 3 — Alert on the database, not on the job
# primary alert: the horizon, published by the job, per shard and table
min by (shard, table) (partition_days_ahead) < 21
# staleness: the job stopped publishing at all
time() - max by (shard) (push_time_seconds{job="partition-maintenance"}) > 7200
# secondary: Kubernetes noticed a failure
kube_job_status_failed{job_name=~"partition-maintenance-.*"} > 0
Operational note: A CronJob removed by a bad kubectl apply --prune produces no failures, no events and no logs. Only a measurement taken from the database notices.
DBA tip: Publish the metric with a shard and table label so one alert rule covers the fleet and the notification names the affected shard.
Verification
Prove the whole chain, including the parts that only matter when something is wrong:
# 1. run it now rather than waiting for the schedule
kubectl create job --from=cronjob/partition-maintenance-shard3 pm-manual-1
kubectl logs job/pm-manual-1
shard3 events: 92d ahead, 0 pending retire
shard3 metrics: 87d ahead, 1 pending retire
# 2. confirm concurrency is actually forbidden
kubectl create job --from=cronjob/partition-maintenance-shard3 pm-manual-2
# the advisory lock makes the second run a no-op; check its logs say so
# 3. simulate the invisible failure
kubectl patch cronjob partition-maintenance-shard3 -p '{"spec":{"suspend":true}}'
# the staleness alert must fire within its window; the horizon alert follows days later
The third check is the one worth doing. A suspended CronJob is the most common real failure and the only one that produces no error anywhere.
Where This Sits Relative to the Other Schedulers
Running maintenance as a Kubernetes CronJob is not better or worse than pg_cron or an external workflow engine — it is a different set of trade-offs, and the right one depends on what the team already operates.
Against pg_cron, the CronJob wins on visibility and loses on proximity. A CronJob’s history, logs and failures land in the same place as every other workload, and the maintenance image is deployed with the same pipeline as the application. What it gives up is being inside the database: pg_cron cannot be blocked by a network partition, does not need credentials, and keeps working when the Kubernetes control plane is unavailable.
Against Airflow or a similar engine, the CronJob wins on simplicity and loses on orchestration. There is no dependency graph, no backfill semantics and no cross-shard coordination — which is exactly right for a job whose steps are idempotent and independent, and exactly wrong if partition maintenance is one node in a larger data pipeline that must run in order.
The practical guidance is to match the scheduler to the on-call rotation. Whoever is paged when partitions stop being created should already be fluent in the tool that creates them, because the first debugging step is always “did it run, and what did it say?”.
Failure mode table
| Failure mode | Root cause | SRE mitigation |
|---|---|---|
| Maintenance silently stops | the CronJob was suspended or pruned by a deployment change | alert on metric staleness and on the partition horizon; both are independent of the job’s own status |
| A burst of runs after a control-plane outage | missed schedules were replayed once the control plane recovered | set startingDeadlineSeconds so missed runs are skipped rather than queued |
| Two runs collide during a slow maintenance pass | concurrencyPolicy left at the default Allow |
set Forbid, and keep the advisory lock inside the procedure as the second layer |
FAQ
One CronJob for all shards or one per shard?
One per shard, generated from the same template. A single job that loops over shards fails as a unit, hides which shard is behind, and cannot be retried selectively. Per-shard jobs make the Kubernetes UI itself the status dashboard and let one unreachable shard fail without affecting the others.
Why not rely on the CronJob's failure status for alerting?
Because the most common failure is that the job did not run at all — suspended, deleted by a bad apply, or scheduled on a node pool that no longer exists. A failed job is visible; an absent job is not. Alert on the partition horizon measured from the database, and treat job failures as a secondary signal.
How should database credentials reach the job?
Through a mounted secret or a workload identity that the job exchanges for a short-lived token, never as a literal in the manifest. The job needs CREATE on the schema and ownership of the partitioned tables, which is a strictly smaller grant than the application’s role — use a dedicated maintenance role rather than reusing the app credentials.
Related
- Automated Partition Creation Workflows — the parent topic comparing schedulers and their failure signals
- Implementing a Partition Retention Policy with Scheduled Jobs — the procedure this container calls
- Automating Partition Creation with Apache Airflow — the same per-shard mapping in a workflow engine