Hospedar executores do GitHub com pools de workers do Cloud Run

Este tutorial mostra como usar executores autohospedados do GitHub em pools de trabalhadores para executar os fluxos de trabalho definidos no seu repositório do GitHub.

Você vai implantar um pool de workers do Cloud Run para processar essa carga de trabalho e, opcionalmente, uma função do Cloud Run para oferecer suporte ao escalonamento do pool de workers.

Sobre executores auto-hospedados do GitHub

Em um fluxo de trabalho do GitHub Actions, os executores são as máquinas que executam jobs. Por exemplo, um executor pode clonar seu repositório localmente, instalar um software de teste e executar comandos que avaliam seu código.

É possível usar executores auto-hospedados para executar ações do GitHub em instâncias de pool de trabalhadores do Cloud Run. Este tutorial mostra como escalonar automaticamente um pool de executores com base no número de jobs em execução e não programados, até mesmo escalonando o pool para zero quando não há jobs.

Recuperar a amostra de código

Para recuperar o exemplo de código para uso, siga estas etapas:

  1. Clone o repositório de amostra na sua máquina local:

    git clone https://github.com/GoogleCloudPlatform/cloud-run-samples
    
  2. Mude para o diretório que contém o código de amostra do Cloud Run:

    cd cloud-run-samples/github-runner
    

Entenda o código principal

A amostra é implementada como um pool de workers e um escalonador automático, conforme descrito a seguir.

Pool de workers

O pool de workers é configurado com um Dockerfile baseado na imagem actions/runner criada pelo GitHub.

Toda a lógica está contida nessa imagem, exceto um pequeno 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"]

Esse script auxiliar é executado quando o contêiner é iniciado, registrando-se no repositório configurado como uma instância efêmera, usando um token que você vai criar. O script também define quais ações serão realizadas quando o contêiner for reduzido.

# 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 $!

Escalonador automático

O autoescalador é uma função que aumenta o pool de workers quando há um novo job na fila ou diminui quando um job é concluído. Ela usa a API Cloud Run para verificar o número atual de workers no pool e ajusta esse valor conforme necessário.

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 o IAM

Este tutorial usa uma conta de serviço personalizada com as permissões mínimas necessárias para usar os recursos provisionados. Para configurar a conta de serviço, faça o seguinte:

  1. Defina o ID do projeto em gcloud:

    gcloud config set project PROJECT_ID
    

    Substitua PROJECT_ID pela ID do seu projeto.

  2. Crie uma conta de serviço do Identity and Access Management:

    gcloud iam service-accounts create gh-runners
    

  3. Conceda à conta de serviço permissões para atuar como uma conta de serviço no seu projeto:

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

    Substitua PROJECT_ID pela ID do seu projeto.

Recuperar informações do GitHub

A documentação do GitHub para adicionar executores autohospedados sugere adicionar executores pelo site do GitHub, que fornece um token específico para usar na autenticação.

Este tutorial adiciona e remove executores de forma dinâmica e precisa de um token estático do GitHub para isso.

Para concluir este tutorial, crie um token do GitHub com acesso para interagir com o repositório selecionado.

Identificar o repositório do GitHub

Neste tutorial, a variável GITHUB_REPO representa o nome do repositório. É a parte do nome do repositório do GitHub que vem depois do nome de domínio, tanto para repositórios de usuários pessoais quanto de organizações.

Você vai referenciar o nome do repositório que vem depois do nome de domínio para repositórios de usuários e organizações.

Neste tutorial:

  • Para https://github.com/myuser/myrepo, o GITHUB_REPO é myuser/myrepo.
  • Para https://github.com/mycompany/ourrepo, o GITHUB_REPO é mycompany/ourrepo.

Criar token de acesso

Você precisa criar um token de acesso no GitHub e salvá-lo com segurança no Secret Manager:

  1. Confira se você fez login na sua conta do GitHub.
  2. Acesse a página Configurações > Configurações do desenvolvedor > Tokens de acesso pessoal do GitHub.
  3. Clique em Gerar novo token e selecione Gerar novo token (clássico).
  4. Crie um novo token com o escopo "repo".
  5. Clique em Gerar token.
  6. Copie o token gerado.

Criar valor do secret

Pegue o token secreto que você acabou de criar, armazene-o no Secret Manager e defina as permissões de acesso.

  1. Crie o secret no Secret Manager:

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

    Substitua GITHUB_TOKEN pelo valor que você copiou do GitHub.

  2. Conceda acesso ao secret recém-criado:

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

Implantar pool de workers

Crie um pool de workers do Cloud Run para processar ações do GitHub. Esse pool vai usar uma imagem baseada na imagem actions/runner criada pelo GitHub.

Configurar o pool de workers do Cloud Run

  1. Navegue até o exemplo de código do pool de worker:

    cd worker-pool-container
    
  2. Implante o pool de workers:

    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
    

    Substitua:

    Se esta for a primeira vez que você usa implantações de origem do Cloud Run neste projeto, será solicitado que você crie um repositório padrão do Artifact Registry.

Como usar o pool de workers

Agora você tem uma única instância no seu pool de trabalhadores, pronta para aceitar jobs das ações do GitHub.

Para verificar se você concluiu a configuração do executor autohospedado, invoque uma ação do GitHub no seu repositório.

Para que sua ação use os executores auto-hospedados, mude um trabalho de ação do GitHub. No job, mude o valor de runs-on para self-hosted.

Se o repositório ainda não tiver nenhuma ação, siga o guia de início rápido para o GitHub Actions.

Depois de configurar uma ação para usar os executores autohospedados, execute-a.

Confirme se a ação foi concluída na interface do GitHub.

Implantar o escalonador automático do runner do GitHub

Você implantou um worker no pool original, o que permite o processamento de uma ação por vez. Dependendo do uso da CI, talvez seja necessário escalonar seu pool para lidar com um aumento de trabalho a ser feito.

Depois de implantar o pool de workers com um runner do GitHub ativo, configure o escalonador automático para provisionar instâncias de worker com base no status do job na fila de ações.

Essa implementação detecta um evento workflow_job. Quando um job de fluxo de trabalho é criado, ele aumenta a escala do pool de workers e, quando o job é concluído, diminui a escala novamente. Ele não vai escalonar o pool além do número máximo de instâncias configuradas e vai escalonar para zero quando todos os jobs em execução forem concluídos.

É possível adaptar esse escalonador automático com base nas suas cargas de trabalho.

Criar valor de secret do webhook

Para criar um valor secreto para o webhook, faça o seguinte:

  1. Crie um secret do Secret Manager que contenha um valor de string arbitrário.

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

    Substitua WEBHOOK_SECRET por um valor de string arbitrário.

  2. Conceda acesso ao secret para a conta de serviço do escalonador automático:

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

Implantar a função para receber solicitações de webhook

Para implantar a função de recebimento de solicitações de webhook, faça o seguinte:

  1. Navegue até o exemplo de código do webhook:

    cd ../autoscaler
    
  2. Implante a Cloud Run function

    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
    

    Substitua:

    • GITHUB_REPO a parte do nome do repositório do GitHub depois do nome de domínio
    • WORKER_POOL_NAME: o nome do pool de workers
    • WORKER_POOL_LOCATION a região do pool de workers
    • REPOSITORY_NAME o nome do repositório do GitHub
  3. Anote o URL em que o serviço foi implantado. Você vai usar esse valor em uma etapa posterior.

  4. Conceda permissões à conta de serviço para atualizar seu pool de workers:

    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
    

    Substitua PROJECT_ID pela ID do seu projeto.

Criar webhook do GitHub

Para criar o webhook do GitHub, siga estas etapas:

  1. Confira se você fez login na sua conta do GitHub.
  2. Navegue até seu repositório do GitHub.
  3. Clique em Configurações.
  4. Em "Código e automação", clique em Webhooks.
  5. Clique em Add webhook.
  6. Digite o seguinte:

    1. Em URL de payload, insira o URL da função do Cloud Run que você implantou anteriormente.

      O URL vai ser parecido com este: https://github-runner-autoscaler-PROJECTNUM.REGION.run.app, em que PROJECTNUM é o identificador numérico exclusivo do seu projeto e REGION é a região em que você implantou o serviço.

    2. Em Tipo de conteúdo, selecione application/json.

    3. Em Secret, insira o valor de WEBHOOK_SECRET criado anteriormente.

    4. Em Verificação SSL, selecione Ativar verificação SSL.

    5. Em "Quais eventos você quer acionar com este webhook?", selecione Quero selecionar eventos individuais.

    6. Na seleção de eventos, escolha Jobs de fluxo de trabalho. Desmarque qualquer outra opção.

    7. Clique em Add webhook.

Reduzir a escala do pool de workers

O webhook já está no lugar, então você não precisa ter um worker persistente no pool. Isso também garante que não haja trabalhadores em execução quando não houver trabalho a ser feito, reduzindo os custos.

  • Ajuste o pool para escalonar para zero:

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

Usar o runner de escalonamento automático

Para verificar se o executor de escalonamento automático está funcionando corretamente, execute uma ação que você configurou anteriormente para runs-on: self-hosted.

Você pode acompanhar o progresso das ações do GitHub na guia "Ações" do seu repositório.

Para verificar a execução da função de webhook e do pool de trabalhadores, acesse a guia "Registros" da função do Cloud Run e do pool de trabalhadores do Cloud Run respectivamente.