
GitHub Runners en Kubernetes
Hoy me animo a compartir algo con lo que trabajo bastante a menudo: GitHub Runners en Kubernetes. Es lo que comúnmente se conoce como Self-hosted Runners y que nosotros nos encargamos de implementar y mantener en nuestra infraestructura propia.
¿Por qué hacerlo si GitHub ya nos ofrece sus propios runners de forma «gratuita»? Precisamente, aquí voy a explicar por qué hay algunas buenas razones para querer hacer esto.
1. Introducción
Si ya vienes usando GitHub Actions desde hace algún tiempo, sabrás que cada uno de los jobs de tu workflow corren en equipos o instancias conocidas como GitHub Runners, los cuales son provistos por el mismo GitHub de manera gratuita bajo ciertas condiciones, las cuales paso a resumir líneas abajo.
Los GitHub Runners son la infraestructura que ejecuta los flujos de trabajo de GitHub Actions. Existen dos tipos principales:
- Los GitHub-hosted runners, que son máquinas virtuales mantenidas por GitHub, y los self-hosted runners, que son máquinas que gestionas tú mismo. Los GitHub-hosted runners son convenientes para la mayoría de los casos de uso, ya que no requieren configuración ni mantenimiento.
- Sin embargo, los Self-hosted runners ofrecen mayor control y personalización, lo que los hace ideales para entornos con requisitos específicos de seguridad, rendimiento o software.
2. ¿Por qué usar GitHub Runners en Kubernetes?
Voy a comentar algunas de las razones más comunes para preferir correr tus propios Self-hosted runners en Kubernetes.
2.1. Costos GitHub-hosted Runners
Esta es el resumen de la capa gratuita de los Runners hospedados por GitHub:

La información es clara: por ejemplo, si usas el plan «GitHub Free» tienes 2 mil minutos de ejecución libre al mes. ¿Es mucho o es poco? Todo depende de la cantidad y naturaleza de tus proyectos.
Pero, una vez superada la cantidad de minutos gratis al mes, los adicionales se cobrarán por minuto, según el tipo de instancia, sistema operativo y otras características adicionales (Ejm: uso de GPUs).
Por ejemplo, aquí los costos de las instancias Linux estándar:

Si tu objetivo es cuidar los costos, entonces aquí ya tienes una buena razón para correr tus propios GitHub Runners en Kubernetes.
2.2. Acceso a redes internas
A menudo, se tiene que correr muchos workflows que interactúan con infraestructura y/o aplicaciones que están en nuestro Data Center o en un espacio privado de nube. Se trata de entornos que no son accesibles desde Internet (o al menos, no deben serlo).
Por ejemplo, puede ser frecuente que tengas workflows que necesiten generar reportes o backups de bases de datos que están en redes internas, inaccesibles desde Internet.
2.3. Personalización
GitHub hace un buen trabajo ofreciendo imágenes Docker de sus GitHub-hosted runners con una lista de software ya instalado. Incluye los paquetes más comúnmente usados para diferentes proyectos como Java, Python, Node.js, PHP, entre otros.
En este enlace puedes encontrar la lista de todas las imágenes disponibles y lo que incluye en cada una de ellas.
Por ejemplo, para Runners ubuntu-latest
, aquí está la lista de paquetes incluidos.
Sin embargo, muchas veces requieres instalar software adicional antes de ejecutar ciertos jobs. Esto es algo que puedes hacer sin problemas con steps previos durante un workflow. Pero, ¿qué tanto tiempo toma instalar y configurar esos paquetes personalizados?
Todo esa instalación repetida una y otra vez le suma tiempo de duración a tus workflows. Y lo que muchos buscan es ejecutar jobs rápidos, sin demora.
En cambio, si corres tus GitHub Runners en Kubernetes, tienes la posibilidad de usar tu propia imagen hecha a medida. Así, cada workflow ejecutado ya no perderá tiempo en instalar paquetes que ya están incluidos dentro de tu imagen personalizada.
2.4. Rendimiento optimizado
Tal vez una de las razones más fuertes para usar tus propios Runners: correr tus workflows en hardware y entornos hechos a medida de tus necesidades.
Los GitHub-hosted runners podrían no adaptarse a necesidades de hardware específico, como grandes requerimientos de CPU y memoria, mayor espacio de disco, acceso a dispositivos locales en tu red, entre otros.
Corriendo tus propios Runners te permite superar esas limitaciones, pues tienes control total del hardware y software subyacente que quizá GitHub podría no dártelo como necesitas.
2.5. Otras razones
Las razones pueden ser múltiples, entre las que se incluyen pero no limitan a:
- Seguridad estricta que impide el uso de entornos compartidos en la nube
- Cumplimiento normativo, para industrias más reguladas
- Control total del entorno con más flexibilidad
3. ¿Y por qué en Kubernetes?
La respuesta es casi una mención a las características que hacen fuerte a Kubernetes:
- Escalabilidad dinámica: aumenta o reduce el número de Runners según la demanda
- Uso eficiente de recursos: puedes correr múltiples Runners (Pods) en un único nodo. Por ejemplo, si corres tus runners en Amazon EKS, sumado con Karpenter, podrías usar instancias EC de tipo Spot y On Demand en una buena combinación para crear hardware subyacente para tus runners con una eficiencia económica.
- Gestión centralizada y simplificada: una vez instalados los Runners, éstos se suman a tu lista de Pods en tus clusters que ya administras de forma estandarizada.
Entre otras buenas razones están la Alta Disponibilidad, el Aislamiento y Seguridad, así como también la Portabilidad.
En este punto, creo ya haberte dado más que razones suficientes para correr tus propios GitHub Runners en Kubernetes.
4. Instalación y configuración
4.1. Información previa
Antes de instalar el software de GitHub Runners, es importante saber que hay dos variantes:
- Legacy, basada en el espacio de nombres
actions.summerwind.net
- Vigente, basada en
Autoscaling Runner Scale Sets
El sitio GitHub del Actions Runner Controller (ARC) incluye la documentación del Legacy, pero el que explicaré a continuación es el Vigente.
Ya que hablé de ARC, debes saber que este es un Operador de Kubernetes que orquesta y escala los runners Self-hosted para GitHub Actions.
Instalar GitHub Runners en Kubernetes es relativamente simple. Básicamente, son estos pasos:
- Crear un token de GitHub
- Instalar el Controlador de Runner Scale Sets
- Instalar una o más instancias de Runner Scale Sets
- Personalizar la imagen Docker para los Runners (Opcional)
Eso es lo que veremos a continuación.
4.1. Paso 1 – Crear un token de GitHub
Debes crear un PAT (Personal Access Token) con los siguientes scopes:
repo
→ (Si trabajas con repositorios privados, obligatorio)admin:org
→ Para administrar runners a nivel de organizaciónadmin:public_key
→ Para administrar claves públicas en runnersadmin:enterprise
→ (Si usas GitHub Enterprise, necesario para runners en toda la empresa)workflow
→ Permite ejecutar workflows de GitHub Actions

Toma nota del token creado, que se usará en el paso 3.
4.3. Paso 2 – Instalación del Controlador
Instala el Controlador es tan fácil como ejecutar este comando:
helm install \ -n github-arc \ --create-namespace \ runner-scale-set-controller \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
Esto debería crear un Deployment
, muy probablemente con un nombre predeterminado como runner-scale-set-controller-gha-rs-controller
Este paso es indispensable y requisito para que puedan correr las instancias de Runner Scale Sets (próximo paso).
4.4. Paso 3 – Instalación de instancia de Runner Scale Sets
Una instancia de Runner Scale Set tiene un nombre propio y gestiona sus propios Runners como Pods, dentro de ciertos límites (cantidad mínima y máxima).
De este modo, es posible tener más de una instancia, cada cual con su propio nombre y sus propias características como se verá más adelante. Cada Team y/o repositorio de GitHub podría decidir utilizar una u otra instancia.
Por ahora, vamos a crear una única instancia llamada self-hosted-default
, como sigue:
helm install \ -n github-arc \ self-hosted-default \ --set minRunners=2 \ --set githubConfigUrl=https://github.com/MiOrganizacion \ --set githubConfigSecret.github_token=<TokenPAT> \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
Debes ajustar estos valores, según tus necesidades:
minRunners
→ Cantidad mínima de instancias que siempre estarán en ejecución. Puede ser 0, inclusive, y cuando existan uno o más Jobs de GitHub Actions en ejecución, se crearán los Runner necesarios a demanda.githubConfigUrl
→ La URL de tu organización en GitHubgithubConfigSecret.github_token
→ El token PAT creado en el paso 1.
Una vez instalada la instancia, se crea un Custom Resource de tipo EphemeralRunnerSet
con un nombre igual a la instancia más un prefijo aleatorio. Lo puedes consultar así:
kubectl -n github-arc get EphemeralRunnerSet
Importante:
- El
EphemeralRunnerSet
funciona como un listener que siempre estará a la escucha de los jobs de GitHub Actions. Cuando detecte uno nuevo, verá si puede atender la petición con un Runner que esté vivo y desocupado, sino procederá a crear un Runner nuevo a demanda.
La instancia se llama igual al nombre del Release de Helm instalado. Es decir, self-hosted-default
. Por ello, los Pods que son parte de dicha instancia se pueden consultar así:
kubectl -n github-arc get pod \ -l actions.github.com/scale-set-name=self-hosted-default
Esto listará tanto el EphemeralRunnerSet
, como los Runners (objetos de tipo EphemeralRunner
).
Por último, si deseas solo consultar los Runners, puedes hacerlo así:
kubectl -n github-arc get ephemeralrunners
Esto mostrará información bastante útil sobre sus nombres, IDs, job de GitHub que lo está usando, estado y una referencia del workflow que lo invoca.
Importante: Si lo necesitas, puedes crear más instancias de Runner Scale Sets repitiendo el comndo Helm, pero usando un nombre de Release distinto.
4.5. Paso 4 – Personalizar la imagen Docker para los Runners (Opcional)
Cuando se instala una instancia de Runner Scale Set como lo mostré en los pasos anteriores, por defecto usará una imagen Docker como esta:
ghcr.io/actions/actions-runner:latest
Esta ya trae el software de GitHub Runner que necesitas. Lo malo es que carece de muchas herramientas que posiblemente necesitarás en tus workflows de GitHub Actions, tales como:
- kubectl
- AWS/Azure CLI
- Terraform/OpenTofu
- Docker
- Etc.
Entonces, ¿debes instalar todas las herramientas necesarias como parte de tu workflow? Pues no. Justamente esa es una de las razones por las cuales aconsejé correr tus propios GitHub Runners en Kubernetes.
La solución pasa por crear tu propia imagen a medida que incluya todas las herramientas que necesitarás con frecuencia. Aquí un ejemplo del Dockerfile
de esta imagen personalizada.
FROM ubuntu:24.04 ARG KUBECTL_VERSION="1.30.0" ARG GITHUB_CLI_VERSION="2.62.0" ARG GITHUB_RUNNER_VERSION="2.322.0" ARG YQ_VERSION="4.45.1" ARG KUBERGRUNT_VERSION="0.16.0" ARG TOFU_VERSION="1.6.2" ENV DEBIAN_FRONTEND=noninteractive # Instalar dependencias base RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends curl tzdata ca-certificates jq wget zip ssh python3-git \ git unzip sudo lsb-release gpg-agent software-properties-common python3-setuptools python-is-python3 && \ apt-get clean && rm -rf /var/lib/apt/lists/* # Instalar OpenTofu RUN curl -Lo /tmp/tofu.zip https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_amd64.zip && \ unzip /tmp/tofu.zip tofu && \ install tofu /usr/bin && \ rm -f /tmp/tofu.zip # Instalar yq RUN curl -Lo /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 && \ chmod +x /usr/bin/yq # Instalar AWS CLI RUN curl -Lo /tmp/awscliv2.zip https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip && \ unzip -q /tmp/awscliv2.zip -d /tmp && \ /tmp/aws/install -b /usr/bin && \ rm -rf /tmp/aws /tmp/awscliv2.zip # Instalar Docker RUN install -m 0755 -d /etc/apt/keyrings && \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \ chmod a+r /etc/apt/keyrings/docker.asc && \ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list && \ apt-get update && \ apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin && rm -rf /var/lib/apt/lists/* # Install kubectl RUN curl -Lo /usr/bin/kubectl "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \ chmod +x /usr/bin/kubectl # Instalar GitHub CLI RUN curl -Lo tarball.tar.gz https://github.com/cli/cli/releases/download/v${GITHUB_CLI_VERSION}/gh_${GITHUB_CLI_VERSION}_linux_amd64.tar.gz && \ tar -xzf tarball.tar.gz -C /usr/bin --strip-components=2 gh_${GITHUB_CLI_VERSION}_linux_amd64/bin/gh && \ chmod +x /usr/bin/gh && \ rm -f tarball.tar.gz RUN adduser --disabled-password --gecos "" --uid 1001 runner \ && usermod -aG sudo -s /bin/bash runner \ && usermod -aG docker runner \ && echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \ && echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers WORKDIR /home/runner # Instalar GitHub runner RUN curl -Lo tarball.tar.gz https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-x64-${GITHUB_RUNNER_VERSION}.tar.gz && \ tar -zxf tarball.tar.gz && \ rm -f tarball.tar.gz USER runner
Importante: Observa que estoy incluyendo la instalación del software de GitHub Runner (el cual es indispensable). Opcionalmente, también estoy incluyendo el GitHub CLI, pues podría requerirlo en alguno de mis workflows.
Digamos que esta imagen personalizada la construí y le apliqué un tag como mi-cuenta-docker/github-runner:1.0.0
. De ser así, ahora debo crear un archivo values.yaml con las opciones personalizadas para la reinstalación de la instancia del Runner Scale Sets con Helm.
Este debería ser el contenido:
# values.yaml --- minRunners: 2 githubConfigSecret: github_token: <TokenPAT> githubConfigUrl: https://github.com/MiOrganizacion template: spec: initContainers: - name: init-dind-externals image: ghcr.io/actions/actions-runner:latest command: ["cp", "-r", "-v", "/home/runner/externals/.", "/home/runner/tmpDir/"] volumeMounts: - name: dind-externals mountPath: /home/runner/tmpDir containers: - name: runner image: mi-cuenta-docker/github-runner:1.0.0 command: ["/home/runner/run.sh"] env: - name: DOCKER_HOST value: unix:///var/run/docker.sock volumeMounts: - name: work mountPath: /home/runner/_work - name: dind-sock mountPath: /var/run - name: dind image: docker:dind args: - dockerd - --host=unix:///var/run/docker.sock - --group=$(DOCKER_GROUP_GID) env: - name: DOCKER_GROUP_GID value: "996" securityContext: privileged: true volumeMounts: - name: work mountPath: /home/runner/_work - name: dind-sock mountPath: /var/run - name: dind-externals mountPath: /home/runner/externals volumes: - name: work emptyDir: {} - name: dind-sock emptyDir: {} - name: dind-externals emptyDir: {}
El contenedor principal de nombre runner es aquel que referencia la imagen Docker personalizada que creamos líneas arriba.
Por último, ahora nos toca actualizar la instalación del Release del Runner Scale Set con Helm de esta manera:
helm upgrade \ -n github-arc \ self-hosted-default \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \ -f values.yaml
Esperamos unos minutos y ya deberíamos tener los nuevos Pods de Runners usando la imagen Docker personalizada.
5. Conclusión
Este fue un post algo extenso, pero ameritaba hacerlo así. Es que no quería perderme ningún detalle sobre las razones para instalar tus propios Runners, la secuencia de instalación (Controlador e instancias de Runner Scale Sets), así como la personalización del imagen que seguramente la necesitarás.
Espero que el post te haya sido de utilidad. No dudes en compartirlo si crees que a alguien más le podría interesar.
¡Hasta la próxima!
Deja un comentario