Kubernetes integration

Monitor Kubernetes CronJob and Job resources with Drumbeats. Sidecar pattern, init container, and lifecycle hooks for ping wiring.

This guide shows how to wire Drumbeats into Kubernetes CronJob and Job resources. By the end your CronJobs send start / success / failure pings without modifying the application container.

The two patterns that work cleanly are command wrapping (the simplest) and init + sidecar containers (when you cannot edit the workload command). Pick command wrapping unless your workload runs in a vendor image you do not control.

Pattern 1: Command wrapping

Wrap the container command with a small shell snippet. Works for any image that includes curl.

cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-backup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: ghcr.io/myorg/backup:latest
              env:
                - name: DRUMBEATS_MONITOR
                  value: "11111111-2222-3333-4444-555555555555"
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -euo pipefail
                  RUN_ID="k8s-$(date +%s)-$RANDOM"
                  API="https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}"

                  curl -sf --max-time 3 "${API}/start?run_id=${RUN_ID}"

                  if /usr/local/bin/backup.sh; then
                    curl -sf --max-time 3 "${API}/success?run_id=${RUN_ID}"
                  else
                    curl -sf --max-time 3 "${API}/failure?run_id=${RUN_ID}"
                    exit 1
                  fi
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-backup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: ghcr.io/myorg/backup:latest
              env:
                - name: DRUMBEATS_MONITOR
                  value: "11111111-2222-3333-4444-555555555555"
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -euo pipefail
                  RUN_ID="k8s-$(date +%s)-$RANDOM"
                  API="https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}"

                  curl -sf --max-time 3 "${API}/start?run_id=${RUN_ID}"

                  if /usr/local/bin/backup.sh; then
                    curl -sf --max-time 3 "${API}/success?run_id=${RUN_ID}"
                  else
                    curl -sf --max-time 3 "${API}/failure?run_id=${RUN_ID}"
                    exit 1
                  fi

The exit 1 after the failure ping preserves the Job's failure state, so Kubernetes' own retry / backoff logic still fires.

Pattern 2: Init container + sidecar (for unmodifiable workloads)

If you cannot change the workload's command, use an initContainer to send start and a lifecycle.postStop hook on a sidecar to send the result. The pattern is more YAML but works for vendor images:

cronjob-vendor.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: vendor-export
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          shareProcessNamespace: true
          initContainers:
            - name: drumbeats-start
              image: curlimages/curl:8.10.1
              env:
                - name: DRUMBEATS_MONITOR
                  value: "66666666-7777-8888-9999-aaaaaaaaaaaa"
                - name: RUN_ID
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
              command:
                - sh
                - -c
                - |
                  curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}/start?run_id=${RUN_ID}"
          containers:
            - name: workload
              image: vendor/export:1.4.0
              # ... vendor-provided spec, command unchanged ...
            - name: drumbeats-finisher
              image: curlimages/curl:8.10.1
              env:
                - name: DRUMBEATS_MONITOR
                  value: "66666666-7777-8888-9999-aaaaaaaaaaaa"
              command: ["sh", "-c", "trap 'curl -sf --max-time 3 \"https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}/success?run_id=${HOSTNAME}\"' EXIT; tail -f /dev/null & wait"]
apiVersion: batch/v1
kind: CronJob
metadata:
  name: vendor-export
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          shareProcessNamespace: true
          initContainers:
            - name: drumbeats-start
              image: curlimages/curl:8.10.1
              env:
                - name: DRUMBEATS_MONITOR
                  value: "66666666-7777-8888-9999-aaaaaaaaaaaa"
                - name: RUN_ID
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
              command:
                - sh
                - -c
                - |
                  curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}/start?run_id=${RUN_ID}"
          containers:
            - name: workload
              image: vendor/export:1.4.0
              # ... vendor-provided spec, command unchanged ...
            - name: drumbeats-finisher
              image: curlimages/curl:8.10.1
              env:
                - name: DRUMBEATS_MONITOR
                  value: "66666666-7777-8888-9999-aaaaaaaaaaaa"
              command: ["sh", "-c", "trap 'curl -sf --max-time 3 \"https://api.drumbeats.io/v1/ping/${DRUMBEATS_MONITOR}/success?run_id=${HOSTNAME}\"' EXIT; tail -f /dev/null & wait"]

The sidecar approach is fragile — it cannot tell success from failure without inspecting the workload container's exit code. For non-trivial cases prefer Pattern 1 or build a wrapper image around the vendor image.

Job vs CronJob

  • Job — one-off work. Use Pattern 1 and call kubectl create from your scheduler. The Drumbeats monitor type should be Event-driven (JOB_BASIC), not Cron — there is no schedule to enforce.
  • CronJob — scheduled work. Use Pattern 1 and pair with a Cron Drumbeats monitor matching the same schedule + timezone.

Error handling

  • activeDeadlineSeconds. When Kubernetes terminates a Job past its deadline, the failure ping never fires. Set max_duration_seconds on the Drumbeats monitor to the same value so the hang surfaces server-side.
  • backoffLimit retries. Each retry sends its own start ping. To deduplicate, include the attempt number in run_id: RUN_ID="k8s-${POD_NAME}-${RETRY:-0}". Drumbeats stores each attempt as a separate run on the same monitor.
  • Pod evictions. A pod evicted before the success ping fires registers as a hung run. Pair max_duration_seconds with the start ping.

Production patterns

  • Time-bound pings. curl --max-time 3 keeps a slow Drumbeats request from blocking the Job's completion.
  • Bundle curl into the image. Workloads built on scratch or distroless will not have curl. Either bake curl into the image or use the sidecar pattern.
  • Stable run IDs. Use metadata.name (the Job name, which embeds the CronJob's timestamp) as the run_id. This lets the Drumbeats dashboard cross-reference the Kubernetes Job from the run history.
  • Multi-environment via slug. A single manifest can route to different monitors per cluster — see Ping API: scheduled pings for the slug endpoint.