Alojar runners de GitHub con grupos de trabajadores de Cloud Run

En este tutorial se explica cómo usar ejecutores de GitHub autohospedados en grupos de trabajadores para ejecutar los flujos de trabajo definidos en tu repositorio de GitHub.

Desplegarás un grupo de trabajadores de Cloud Run para gestionar esta carga de trabajo y, opcionalmente, una función de Cloud Run para admitir el escalado del grupo de trabajadores.

Acerca de los runners de GitHub autohospedados

En un flujo de trabajo de GitHub Actions, los runners son las máquinas que ejecutan los trabajos. Por ejemplo, un runner puede clonar tu repositorio de forma local, instalar software de pruebas y, a continuación, ejecutar comandos que evalúen tu código.

Puedes usar ejecutores autohospedados para ejecutar GitHub Actions en instancias de grupos de trabajadores de Cloud Run. En este tutorial se muestra cómo escalar automáticamente un grupo de ejecutores en función del número de tareas en ejecución y sin programar, e incluso cómo reducir el grupo a cero cuando no haya tareas.

Obtener el código de ejemplo

Para obtener el código de muestra que vas a usar, sigue estos pasos:

  1. Clona el repositorio de ejemplo en tu máquina local:

    git clone https://github.com/GoogleCloudPlatform/cloud-run-samples
    
  2. Cambia al directorio que contiene el código de ejemplo de Cloud Run:

    cd cloud-run-samples/github-runner
    

Entender el código principal

El ejemplo se implementa como un grupo de trabajadores y un escalador automático, que se describen a continuación.

Grupo de trabajadores

El grupo de trabajadores se configura con un Dockerfile basado en la imagen actions/runner creada por GitHub.

Toda la lógica está incluida en esta imagen, excepto un pequeño script auxiliar.

FROM ghcr.io/actions/actions-runner:2.329.0

# Add scripts with right permissions.
USER root
# hadolint ignore=DL3045
COPY start.sh start.sh
RUN chmod +x start.sh

# Add start entrypoint with right permissions.
USER runner
ENTRYPOINT ["./start.sh"]

Esta secuencia de comandos auxiliar se ejecuta cuando se inicia el contenedor y se registra en el repositorio configurado como una instancia efímera mediante un token que crearás. La secuencia de comandos también define las acciones que se deben llevar a cabo cuando se reduce el tamaño del contenedor.

# Configure the current runner instance with URL, token and name.
mkdir /home/docker/actions-runner && cd /home/docker/actions-runner
echo "GitHub Repo: ${GITHUB_REPO_URL} for ${RUNNER_PREFIX}-${RUNNER_SUFFIX}"
./config.sh --unattended --url ${GITHUB_REPO_URL} --pat ${GH_TOKEN} --name ${RUNNER_NAME}

# Function to cleanup and remove runner from Github.
cleanup() {
   echo "Removing runner..."
   ./config.sh remove --unattended --pat ${GH_TOKEN}
}

# Trap signals.
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

# Run the runner.
./run.sh & wait $!

Herramienta de adaptación dinámica

El autoescalador es una función que aumenta el tamaño del grupo de trabajadores cuando hay una tarea nueva en la cola o lo reduce cuando se completa una tarea. Usa la API Cloud Run para comprobar el número actual de trabajadores del grupo y ajusta ese valor según sea necesario.

try:
    current_instance_count = get_current_worker_pool_instance_count()
except ValueError as e:
    return f"Could not retrieve instance count: {e}", 500

# Scale Up: If a job is queued and we have available capacity
if action == "queued" and job_status == "queued":
    print(f"Job '{job_name}' is queued.")

    if current_instance_count < MAX_RUNNERS:
        new_instance_count = current_instance_count + 1
        try:
            update_runner_instance_count(new_instance_count)
            print(f"Successfully scaled up to {new_instance_count} instances.")
        except ValueError as e:
            return f"Error scaling up instances: {e}", 500
    else:
        print(f"Max runners ({MAX_RUNNERS}) reached.")

# Scale Down: If a job is completed, check to see if there are any more pending
# or in progress jobs and scale accordingly.
elif action == "completed" and job_status == "completed":
    print(f"Job '{job_name}' completed.")

    current_queued_actions, current_running_actions = get_current_actions()
    current_actions = current_queued_actions + current_running_actions

    if current_queued_actions >= 1:
        print(
            f"GitHub says {current_queued_actions} are still pending."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_queued_actions == 0 and current_running_actions >= 1:
        print(
            f"GitHub says no queued actions, but {current_running_actions} running actions."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_actions == 0:
        print(f"GitHub says no pending actions. Scaling to zero.")
        update_runner_instance_count(0)
        print(f"Successfully scaled down to zero.")
    else:
        print(
            f"Detected an unhandled state: {current_queued_actions=}, {current_running_actions=}"
        )
else:
    print(
        f"Workflow job event for '{job_name}' with action '{action}' and "
        f"status '{job_status}' did not trigger a scaling action."
    )

Configurar IAM

En este tutorial se usa una cuenta de servicio personalizada con los permisos mínimos necesarios para usar los recursos aprovisionados. Para configurar la cuenta de servicio, sigue estos pasos:

  1. Define el ID de tu proyecto en gcloud:

    gcloud config set project PROJECT_ID
    

    Sustituye PROJECT_ID por el ID del proyecto.

  2. Crea una cuenta de servicio de gestión de identidades y accesos:

    gcloud iam service-accounts create gh-runners
    

  3. Concede a la cuenta de servicio permisos para actuar como cuenta de servicio en tu proyecto:

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/iam.serviceAccountUser
    

    Sustituye PROJECT_ID por el ID del proyecto.

Recuperar información de GitHub

En la documentación de GitHub sobre cómo añadir runners autohospedados se sugiere añadir runners a través del sitio web de GitHub, que proporciona un token específico para la autenticación.

En este tutorial se añadirán y quitarán runners de forma dinámica, y para ello se necesita un token de GitHub estático.

Para completar este tutorial, debes crear un token de GitHub con acceso para interactuar con el repositorio seleccionado.

Identificar el repositorio de GitHub

En este tutorial, la variable GITHUB_REPO representa el nombre del repositorio. Es la parte del nombre del repositorio de GitHub que va después del nombre de dominio, tanto en los repositorios de usuarios personales como en los de organizaciones.

En ambos casos, harás referencia al nombre del repositorio que aparece después del nombre de dominio.

En este tutorial:

  • En el caso de https://github.com/myuser/myrepo, el GITHUB_REPO es myuser/myrepo.
  • En el caso de https://github.com/mycompany/ourrepo, el GITHUB_REPO es mycompany/ourrepo.

Crear token de acceso

Debes crear un token de acceso en GitHub y guardarlo de forma segura en Secret Manager:

  1. Asegúrate de haber iniciado sesión en tu cuenta de GitHub.
  2. Ve a la página Settings > Developer Settings > Personal Access Tokens (Configuración > Configuración de desarrollador > Tokens de acceso personal) de GitHub.
  3. Haz clic en Generar token nuevo y selecciona Generar token nuevo (clásico).
  4. Crea un token con el ámbito "repo".
  5. Haz clic en Generate token (Generar token).
  6. Copia el token generado.

Crear valor de secreto

Toma el token secreto que acabas de crear, almacénalo en Secret Manager y define los permisos de acceso.

  1. Crea el secreto en Secret Manager:

    echo -n "GITHUB_TOKEN" | gcloud secrets create github_runner_token --data-file=-
    

    Sustituye GITHUB_TOKEN por el valor que has copiado de GitHub.

  2. Concede acceso al secreto que acabas de crear:

    gcloud secrets add-iam-policy-binding github_runner_token \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

Desplegar grupo de trabajadores

Crea un grupo de trabajadores de Cloud Run para procesar acciones de GitHub. Este pool usará una imagen basada en la imagen actions/runner creada por GitHub.

Configurar un grupo de trabajadores de Cloud Run

  1. Ve al código de ejemplo del grupo de trabajadores:

    cd worker-pool-container
    
  2. Implementa el grupo de trabajadores:

    gcloud beta run worker-pools deploy WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --source . \
      --scaling 1 \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --memory 2Gi \
      --cpu 4
    

    Haz los cambios siguientes:

    Si es la primera vez que usas las implementaciones de origen de Cloud Run en este proyecto, se te pedirá que crees un repositorio predeterminado de Artifact Registry.

Usar un grupo de trabajadores

Ahora tienes una sola instancia en tu grupo de trabajadores, lista para aceptar trabajos de acciones de GitHub.

Para verificar que has completado la configuración de tu runner autohospedado, invoca una acción de GitHub en tu repositorio.

Para que tu acción use tus runners autohospedados, debes cambiar el trabajo de una acción de GitHub. En el trabajo, cambia el valor de runs-on a self-hosted.

Si tu repositorio aún no tiene ninguna acción, puedes seguir la guía de inicio rápido de GitHub Actions.

Una vez que hayas configurado una acción para usar los runners autohospedados, ejecútala.

Confirma que la acción se ha completado correctamente en la interfaz de GitHub.

Desplegar el escalado automático de GitHub Runner

Has desplegado un trabajador en tu grupo original, lo que permitirá procesar una acción a la vez. En función del uso que hagas de la integración continua, es posible que tengas que escalar tu grupo para gestionar un aumento del trabajo que se debe realizar.

Una vez que hayas desplegado el grupo de trabajadores con un runner de GitHub activo, configura el escalador automático para que aprovisione instancias de trabajador en función del estado de las tareas en la cola de acciones.

Esta implementación procesa un evento workflow_job. Cuando se cree un trabajo de flujo de trabajo, se aumentará el tamaño del grupo de trabajadores y, una vez que se haya completado el trabajo, se reducirá de nuevo. No escalará el grupo más allá del número máximo de instancias configurado y se reducirá a cero cuando se hayan completado todos los trabajos en ejecución.

Puedes adaptar esta herramienta de escalado automático en función de tus cargas de trabajo.

Crear un valor secreto de webhook

Para crear un valor secreto para la webhook, sigue estos pasos:

  1. Crea un secreto de Secret Manager que contenga un valor de cadena arbitrario.

    echo -n "WEBHOOK_SECRET" | gcloud secrets create github_webhook_secret --data-file=-
    

    Sustituye WEBHOOK_SECRET por un valor de cadena arbitrario.

  2. Concede acceso al secreto a la cuenta de servicio del escalador automático:

    gcloud secrets add-iam-policy-binding github_webhook_secret \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

Desplegar la función para recibir solicitudes de webhook

Para implementar la función que recibe solicitudes de webhook, haz lo siguiente:

  1. Ve al código de ejemplo del webhook:

    cd ../autoscaler
    
  2. Despliega la función de Cloud Run:

    gcloud run deploy github-runner-autoscaler \
      --function github_webhook_handler \
      --region WORKER_POOL_LOCATION \
      --source . \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-env-vars WORKER_POOL_NAME=WORKER_POOL_NAME \
      --set-env-vars WORKER_POOL_LOCATION=WORKER_POOL_LOCATION \
      --set-env-vars MAX_RUNNERS=5 \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --set-secrets WEBHOOK_SECRET=github_webhook_secret:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --allow-unauthenticated
    

    Haz los cambios siguientes:

    • GITHUB_REPO la parte del nombre de tu repositorio de GitHub que va después del nombre de dominio
    • WORKER_POOL_NAME el nombre del grupo de trabajadores
    • WORKER_POOL_LOCATION la región del grupo de trabajadores
    • REPOSITORY_NAME el nombre del repositorio de GitHub
  3. Anota la URL en la que se ha implementado tu servicio. Usarás este valor en un paso posterior.

  4. Concede a la cuenta de servicio permisos para actualizar tu grupo de trabajadores:

    gcloud alpha run worker-pools add-iam-policy-binding WORKER_POOL_NAME \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/run.developer
    

    Sustituye PROJECT_ID por el ID del proyecto.

Crear un webhook de GitHub

Para crear el webhook de GitHub, sigue estos pasos:

  1. Asegúrate de haber iniciado sesión en tu cuenta de GitHub.
  2. Ve a tu repositorio de GitHub.
  3. Haz clic en Settings (Configuración).
  4. En "Código y automatización", haz clic en Webhooks.
  5. Haz clic en Añadir webhook.
  6. Introduce lo siguiente:

    1. En URL de carga útil, introduce la URL de la función de Cloud Run que has implementado anteriormente.

      La URL tendrá el siguiente aspecto: https://github-runner-autoscaler-PROJECTNUM.REGION.run.app, donde PROJECTNUM es el identificador numérico único de tu proyecto y REGION es la región en la que has implementado el servicio.

    2. En Content type (Tipo de contenido), selecciona application/json.

    3. En Secreto, introduce el valor WEBHOOK_SECRET que has creado anteriormente.

    4. En Verificación SSL, selecciona Habilitar verificación SSL.

    5. En "¿Qué eventos quieres que activen este webhook?", selecciona Quiero seleccionar eventos concretos.

    6. En la selección de eventos, selecciona Tareas de flujo de trabajo. Desmarca cualquier otra opción.

    7. Haz clic en Añadir webhook.

Reducir el tamaño de un grupo de trabajadores

El webhook ya está configurado, por lo que no es necesario que haya un trabajador persistente en el grupo. De esta forma, también te asegurarás de que no haya trabajadores en ejecución cuando no haya trabajo que hacer, lo que reducirá los costes.

  • Ajusta tu grupo para que se escale a cero:

    gcloud beta run worker-pools update WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --scaling 0
    

Usar tu runner de escalado automático

Para verificar que el runner de escalado automático funciona correctamente, ejecuta una acción que hayas configurado previamente para runs-on: self-hosted.

Puedes monitorizar el progreso de tus acciones de GitHub en la pestaña "Acciones" de tu repositorio.

Para comprobar la ejecución de tu función de webhook y tu grupo de trabajadores, consulta las pestañas Logs de la función de Cloud Run y del grupo de trabajadores de Cloud Run, respectivamente.