Actions runner controller¶
Purpose¶
Actions-runner-controller (ARC) is the gha-runner-scale-set-controller chart: one controller
Deployment in actions-runner-system that every runner scale set's controllerServiceAccount
points at. Two Flux Kustomizations apply it. actions-runner-controller renders app/, and
actions-runner-controller-runners, which dependsOn the first, renders runners/: one directory
per GitHub org or repository scale set.
Fourteen scale sets exist, each a self-hosted runner pool for one GitHub target. The scale set name
matches its runners/ directory name except for packer, whose runnerScaleSetName is
packer-vsphere:
| Scale set | Target | Mode | maxRunners |
Work storage |
|---|---|---|---|---|
codelooks-org |
codelooks-com (org, shared pool) | dind | 2 | 40Gi |
lgwebos |
LukeEvansTech/lgwebos | dind | 2 | 40Gi |
loupe |
LukeEvansTech/loupe | dind | 2 | 40Gi |
lurcher |
LukeEvansTech/lurcher | dind | 3 | 40Gi |
networkops |
LukeEvansTech/network-ops | dind | 3 | 40Gi |
nixconfig |
LukeEvansTech/nix-config | dind | 2 | 40Gi |
nut |
LukeEvansTech/nut-apps | dind | 3 | 40Gi |
packer-vsphere |
codelooks-com/packer-vsphere | kubernetes | 3 | 40Gi |
seedbox |
LukeEvansTech/seedbox-apps | dind | 3 | 40Gi |
subspy |
LukeEvansTech/subspy | dind | 3 | 40Gi |
talos-cluster |
LukeEvansTech/talos-cluster | dind | 3 | 25Gi |
terraform-vsphere |
codelooks-com/terraform-vsphere | kubernetes | 2 | 5Gi |
truenas |
LukeEvansTech/truenas-apps-cr | dind | 3 | 40Gi |
truenas-br |
LukeEvansTech/truenas-apps-br | dind | 1 | 40Gi |
The repository pools (lurcher, networkops, nut, seedbox, subspy, truenas) started at
maxRunners: 1 and have since grown to 3; lgwebos, loupe and nixconfig start at 2 because
each change runs two jobs at once. truenas-br (2026-10-22) starts at 1: its target repository runs only lint. nixconfig also raises its runner memory limit to 8Gi for
evaluating both darwin systems. codelooks-org, the
shared pool, starts at 2 for the same reason. talos-cluster, packer-vsphere and
terraform-vsphere predate that pilot convention and already run at higher ceilings.
GitHub App authentication¶
Each scale set's ExternalSecret extracts a GitHub App's credentials from one of two 1Password
items: github for the eleven LukeEvansTech-scoped runners, github-codelooks for the three
codelooks-com-scoped ones (codelooks-org, packer, terraform-vsphere).
codelooks-org is org-scoped rather than repository-scoped, and that changes what permission the
GitHub App needs. An org-scoped scale set needs the app to hold the organisation permission
"Self-hosted runners: Read and write" (#4410); repository-scoped registration, which is all the
other scale sets have needed, only needs the repository-level one. If the organisation permission
is missing, the listener fails to register the scale set, and the error surfaces in the listener
pod rather than in the ExternalSecret.
fsGroup and runner storage¶
All fourteen scale sets mount their _work volume from miroir-local, a real ext4 block device whose
root directory comes up root:root 0755. The runner images all run as uid/gid 1001, so every pod
spec sets securityContext.fsGroup: 1001. Without it, the runner cannot create _work/_tool, and
every job dies within about two seconds with UnauthorizedAccessException before a single step
runs. miroir-local's CSIDriver reports fsGroupPolicy: ReadWriteOnceWithFSType, and the claim is
ReadWriteOnce plus ext4, so kubelet honours fsGroup and chowns the volume on mount.
This broke all three scale sets that existed when their work volumes moved from openebs-hostpath
(an already-writable host directory) to miroir-local in #3583. The fix landed in #3739.
talos-cluster failed the same way but silently: its only privileged job, image pre-pull, kept
skipping for unrelated reasons, so nothing exercised _work/_tool until after the fix had already
merged.
The dind-mode scale sets that build container images size their work claim at 40Gi, roomier than
talos-cluster's 25Gi, because the super-linter image alone is several gigabytes and checkov
and trivy add more. That claim never holds dind's own image store, which lives on the sidecar's
writable layer (node ephemeral storage) instead. Spegel mirrors containerd pulls cluster-wide but
not dind's, so every dind job pulls super-linter from ghcr.io fresh. terraform-vsphere's
kubernetes-mode claim only needs 5Gi: Terraform's own footprint is remote state plus a handful of
small providers.
Docker sidecar resources¶
The gha-runner-scale-set chart's dind mode generates a Docker sidecar container with no values
field to size it. Eleven of the twelve dind-mode scale sets (codelooks-org, lgwebos,
loupe, lurcher, networkops, nixconfig, nut, seedbox, subspy, truenas, truenas-br) carry a postRenderers Kustomize patch that
adds resource requests and limits to that sidecar after the chart renders. talos-cluster, the
oldest scale set in the fleet, carries no such patch and sets its runner environment directly
instead (ACTIONS_RUNNER_CONTAINER_HOOKS, ACTIONS_RUNNER_POD_NAME) rather than relying on the
chart's default dind wiring the way the newer scale sets do.
Kubernetes-mode runners: no pod-management RBAC¶
packer-vsphere and terraform-vsphere run in containerMode: kubernetes, whose chart normally
auto-creates a permissive ServiceAccount that lets container: jobs, service containers and
docker:// actions run. Both scale sets instead point serviceAccountName at a ServiceAccount
with no pod-management Role, which suppresses that auto-created one, so those three step types
fail; plain run: steps and non-container actions such as JavaScript or composite actions still
execute normally, since they run in the runner's own process rather than a separate pod. Both also
override ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER from kubernetes mode's default of true to
false, because their steps run directly on the runner. terraform-vsphere needs no Kubernetes
permissions at all: its steps only talk to vCenter and Cloudflare R2.
talos-cluster: os:admin access and its guardrail¶
talos-cluster is the one scale set with real cluster privilege. Its rbac.yaml mints a
talos.dev ServiceAccount with the os:admin role into a Secret also named
talos-cluster-runner, mounted at ~/.talos/config under key config. The runner pods
themselves carry no Kubernetes RBAC, because their only privileged job runs talosctl image pull,
never kubectl.
This wiring has broken once already. Pointing the volume at the similarly named
talos-cluster-runner-secret (the GitHub App credentials, which carry no talosconfig key) with
subPath: talosconfig rendered an empty directory instead of a file, and talosctl failed with
"is a directory" on every image-pull run until #4329 fixed it. Because this runner holds
os:admin, the workflow that uses it triggers on push to main, never pull_request, so
untrusted PR code is never scheduled onto it.
The minted talosconfig names only the in-cluster talos.default endpoint, never nodes, so the
image-pull job lists its targets at run time with talosctl get members and masks each address
before talosctl prints it. That query passes --nodes 127.0.0.1, because talosctl get refuses
to run without a node: apid on whichever node the endpoint reached dials its own loopback and
answers for itself. No node address lives in the workflow, and adding a node needs no workflow
change.
packer: pinned to a digest¶
packer's runner image is codelooks-com/packer-runner, built by that private repository rather
than by home-operations. It is pinned to an immutable digest rather than :latest: a mutable tag
once let a node keep a stale cached image whose baked Ansible virtualenv was missing pywinrm,
which failed Windows template builds with "No module named 'winrm'" (#3034). Renovate bumps the
digest as new images are built.
Secret templating conventions¶
terraform-vsphere and packer each carry a second ExternalSecret (terraform-vsphere-creds,
packer-vsphere-creds) that surfaces vSphere and backend credentials as the environment variables
their own tool reads natively: TF_VAR_<name> for Terraform, PKR_VAR_<name> for Packer. Both
templates carry one literal, non-1Password field, TF_VAR_vsphere_unverified_ssl and
PKR_VAR_vsphere_insecure_connection respectively, both "true", because vCenter presents a VMCA
certificate neither pod trusts. terraform-vsphere-creds also carries the Cloudflare R2 access
keys the OpenTofu S3 backend uses for remote state.