Skip to content

k8s: optionally mount externals from an image volume instead of copying in fs-init - #399

Open
DJRH wants to merge 1 commit into
actions:mainfrom
DJRH:feat/novolume-externals-image-volume
Open

k8s: optionally mount externals from an image volume instead of copying in fs-init#399
DJRH wants to merge 1 commit into
actions:mainfrom
DJRH:feat/novolume-externals-image-volume

Conversation

@DJRH

@DJRH DJRH commented Jul 23, 2026

Copy link
Copy Markdown

What

Adds an opt-in ACTIONS_RUNNER_K8S_EXTERNALS_FROM_IMAGE=true that mounts the runner's externals directly from an image volume (the runner image, from ACTIONS_RUNNER_IMAGE) instead of copying them into an emptyDir in the fs-init init container on every job.

When enabled:

  • The externals volume becomes an image: volume referencing ACTIONS_RUNNER_IMAGE, mounted read-only at /__e via subPath: home/runner/externals.
  • fs-init no longer mvs /home/runner/externals/* into an emptyDir (and no longer mounts that volume).

Default (unset/false) is unchanged — the externals emptyDir is seeded by fs-init exactly as today.

Why

In kubernetes / kubernetes-novolume mode, fs-init copies the full externals tree (~600MB) into an emptyDir on every job pod. That copy is pure startup latency on every job. Kubernetes image volumes let the kubelet mount the runner image's already-present externals directly, so the per-job copy disappears (the image is pulled once per node and cached). In our environment this removed a multi-minute cold-start under burst.

Safety / gating

  • Opt-in, default off — zero behaviour change unless the env is set.
  • Image volumes need the ImageVolume feature gate, default-on in Kubernetes 1.35+ (beta). On clusters where it's unavailable the API server silently drops the image: volume source and the job would start with nothing at /__e, so this is deliberately not auto-enabled — the operator opts in when they know their cluster supports it. Documented in packages/k8s/README.md.

Implementation

  • utils.ts: ENV_EXTERNALS_FROM_IMAGE, externalsFromImage(), runnerImage(), externalsVolume(); CONTAINER_VOLUMEScontainerVolumes() (the /__e mount gains subPath+readOnly only when enabled).
  • index.ts: gate the fs-init seed commands + externals mount, and use externalsVolume() in both createJobPod and createContainerStepPod.
  • prepare-job.ts / run-container-step.ts: use containerVolumes().

Testing

  • npm run bootstrap && npm run build-all — clean (TypeScript compiles; image: volume typechecks against @kubernetes/client-node ^1.3.0).
  • prettier --check — clean.
  • Manually validated both paths render the expected pod spec (default = emptyDir + fs-init copy; enabled = image: volume, no copy, read-only /__e subPath).

Happy to add unit coverage for containerVolumes()/externalsVolume() if you'd like.

@DJRH
DJRH requested a review from nikola-jokic as a code owner July 23, 2026 16:47
The kubernetes/kubernetes-novolume hooks copy the runner's externals
(~600MB) from the runner image into an emptyDir via the fs-init init
container on every job, which adds materially to job pod startup time.

Add an opt-in ACTIONS_RUNNER_K8S_EXTERNALS_FROM_IMAGE=true that instead
mounts the externals directly from an image volume (the runner image, from
ACTIONS_RUNNER_IMAGE) read-only at /__e via subPath home/runner/externals,
and skips the fs-init copy entirely. Defaults to off, preserving the current
emptyDir behaviour exactly. Requires the ImageVolume feature gate (default-on
in Kubernetes 1.35+).
@DJRH
DJRH force-pushed the feat/novolume-externals-image-volume branch from 780390d to 40c63c4 Compare July 23, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant