Runners autoalojados de GitHub Actions: cuándo salen las cuentas y cómo no acabar comprometido
Los minutos gestionados dejan de ser baratos hacia los 100.000 al mes. Montar tus propios runners en Kubernetes es sencillo; montarlos de forma segura son tres decisiones concretas.
El motivo para autoalojar runners es casi siempre uno de tres: la factura de los gestionados, compilaciones que necesitan más memoria o una GPU, o jobs que deben alcanzar algo dentro de tu VPC. Cada uno tiene una respuesta distinta, y las implicaciones de seguridad son las mismas en los tres.
Cuándo salen las cuentas
Los minutos Linux gestionados se facturan por minuto a un precio muy difícil de batir con poco volumen: un runner que aprovisionas tú tiene tiempo ocioso, un plano de control y una persona detrás.
El punto de cruce, por nuestra experiencia, está entre 80.000 y 150.000 minutos Linux al mes, y llega mucho antes si:
- Necesitas runners grandes. Los runners grandes gestionados escalan de precio más o menos con el tamaño; una instancia spot del mismo tamaño cuesta una fracción.
- Compilas en macOS. El multiplicador ×10 hace que incluso un poco de macOS sea la mayor partida de la factura. Autoalojar macOS significa Macs físicos o un proveedor de host dedicado, que es trabajo real, pero el ahorro es lo bastante grande como para justificarlo con volumen moderado.
- Tus jobs descargan artefactos grandes. La transferencia y las idas y venidas de caché dentro de tu red son gratis y rápidas; desde runners gestionados no son ninguna de las dos cosas.
Antes de aprovisionar nada, consigue el número: la página de facturación da los minutos por workflow. Multiplica tus cinco workflows principales por su frecuencia y normalmente descubres que el 70 por ciento de la factura viene de uno o dos jobs, y que hacerlos más rápidos sale más barato que mudarlos.
Actions Runner Controller
Para Kubernetes, ARC es el camino soportado. Vigila los jobs en cola y crea un pod de runner efímero por job.
# valores del chart gha-runner-scale-set
githubConfigUrl: https://github.com/acme
githubConfigSecret: arc-github-app
minRunners: 0
maxRunners: 60
containerMode:
type: dind
template:
spec:
nodeSelector: { workload: ci }
tolerations:
- key: ci
operator: Exists
effect: NoSchedule
containers:
- name: runner
image: ghcr.io/actions/actions-runner:2.330.0
resources:
requests: { cpu: "2", memory: "4Gi" }
y los workflows lo eligen por nombre:
jobs:
build:
runs-on: arc-linux-x64
minRunners: 0 más nodos spot con un taint es la configuración que de verdad ahorra dinero: los nodos existen solo mientras hay jobs en cola. Cuenta de 30 a 60 segundos de arranque en frío cuando el grupo de nodos escala desde cero; minRunners: 2 en horario laboral te devuelve ese tiempo al precio de dos pods ociosos.
Autentica ARC con una GitHub App, no con un token personal. Un token personal ata tu flota de CI a la cuenta de una persona y hereda todos sus permisos.
Tres decisiones para no acabar comprometido
1. Efímeros, siempre. Un runner que atiende dos jobs permite que el primero deje algo para el segundo: un ~/.gitconfig modificado, un node_modules envenenado, un proceso en segundo plano leyendo los secretos del siguiente job. Los conjuntos de escalado de ARC son efímeros por defecto. No lo desactives para facilitar la caché; usa una caché remota.
2. Nunca en pull requests de repositorios públicos. Esta es la que muerde a las empresas. Una PR desde un fork puede modificar el workflow y ejecutar código arbitrario en tu infraestructura, dentro de tu red, con lo que conceda el perfil de instancia del nodo. Si un repositorio es público, deja sus builds de PR en runners gestionados. Si tienes que autoalojarlos, hazlo en una cuenta y un clúster aparte, sin ruta de red hacia nada.
3. Sin credenciales de nube ambientales en el nodo. Un pod de runner que alcanza el endpoint de metadatos hereda el rol IAM del nodo, y todos los jobs del clúster lo tienen. Bloquéalo:
# en el grupo de nodos
--http-put-response-hop-limit=1
más una NetworkPolicy que deniegue la salida hacia 169.254.169.254. Los jobs obtienen acceso a la nube mediante federación OIDC con alcance de repositorio y entorno, que es auditable y caduca en una hora.
La misma lógica aplica al clúster: da a los pods de runner su propio espacio de nombres, su propia cuenta de servicio sin permisos sobre la API de Kubernetes y un grupo de nodos que no comparta con nada más. Trata el clúster de CI como no confiable, porque ejecuta código de todos los desarrolladores de la empresa.
Lo que cuesta mantenerlo
Presupuesta: el clúster o grupo de nodos, una imagen de runner mantenida (reconstruida cada semana, con la versión anclada), actualizaciones de ARC cada dos meses y alguien de guardia cuando la flota deja de coger jobs un lunes a las nueve.
Para la mayoría de equipos eso son unas horas al mes una vez estabilizado, y el ahorro es real. Para un equipo con 20.000 minutos gestionados al mes no compensa: paga la factura y dedica el tiempo a otra cosa.