Problema

Muchas organizaciones usan GitHub Actions como motor de CI/CD, pero los runners gestionados por GitHub tienen tres limitaciones que aparecen con frecuencia:

  1. Coste por minuto que supera al de alternativas serverless cuando el flujo de trabajo es intensivo.
  2. Límite de tiempo (6 h) que impide jobs de larga duración, como compilaciones de firmware o pruebas de integración extensas.
  3. Compromiso de facturación de 60 s, lo que penaliza pipelines con etapas cortas y alta concurrencia.

El patrón típico es: un equipo necesita ejecutar jobs de forma esporádica, con diferentes arquitecturas (ARM64 vs x86) y sin mantener infraestructura permanente. La solución ideal sería un runner que aparezca sólo cuando hay trabajo, se apague inmediatamente después y cueste lo menos posible.

Causa

Los runners gestionados están diseñados para ofrecer una experiencia “plug‑and‑play” en máquinas virtuales tradicionales. Sus limitaciones provienen de:

  • Modelo de facturación fijo: AWS Lambda y otras plataformas serverless facturan por milisegundo, mientras que los runners de GitHub usan un modelo de bloque de 60 s.
  • Arquitectura predefinida: Los runners oficiales solo están disponibles en x86_64, lo que impide aprovechar hardware ARM64 más barato.
  • Escalado estático: Los runners se provisionan en grupos de autoscaling que pueden permanecer inactivos, generando costos de reserva.
  • Restricciones de tiempo: La política de 6 h es una medida de seguridad para evitar procesos colgados, pero no se adapta a jobs que necesitan más tiempo.

Solución

Una arquitectura basada en Lambda MicroVMs (AWS Graviton2 ARM64) permite crear runners “just‑in‑time” (JIT) que cumplen con los requisitos anteriores. El flujo general es:

  1. GitHub App: crea una aplicación con permisos actions:write y metadata:read. Genera un private key para firmar JWT.
  2. IAM Role: concede a la función Lambda permiso para lanzar MicroVMs, leer secretos de Parameter Store y escribir logs en CloudWatch.
  3. Lambda Function (dispatcher): recibe el webhook workflow_job de GitHub, valida el JWT, y lanza una MicroVM con una imagen Docker que contiene el agente de runner.
  4. Terraform module: encapsula toda la infraestructura (App, Role, Lambda, VPC, Security Groups, SSM parameters). Un solo terraform apply crea el entorno listo para usar.
  5. MicroVM lifecycle: la VM arranca en < 5 s, registra el runner mediante la API de GitHub, ejecuta el job y, al terminar, se deregistra y se apaga automáticamente.

Pasos clave

  • Crear la App y exportar APP_ID, CLIENT_ID, CLIENT_SECRET y la clave privada.
  • Definir variables en SSM (/github/runner/app_id, /github/runner/private_key).
  • Configurar Terraform con los módulos aws_lambda_function, aws_iam_role, aws_ec2_instance (tipo t4g.micro bajo la capa de MicroVM) y aws_cloudwatch_event_rule para el webhook.
  • Construir la imagen Docker basada en ubuntu:22.04 con el binario actions-runner y los scripts de registro/deregistro.
  • Desplegar con terraform init && terraform apply.

Cuándo aplicar esta solución

  • Jobs con alta variabilidad de duración (desde segundos hasta > 6 h) donde el límite de los runners gestionados es un cuello de botella.
  • Entornos que pueden usar ARM64 (compilaciones cruzadas, pruebas de contenedores multi‑arch) y buscan reducir el coste de cómputo.
  • Equipos que requieren aislamiento: cada job se ejecuta en su propia MicroVM, evitando contaminación de estado.
  • Escenarios con bajo nivel de concurrencia: la arquitectura paga solo por el tiempo de ejecución real, ideal para pipelines que se disparan esporádicamente.

No aplicar cuando:

  • Se necesita soporte nativo de Windows o GPU, ya que las MicroVMs actuales solo ofrecen ARM64 Linux.
  • La política de la empresa prohíbe ejecutar código arbitrario dentro de una función Lambda por motivos de compliance.

Código

# variables.tf
variable "github_app_id" {}
variable "github_private_key" {}
variable "region" {
  default = "us-east-1"
}

# main.tf (simplificado)
resource "aws_iam_role" "lambda_role" {
  name = "github-runner-dispatcher"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

resource "aws_lambda_function" "dispatcher" {
  function_name = "github-runner-dispatcher"
  runtime       = "python3.11"
  handler       = "handler.main"
  role          = aws_iam_role.lambda_role.arn
  filename      = "dispatcher.zip"
  environment {
    variables = {
      GITHUB_APP_ID      = var.github_app_id
      GITHUB_PRIVATE_KEY = var.github_private_key
    }
  }
}

resource "aws_cloudwatch_event_rule" "github_webhook" {
  name        = "github-webhook"
  event_pattern = jsonencode({
    "source": ["aws.lambda"],
    "detail-type": ["GitHub Workflow Job"]
  })
}

resource "aws_cloudwatch_event_target" "lambda_target" {
  rule      = aws_cloudwatch_event_rule.github_webhook.name
  arn       = aws_lambda_function.dispatcher.arn
}

Verificación

  1. Webhook activo: revisa en la configuración de la GitHub App que la URL apunte a la API Gateway que expone la Lambda.
  2. Runner registrado: en la página de Settings → Actions → Runners debería aparecer un runner “online” justo después de disparar un workflow.
  3. Job completado: verifica que el job termina sin errores y que en CloudWatch Logs aparecen los mensajes “Runner registered” y “Runner deregistered”.
  4. Facturación: en la consola de AWS revisa el uso de MicroVMs; debería coincidir con la duración del job (pocos segundos a minutos).

Notas adicionales

  • ARM64 compatibility: la mayoría de los pasos de CI funcionan sin cambios, pero algunos binarios precompilados solo existen para x86_64. Usa docker buildx para crear imágenes multi‑arch.
  • Límites de Lambda: la función dispatcher tiene un timeout máximo de 15 min; si el job supera ese tiempo, la Lambda solo lanza la MicroVM y termina, delegando la gestión al propio runner.
  • Seguridad: almacena la clave privada en AWS Secrets Manager con rotación automática y limita el acceso del rol Lambda a ssm:GetParameter y secretsmanager:GetSecretValue.
  • Escalado concurrente: si varios jobs llegan al mismo tiempo, la Lambda puede lanzar varias MicroVMs en paralelo; ajusta el límite de instancias en la VPC para evitar sobresaturación.
  • Limpieza: la imagen Docker incluye un script cleanup.sh que elimina artefactos temporales y revoca el token de runner antes de apagar la VM. Esto evita fugas de credenciales.