Kubernetes
The container
Section titled “The container”The image is built by apps/runner/Dockerfile, with the repository root as context. The entrypoint is
apps/runner/docker-entrypoint.sh and the command is:
node --import @rootpilot/otel/register apps/runner/dist/index.jsThe image runs as root, which is why Compose mounts ~/.aws at /root/.aws. If your cluster
requires runAsNonRoot, that adaptation is on you, and it has not been done here.
Requirement 1: there is no HTTP probe
Section titled “Requirement 1: there is no HTTP probe”The runner is an outbound client. It listens on no port, exposes no /healthz, and has no
readiness endpoint.
The direct consequence: do not configure httpGet liveness or readiness probes: every one of
them will fail. If you need a signal, it comes from the boot logs (boot: ready) and from the
control-plane’s Fleet view, which sees the runner connected. Per-connector health is also reported in
the handshake, not over HTTP.
Requirement 2: termination grace longer than the drain
Section titled “Requirement 2: termination grace longer than the drain”The runner handles SIGTERM by draining: it stops accepting new work, finishes what’s in flight, and
only then exits. The ceiling is RUNNER_DRAIN_TIMEOUT_MS, which defaults to 120,000 ms.
Kubernetes defaults terminationGracePeriodSeconds to 30 s. Leave it there and SIGKILL arrives
mid-drain.
terminationGracePeriodSeconds: 150 # > RUNNER_DRAIN_TIMEOUT_MS (120s), with headroomA hard reclaim with no drain (SIGKILL, OOM, spot) loses learned state since the last flush.
RUNNER_LEARNED_FLUSH_INTERVAL_MS (default 60 s) exists to bound that loss, but it doesn’t eliminate
it.
Requirement 3: no persistent volume
Section titled “Requirement 3: no persistent volume”The runner is cattle and holds no durable state. The learned-store SQLite cache is :memory: by
default. Do not create a PVC. If you point RUNNER_LEARNED_STORE_PATH at a path on disk, know
that you’re creating a cache, not a source of truth: the source of truth is the control-plane.
Requirement 4: identity with no secret in the image
Section titled “Requirement 4: identity with no secret in the image”This is where Kubernetes changes the design, and for the better.
GKE: gke-oidc
Section titled “GKE: gke-oidc”The runner reads the projected OIDC token of the Kubernetes ServiceAccount and sends it as attestation. Nothing secret enters the image: the token is a short-lived, audience-bound JWT the kubelet projects.
The default path is /var/run/secrets/tokens/gke-oidc/token, configurable via
RUNNER_GKE_TOKEN_PATH. So the pod needs a projected volume with a serviceAccountToken mounted
exactly there.
If the volume isn’t mounted, the error says so explicitly:
RUNNER_ATTESTATION_MODE=gke-oidc: failed to read projected KSA token at/var/run/secrets/tokens/gke-oidc/token (is the Workload Identity token volume mounted?)EKS: aws-sts, not eks-oidc
Section titled “EKS: aws-sts, not eks-oidc”aws-sts signs a GetCallerIdentity request using the SDK’s default credential chain, which on
EKS is IRSA. That means an annotated ServiceAccount with an IAM role is enough, with no bootstrap
token and no human in a browser.
It requires AWS_REGION explicitly. Without it, boot fails with
RUNNER_ATTESTATION_MODE=aws-sts requires AWS_REGION. The region is not guessed on purpose, because
the control-plane only accepts regions from its own allowlist, and a guess would surface as an
attestation_invalid that reads like a credential problem.
Requirement 5: secrets
Section titled “Requirement 5: secrets”Prefer the secret manager you already operate over Kubernetes Secrets:
RUNNER_SECRET_STORE_MODE=aws (AWS Secrets Manager) or vault (HashiCorp Vault KV v2). Both resolve
through workload identity, with no static keys. See Secret stores.
For AWS read credentials, RUNNER_AWS_CRED_MODE=chain makes the SDK resolve through IRSA directly,
with no key passing through the cluster at all.
Requirement 6: egress
Section titled “Requirement 6: egress”Outbound only, nothing inbound. If you use NetworkPolicy, the destination list is in Requirements. Remember that the allowlist declared in connector manifests is documentation, not enforcement: for native connectors, the cluster is what enforces egress.
What’s still missing for a chart to exist
Section titled “What’s still missing for a chart to exist”Being explicit about the gap, since the question will come up: a real chart would need decisions this
repository hasn’t made: runAsNonRoot and moving off /root/.aws, resources values (there is no
versioned benchmark), and a canonical way to express the attestation choice per provider. Until those
exist, writing the manifests on the installer’s side is the honest path.