Keep every container image referenced by any pod in your Kubernetes cluster pre-pulled on every node, without ever running those images.
Kubelet pulls images on demand when a pod starts. If a pod is rescheduled to a node that doesn't have the image cached (e.g. a worker that just rejoined after a reboot) and the upstream registry is unreachable at that moment (e.g. your home internet is down after a power outage), the pod stays Pending.
Pre-pulling every "important" image on every node fixes that, but the obvious patterns are awkward:
- One DaemonSet per image with the image as the container:
fine for
busybox-style images, broken for distroless ones (nosleepto keep the container alive, so you'd need a different no-op command per image). - A privileged DaemonSet that calls
crictl pull: works, but needshostPathto the container runtime socket and a privileged container, and the pulled image can still be evicted by kubelet's image GC if no Pod spec references it.
This operator uses Kubernetes [ImageVolume]
(https://kubernetes.io/docs/concepts/storage/volumes/#image) sources.
Each image to be kept warm becomes a volumes[].image.reference
entry on the warmer DaemonSet's pod template:
volumes:
- name: warm-deadbeef
image:
reference: rancher/mirrored-coredns-coredns:1.14.2
pullPolicy: IfNotPresentThe kubelet pulls the image to mount it as a read-only volume, and
treats the volume reference as "image in use", so the image stays
cached even under disk pressure. The pod's only container is a tiny
holder image (you bring your own) that just runs sleep infinity.
None of the warmed images are ever executed.
ImageVolume is alpha in Kubernetes v1.31, beta on by default in
v1.33, and GA in v1.34. This operator assumes that path is available
on the cluster it runs against.
Watches all Pods cluster-wide, collects the unique set of container
image references (both containers[] and initContainers[]),
filters out references the operator's flags say to skip, and
reconciles a single DaemonSet whose pod template lists every
remaining image. New workloads' images get added on the next
reconcile (debounced after each pod event, with a configurable
periodic resync as a safety net).
Build:
go build .
Required RBAC: cluster-wide list/watch on Pods, plus
get/create/update/patch/delete on DaemonSets in the
namespace it's configured to manage.
Flags:
--namespace string Namespace in which to manage the warmer DaemonSet. (default "image-warmer")
--daemonset-name string Name of the warmer DaemonSet. (default "image-warmer")
--holder-image string Image reference for the holder container.
Must already be cached on every node and
include a tag or digest. Required.
--exclude-prefixes string Comma-separated list of image-ref prefixes
to skip. Images starting with any of these
are never added to the warmer.
--resync duration Resync interval. (default 5m0s)
--kubeconfig string Path to kubeconfig (only for development).
Typical pattern: choose a holder image that you already control
(e.g. your own tiny alpine-based binary you ship to every node
anyway) and an --exclude-prefixes that skips your own registry
(images already coming from a registry you control don't usually
need warming).
image-warmer-operator \
--holder-image=registry.example.com/me/holder:abc123 \
--exclude-prefixes=registry.example.com/
Early. Used in a personal home cluster. No formal release. Open to issues/PRs if you find it useful.
Apache 2.0