O blog da AWS

Escalando executores self-hosted do GitHub Actions com Amazon ECS

Vinicius Schettino, Engenheiro DevOps, Passei Direto
Gabriel Bella Martini, Arquiteto de Soluções, AWS Brasil Setor Público
Thiago Pádua, Arquiteto de Soluções, AWS Brasil Setor Público

 

A Passei Direto, maior rede de estudos e compartilhamento de materiais de ensino do Brasil, passou por um crescimento vertiginoso entre 2019 e 2020, atingindo mais de 10 milhões de estudantes utilizando a sua plataforma. Com isso, ocorreu a ampliação do setor de engenharia da mesma, levando à necessidade de adotar um modelo distribuído de trabalho, com squads multidisciplinares focadas em elementos específicos do produto. Em um dia típico, a equipe lida com cerca de 1.800 procedimentos automatizados, totalizando uma média de 55 horas de execução. Para suportar essa carga de trabalho crescente de forma otimizada e eficiente, o Jenkins vem sendo gradualmente substituído pelo GitHub Actions como principal ferramenta de CI/CD.

Contudo, do ponto de vista da infraestrutura necessária para executar as rotinas de testes, deploy e desenvolvimento, o modelo auto gerenciado do GitHub Actions não atende todos os casos das squads na Passei Direto. Um dos principais desafios são pipelines que demandam mais recursos computacionais, especialmente relacionadas à área de dados e aprendizado de máquina. Mesmo para tarefas mais curtas, existem casos de integração com diversos serviços da AWS que ficam protegidos por Amazon VPC e Security Groups. Por isso, a Passei Direto resolveu criar uma solução Open Source de executores self-hosted utilizando o Amazon Elastic Container Service (ECS). As principais vantagens encontradas foram a escalabilidade (mais tarefas sendo executadas em paralelo), redução de custos (executores ligados somente quando necessário) e flexibilidade com recursos personalizados para tarefas específicas (requisitos de memória e GPU, por exemplo). Neste artigo explicaremos a arquitetura da solução criada, os principais resultados e os planos para melhorias futuras.

 

Visão geral da solução

A solução inicializa executores do GitHub Actions através de tasks ECS, hospedadas em instâncias Amazon EC2. São três componentes principais na arquitetura:

Todos os componentes acima são iniciativas Open Source da Passei Direto e estão disponíveis para contribuição e replicação “as is” da solução, ou como referência para abordagens semelhantes.

O diagrama de sequência abaixo descreve o comportamento destes componentes a partir do disparo de uma pipeline:

 

Figura 1 – Diagrama de execução dos Self Hosted Runners

A pipeline é disparada pelo próprio GitHub baseada em um evento, como um novo Pull Request ou a criação de uma tag. O executor é disparado em um container Docker por uma tarefa que antecede a rotina que foi iniciada. Essa tarefa será chamada de pre-job, sendo responsável por disparar a task no cluster ECS e esperar o executor estar pronto para receber a carga de trabalho descrita no workflow.

O pre-job é processado na infraestrutura do GitHub Actions, executando uma action que utiliza o SDK da AWS para Python, boto3, para lançar a task que irá inicializa o container executor. É possível configurar parâmetros como memória de CPU reservadas para o executor, modelo de rede, tags e outras configurações de runtime da task definition.

O cluster ECS é o responsável por inicializar as instâncias necessárias para lidar com a carga de trabalho na fila. De acordo com a configuração do Capacity Provider, o cluster irá disponibilizar novas instâncias ou remover as ociosas. Mais detalhes sobre a escalabilidade dos clusters ECS podem ser encontradas neste artigo.

O ECS pode executar containers tanto em instâncias EC2 quanto no AWS Fargate. Uma vez que o GitHub Actions também executa as suas tarefas dentro de containers, é necessário o uso do Docker-in-Docker (DinD) que requer o uso do parâmetro privileged para funcionar adequadamente, o que é suportado apenas em instâncias EC2 e por isso foi adotada como padrão na solução. Entretanto, ainda é possível o uso do Fargate de forma limitada, mas ele traz benefícios como o tempo de inicialização reduzido dos executores e não há necessidade de gerenciar as instâncias diretamente.

O desenho abaixo compara o modelo de execução do ECS utilizando o EC2 com o Fargate:

 

Figura 2 – Cluster ECS, EC2 e Fargate

A task definition conta com a definição do container do executor e a configuração de alguns valores default para configuração de rede, volumes e variáveis de ambiente. Para o executor, a solução utiliza uma abordagem efêmera, onde o container é parado logo após a finalização de uma pipeline. Com isso é possível garantir o isolamento entre as builds, facilitando a escalabilidade e otimização de recursos, já que em momentos ociosos, como por exemplo, fora do horário de trabalho, recessos, finais de semana e feriados, não há necessidade de instâncias ligadas. Apesar da abordagem efêmera ainda não ser plenamente suportada pelo GitHub Actions, existem diversas iniciativas do core do projeto e da comunidade voltadas para viabilização deste modelo de trabalho.

Toda a infraestrutura do projeto é mantida através de scripts CDK e chamadas para APIs da AWS.

 

Implantação de exemplo

Para demonstrar a solução, será exemplificado um caso de uso simples: quando uma release é criada no GitHub, uma pipeline de deploy deve ser executada. Como nesse produto fictício é preciso ter acesso às instâncias contidas em uma VPC privada, foi decidido utilizar um executor self-hosted. O workflow seria descrito por um arquivo YAML no seguinte formato:

 

name: 'Deploy new release to AWS'

on:

  release:

    types: [published]

jobs:

  pre-job:

    runs-on: ubuntu-latest

    steps:

    - uses: actions/checkout@v2

    - uses: aws-actions/configure-aws-credentials@v1

      with:

        aws-access-key-id: ${{ secrets.AWS_KEY_ID }}

        aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}

        aws-region: ${{ secrets.AWS_DEFAULT_REGION }}

    - name: Provide a self hosted to execute this job

      uses: PasseiDireto/gh-runner-task-action@main

      with:

        github_pat: ${{ secrets.GITHUB_ACTIONS_RUNNER_TOKEN }}

        task_definition: 'gh-runner'

        cluster: 'gh-runner'

  deploy:

    runs-on: self-hosted

    needs: pre-job

    container:

      image: python:3.9-alpine

    steps:

      - uses: aws-actions/configure-aws-credentials@v1

        with:

          aws-access-key-id: ${{ secrets.ECR_ACCESS_ID }}

          aws-secret-access-key: ${{ secrets.ECR_SECRET_KEY }}

          aws-region: ${{ secrets.AWS_DEFAULT_REGION }}

      - name: Deploy

        env:

          ENVIRONMENT: ${{ github.refs }}

        run: python scripts/deploy.py $ENVIRONMENT

Quando uma nova versão é lançada, o GitHub dispara o pre-job e em seguida o deploy. Graficamente é possível observar o fluxo na interface do GitHub:

 

Figura 3 – Fluxo de exemplo do GitHub

O pre-job é rapidamente executado, sendo um overhead pequeno em pipelines maiores. Isso acontece especialmente em casos onde já existe uma instância EC2 ativa e com a imagem do executor já disponível. Os custos do pre-job e deployment serão respectivamente cobrados pelo GitHub e pela AWS. Por fim, é possível observar a dependência entre as duas tarefas da pipeline, causadas pela diretiva needs que foi inserida no deploy.

Quando os executores estão online, é possível observar na contagem de tasks dentro do cluster ECS:

 

Figura 4 – Tasks do ECS

De acordo com o ciclo de vida da tarefa ECS, o caminho padrão seria PROVISIONING (enquanto aguarda instâncias disponíveis), PENDING (enquanto inicializa o container) e RUNNING (a tarefa está sendo executada com sucesso). No repositório (em configurações/actions) pode-se ver os executores registrados:

 

Figura 5 – Self-hosted runners disponíveis

Ao finalizar a pipeline, o executor automaticamente é removido do pool do repositório e a task ECS é finalizada com sucesso.

Ainda é possível utilizar a diretiva matrix para paralelizar os jobs, escalando o número de executores de acordo com a quantidade de tarefas que precisam ser executadas. Por exemplo, na etapa inicial do workflow é decidido quais são os componentes que precisam passar por uma nova build, e então será iniciado o número necessário de executores para que o processo possa ocorrer em paralelo:

 

Figura 6 – Utilização de Matrix

 

Voz do cliente

Com o GHA temos mais liberdade e facilidade em escalar nossos deploys, sem contar que estando tudo num mesmo lugar facilita bastante no monitoramento. Além disso, com o Jenkins precisávamos das máquinas sempre ligadas enquanto que com o GHA podemos ter o esquema de uso apenas quando necessário, o que pode levar a uma demora um pouco maior em alguns builds, mas que é compensado na economia.” — Renato Bibiano, Tech Manager [Data Squad]

 

Conclusão e próximos passos

A Passei Direto está passando por uma migração gradual do Jenkins para essa nova solução. Contudo, do ponto de vista dos custos, as diferenças já são evidentes. Utilizando o painel de custos da AWS é possível visualizar uma redução dos custos de CI/CD em 11x nas squads que já adotaram plenamente o GitHub Actions com ECS, originado principalmente pela mudança no modelo de execução de 24×7 para sob demanda.

A Passei Direto está trabalhando em melhorias da solução apresentada neste blog post. Entre elas podemos destacar:

  • Atualização automática do executor no container;
  • Otimização da imagem do executor do ponto de vista de segurança, tamanho e tempo de inicialização;
  • Criação de task definitions com utilização do Fargate para casos específicos;
  • Disponibilização dos logs de execução no Amazon CloudWatch;
  • Monitoramento centralizado das pipelines;
  • Utilização de labels para identificar os executores e evitar condições de corrida;
  • Melhorias nas abordagens de paralelismo para iniciar os jobs na medida da disponibilidade dos executores, acelerando o processo.

Nos próximos meses esperamos que dezenas de novas pipelines sejam migradas para a nova abordagem. Assim iremos gradualmente evoluindo a proposta e publicando resultados mais maduros, com objetivo de ajudar a comunidade a construir uma solução escalável para executores self-hosted do GitHub Actions.

Colaboraram na solução Rodrigo Martins e Robson Andrade, Engenheiros DevOps da Passei Direto.

 


Sobre os autores

Vinícius Schettino é DevOps Engineer na Passei Direto com mais de 10 anos de experiência em engenharia de software. Focado em Data CI/CD, MLOps, qualidade de software e automação.

 

 

 

 

Gabriel Bella Martini é um Arquiteto de Soluções na AWS com foco em clientes de Educação. Tem experiência em diferentes projetos relacionados a Inteligência Artificial e tem grande interesse na área de computação gráfica.

 

 

 

 

Thiago Pádua é Arquiteto de Soluções na AWS atua com o desenvolvimento e apoio aos parceiros do Setor Público. Trabalhou anteriormente com desenvolvimento de software e integração de sistemas, principalmente na indústria de Telecomunicações. Tem um interesse especial em arquitetura de microserviços, serverless e containers.