Skip to content
GantryCD

Kubernetes

At a glance — a self-hosted launcher that runs each ephemeral runner as a Pod (one per run). Use it when you’re already on a cluster and want horizontal scale. There’s a gantrycd-runner-group-kubernetes Helm chart that sets most of this for you.

Configure

On top of the shared runner-group config:

VariableRequiredNotes
KUBERNETES_IMAGEYesephemeral runner image
KUBERNETES_NAMESPACEYesnamespace for runner Pods (the chart sets it from the release namespace)
KUBERNETES_SERVICE_ACCOUNTNoSA for the runner Pod; empty = namespace default
KUBERNETES_MEMORYNomemory request+limit (e.g. 512Mi)
KUBERNETES_CPUNoCPU request+limit (e.g. 500m)
KUBERNETES_CONTAINER_BACKEND_URLNoBACKEND_URL as seen from inside the Pod, if needed
KUBERNETES_STARTUP_GRACE_PERIODNo5m; how long a Pod may stay Pending before it’s force-deleted
KUBERNETES_IMAGE_PULL_SECRETSNocomma-separated Secret names for private-registry pulls
KUBERNETES_IMAGE_PULL_POLICYNoAlways, IfNotPresent or Never for the runner Pods; empty = kubelet default (see below)
KUBERNETES_ALLOW_STACK_IMAGE_OVERRIDENofalse; allow a stack to override the runner image per run (see below)
KUBECONFIGNopath to kubeconfig; empty = in-cluster

The KUBERNETES_* values above are the group-wide defaults applied to every runner Pod.

Image pull policy

The published runner image uses a mutable tag: a release republishes it in place. Nothing about that reaches the cluster — runner Pods are created by the launcher rather than by Helm, so there is no manifest change, no event and no restart to notice. With KUBERNETES_IMAGE_PULL_POLICY unset the kubelet defaults to IfNotPresent for any tag but :latest, which never re-resolves the tag, and each node goes on running whichever digest it cached first — indefinitely, and differently per node.

So the chart ships Always, and that is the right setting whenever ephemeralRunner.image.tag names a tag. The pull is a manifest check against the registry, paid once per run against a run that then executes OpenTofu, and layers are re-downloaded only when the digest actually moved.

Set IfNotPresent when ephemeralRunner.image names an immutable digest (repository: ghcr.io/gantrycd/gantrycd-runner@sha256, tag: <hex>), where the reference cannot go stale and re-checking buys nothing.

To confirm what a Pod actually ran, compare its resolved digest against the registry’s — the tag is no evidence either way:

kubectl -n <runner-namespace> get pod <runner-pod> \
  -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'

Per-stack overrides

A stack can override the image and resource envelope of its own runs with reserved gantrycd:runner-spec: labels. They are snapshotted onto the run at creation, returned in the poll response, and applied by this launcher when it builds the Pod. Any field left unset falls back to the KUBERNETES_* default.

Stack labelOverrides
gantrycd:runner-spec:kubernetes_imagecontainer image
gantrycd:runner-spec:kubernetes_request_cpuCPU request
gantrycd:runner-spec:kubernetes_limit_cpuCPU limit
gantrycd:runner-spec:kubernetes_request_memorymemory request
gantrycd:runner-spec:kubernetes_limit_memorymemory limit

CPU/memory values must be valid Kubernetes quantities (e.g. 500m, 2, 512Mi, 4Gi) — invalid values are rejected when the stack is saved. Request/limit are independent: set only kubernetes_limit_memory to raise the cap while leaving the request at the group default. Mismatched values (request > limit) are rejected by Kubernetes when the Pod is created.

Image override is opt-in and gated. A runner Pod runs under this group’s ServiceAccount — frequently bound to IRSA / cloud credentials — so a stack editor choosing the image is a privilege-escalation vector. kubernetes_image is honored only when the operator sets KUBERNETES_ALLOW_STACK_IMAGE_OVERRIDE=true (ephemeralRunner.allowStackImageOverride in the chart); otherwise it is ignored and a warning is logged. CPU/memory overrides need no opt-in — they are bounded by your namespace ResourceQuota / LimitRange. If you need a different image by default rather than per-stack, prefer a separate runner group (see overview).

How it behaves

  • Each runner’s JWT is stored in a per-run Secret (not a plaintext Pod env value), so the group’s Role needs secrets create/delete alongside its pods verbs.
  • Pending Pods that exceed KUBERNETES_STARTUP_GRACE_PERIOD are force-deleted; finished Pods are reaped on REAP_INTERVAL.

Internals

Configuration → Kubernetes Launcher.