OpenMCF logoOpenMCF

Loading...

Kubernetes GHA Runner Scale Set Controller

Deploys the GitHub Actions Runner Scale Set Controller on Kubernetes using the official OCI Helm chart from ghcr.io/actions/actions-runner-controller-charts. The controller manages AutoScalingRunnerSet and EphemeralRunner custom resources, enabling self-hosted GitHub Actions runners that scale dynamically based on workflow demand. This component installs only the controller; runner scale sets (the actual runner pods) are deployed separately.

What Gets Created

When you deploy a KubernetesGhaRunnerScaleSetController resource, OpenMCF provisions:

  • Kubernetes Namespace — created if createNamespace is true
  • Helm Release — the gha-runner-scale-set-controller OCI chart (default version 0.13.1) from ghcr.io/actions/actions-runner-controller-charts, which creates:
    • A controller Deployment that watches for AutoScalingRunnerSet resources and manages EphemeralRunner pods
    • A ServiceAccount for the controller
    • CRD management for AutoScalingRunnerSet and EphemeralRunner resource types
    • Leader election support when running multiple replicas
    • Metrics endpoints (when metrics configuration is provided)

Prerequisites

  • A Kubernetes cluster with kubectl configured for access
  • Helm 3 available (the module uses an OCI-based Helm chart)
  • A GitHub App or PAT configured for runner registration (required when deploying runner scale sets, not for the controller itself)

Quick Start

Create a file gha-controller.yaml:

apiVersion: kubernetes.openmcf.org/v1
kind: KubernetesGhaRunnerScaleSetController
metadata:
  name: arc-controller
  labels:
    openmcf.org/provisioner: pulumi
    pulumi.openmcf.org/organization: my-org
    pulumi.openmcf.org/project: my-project
    pulumi.openmcf.org/stack.name: dev.KubernetesGhaRunnerScaleSetController.arc-controller
spec:
  namespace:
    value: arc-system
  createNamespace: true
  container:
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"

Deploy:

openmcf apply -f gha-controller.yaml

This installs the controller in the arc-system namespace with default resource limits. Once running, you can deploy AutoScalingRunnerSet resources in any namespace to register self-hosted GitHub Actions runners.

Configuration Reference

Required Fields

FieldTypeDescriptionValidation
namespaceStringValueOrRefKubernetes namespace where the controller will be installed. Use value for a direct string or valueFrom to reference a KubernetesNamespace resource.Required
containerobjectContainer specifications for the controller pod, including CPU/memory resources.Required

Optional Fields

FieldTypeDefaultDescription
createNamespaceboolfalseCreate the namespace if it does not exist.
helmChartVersionstring"0.13.1"Version of the Helm chart to deploy. Chart versions match controller image versions. See releases.
replicaCountint321Number of controller replicas. Leader election is automatically enabled when greater than 1.
container.resources.requests.cpustring"100m"CPU request for the controller container.
container.resources.requests.memorystring"128Mi"Memory request for the controller container.
container.resources.limits.cpustring"500m"CPU limit for the controller container.
container.resources.limits.memorystring"512Mi"Memory limit for the controller container.
container.image.repositorystring"ghcr.io/actions/gha-runner-scale-set-controller"Custom container image repository.
container.image.tagstringchart appVersionCustom image tag.
container.image.pullPolicystring--Image pull policy: Always, IfNotPresent, or Never.
flags.logLevelenum"debug"Log level for the controller. Valid values: debug, info, warn, error.
flags.logFormatenum"text"Log format. Valid values: text, json.
flags.watchSingleNamespacestring--Restrict the controller to watch only the specified namespace. By default, watches all namespaces.
flags.runnerMaxConcurrentReconcilesint322Maximum concurrent reconciles for the EphemeralRunner controller.
flags.updateStrategyenum"immediate"How upgrades are handled while jobs are running. immediate applies changes right away (may cause overprovisioning). eventual waits for running jobs to complete.
flags.excludeLabelPropagationPrefixesstring[][]Label prefixes to exclude from propagation to internal resources (e.g., ArgoCD labels).
flags.k8sClientRateLimiterQpsint320Kubernetes API client rate limiter QPS.
flags.k8sClientRateLimiterBurstint320Kubernetes API client rate limiter burst.
metrics.controllerManagerAddrstring--Metrics address for the controller manager (e.g., ":8080"). Providing this value enables metrics.
metrics.listenerAddrstring--Metrics address for the listener (e.g., ":8080").
metrics.listenerEndpointstring--Metrics endpoint path for the listener (e.g., "/metrics").
imagePullSecretsstring[][]Image pull secrets for private container registries. Also passed to the auto-scaler for pulling listener images.
priorityClassNamestring--Priority class name for controller pods. Use "system-cluster-critical" to ensure the controller survives resource pressure.

Examples

Minimal Controller with Defaults

Deploy the controller with default settings in a dedicated namespace:

apiVersion: kubernetes.openmcf.org/v1
kind: KubernetesGhaRunnerScaleSetController
metadata:
  name: arc-controller
  labels:
    openmcf.org/provisioner: pulumi
    pulumi.openmcf.org/organization: my-org
    pulumi.openmcf.org/project: my-project
    pulumi.openmcf.org/stack.name: dev.KubernetesGhaRunnerScaleSetController.arc-controller
spec:
  namespace:
    value: arc-system
  createNamespace: true
  container:
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"

High-Availability with Structured Logging

Run multiple replicas for high availability and configure JSON logging for structured log aggregation:

apiVersion: kubernetes.openmcf.org/v1
kind: KubernetesGhaRunnerScaleSetController
metadata:
  name: arc-controller-ha
  labels:
    openmcf.org/provisioner: pulumi
    pulumi.openmcf.org/organization: my-org
    pulumi.openmcf.org/project: my-project
    pulumi.openmcf.org/stack.name: staging.KubernetesGhaRunnerScaleSetController.arc-controller-ha
spec:
  namespace:
    value: arc-system
  createNamespace: true
  helmChartVersion: "0.13.1"
  replicaCount: 3
  container:
    resources:
      requests:
        cpu: "250m"
        memory: "256Mi"
      limits:
        cpu: "1000m"
        memory: "1Gi"
  flags:
    logLevel: info
    logFormat: json
    updateStrategy: eventual
    runnerMaxConcurrentReconciles: 5
  priorityClassName: system-cluster-critical

Production with Metrics, Private Registry, and Namespace Scoping

Full production configuration with metrics enabled, a private container registry, and the controller scoped to a single namespace:

apiVersion: kubernetes.openmcf.org/v1
kind: KubernetesGhaRunnerScaleSetController
metadata:
  name: arc-controller-prod
  labels:
    openmcf.org/provisioner: pulumi
    pulumi.openmcf.org/organization: my-org
    pulumi.openmcf.org/project: my-project
    pulumi.openmcf.org/stack.name: prod.KubernetesGhaRunnerScaleSetController.arc-controller-prod
spec:
  namespace:
    valueFrom:
      kind: KubernetesNamespace
      name: arc-system-prod
      fieldPath: spec.name
  container:
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"
      limits:
        cpu: "2000m"
        memory: "2Gi"
    image:
      repository: registry.internal.example.com/actions/gha-runner-scale-set-controller
      tag: "0.13.1"
      pullPolicy: IfNotPresent
  replicaCount: 3
  flags:
    logLevel: info
    logFormat: json
    watchSingleNamespace: runners-prod
    updateStrategy: eventual
    runnerMaxConcurrentReconciles: 10
    excludeLabelPropagationPrefixes:
      - argocd.argoproj.io/
      - app.kubernetes.io/managed-by
    k8sClientRateLimiterQps: 50
    k8sClientRateLimiterBurst: 100
  metrics:
    controllerManagerAddr: ":8080"
    listenerAddr: ":8080"
    listenerEndpoint: "/metrics"
  imagePullSecrets:
    - registry-credentials
  priorityClassName: system-cluster-critical

Stack Outputs

After deployment, the following outputs are available in status.outputs:

OutputTypeDescription
namespacestringNamespace where the controller is deployed
releaseNamestringName of the Helm release
chartVersionstringVersion of the deployed Helm chart
deploymentNamestringName of the controller Deployment
serviceAccountNamestringName of the controller ServiceAccount
metricsEndpointstringController metrics endpoint in <service>.<namespace>.svc.cluster.local:<port> format (only present when metrics are enabled)

Related Components

  • KubernetesNamespace — pre-create a namespace to reference via valueFrom
  • KubernetesHelmRelease — deploy additional Helm charts, such as the gha-runner-scale-set chart for runner pods
  • KubernetesDeployment — deploy custom workloads alongside the controller
  • KubernetesSecret — manage secrets for GitHub App credentials or PAT tokens used by runner scale sets

Next article

Kubernetes Gitlab

Kubernetes Gitlab Deploys a GitLab instance on Kubernetes with a ClusterIP Service, optional namespace creation, configurable container resources, and optional Ingress with TLS termination via cert-manager and Istio. What Gets Created When you deploy a KubernetesGitlab resource, OpenMCF provisions: Namespace — created only when createNamespace is true ClusterIP Service — exposes GitLab on port 80 (targeting container port 8080) with app-level selectors derived from the resource metadata Ingress...
Read next article
Presets
1 ready-to-deploy configurationView presets →