Full lesson
Explore the full explanation, examples, and visuals at your own pace.
Nightly export: work done, Job still active
The export finished, but the Job never did. The logs say one point two million rows written, exiting zero. Yet nine hours later, the Job shows zero of one completions, and the Pod phase is still Running. The culprit is the log-shipper sidecar, which never stops running.
$ kubectl logs nightly-export-28123-x7k2p -c export
export done: 1.2M rows written, exiting 0
$ kubectl get job nightly-export-28123
NAME COMPLETIONS DURATION AGE
nightly-export-28123 0/1 9h 9h
$ kubectl get pod nightly-export-28123-x7k2p \
-o jsonpath='{.status.phase}'
RunningOne Pod, two containers, one timeline
Follow one Pod through the night. At 22:00, export and log-shipper start together. Export does its work, and at 22:40 it exits with code 0. But nothing tells log-shipper to stop. It's built to run indefinitely, so it never exits, and the Pod stays in Running.
Completion flows up only when every container stops
Completion only flows upward. The Job controller counts a success only when the Pod reaches Succeeded, and the Pod gets there only after all its regular containers exit with code zero.
So export exiting isn't the Pod completing. The Job waits for the Pod, and the Pod waits for every regular container.
Next 22:00 run, by concurrencyPolicy
Because the stuck Job still counts as active, concurrencyPolicy decides what the next 22:00 run does. Forbid skips it. Allow starts a new Job, so stuck Pods pile up. Replace kills the stuck run, but the new one hangs too.
- Forbid: new run skipped, stuck one stays
- Allow: new Job starts, stuck Pods pile up
- Replace: kills stuck run, new one hangs too
activeDeadlineSeconds ends the Job as Failed, not Succeeded
activeDeadlineSeconds is a safety limit, not a fix. It kills the Pod and marks the Job Failed with DeadlineExceeded, so alerts fire even though the export succeeded.
Fix 1: native sidecar in the job template
The fix is to move log-shipper out of containers and into initContainers, then give it restartPolicy Always. That one field marks it as a native sidecar. Export stays under containers. This needs a recent cluster, since the feature is on by default from 1.29, and your mesh or tooling must support it.
# CronJob jobTemplate.spec.template.spec
restartPolicy: Never
initContainers:
- name: log-shipper
image: fluent/fluent-bit
restartPolicy: Always # marks it a sidecar
containers:
- name: export
image: example/export:1.4Native sidecar: kubelet stops it after export exits
Replay the same night with a native sidecar. The shipper starts first, then export runs. When export exits with code 0, the kubelet sees the main work is done and stops the shipper. Now every container that counts has finished, so the Pod reaches Succeeded, and the Job shows 1 of 1 completions.
Other fixes and their costs
You have other options, each with a cost. A mesh shutdown endpoint, like Istio's quitquitquit, ties your export to that one mesh. Signalling the sidecar needs a shared file or process namespace. Native sidecars are cleanest, but your cluster version has to support them.
- Mesh quit endpoint: couples script to mesh
- Signal sidecar to exit: custom wiring
- Native sidecar: cleanest, needs version support
Diagnostic path
Three commands confirm it. First, kubectl get pod shows READY 1/2, so one container is still up. Then describe names the culprit: export is Completed with exit code 0, while log-shipper is Running. Finally, the Job status shows one active Pod and no succeeded count. The work finished; the Pod didn't.
kubectl get pod nightly-export-28123-x7k2p
# READY 1/2 -> one container still up
kubectl describe pod nightly-export-28123-x7k2p
# export: Terminated, Completed, Exit Code 0
# log-shipper: Running
kubectl get job nightly-export-28123 \
-o jsonpath='{.status.active} {.status.succeeded}'
# 1 (succeeded empty)Job waits for Pod; Pod waits for every regular container.
The Job waits for the Pod, and the Pod waits for every regular container. When a Job hangs after successful work, look for a long-lived container.