Sempre fui um grande fã do GitHub e do GitHub Actions. Sempre foram duas ferramentas que em conjunto “simplesmente funcionam…”. Ou pelo menos funcionavam! Ultimamente, tenho reparado que o GitHub está extremamente lento e que os ciclos de feedback são cada vez mais demorados e difíceis de reproduzir. Não que antes fosse super rápido, mas agora é dolorosamente lento.

Esta última execução foi a gota de água que me levou a investigar o assunto a fundo. Mais de oito minutos para uma simples build multi-arquitetura, com muito poucos passos! Não pode ser, não faz sentido nenhum.

Estarei eu a fazer alguma coisa mal? Estará o GitHub Actions mesmo assim tão mau ou estou a deixar-me “emprenhar pelos ouvidos”? Dito isto, decidi investigar alternativas e reescrevi o workflow por completo.

A busca pela consistência e o problema dos segredos

Criei um Taskfile com todas as tarefas necessárias para o projeto: testes, construção da imagem, tudo. O objetivo? Quero que tudo corra da mesma forma, independentemente do ambiente onde estiver. Todas as tarefas, do início ao fim, devem ser possíveis de correr tanto na minha máquina, como num ambiente CI, como na máquina de qualquer contribuidor.

Mas para isto, tinha de resolver primeiro outro problema: a gestão de segredos. O que tem sido, de facto, outra frustração enorme com o GitHub e com o GitHub Actions. Como para contas pessoais não há possibilidade de criar segredos partilhados por todos os projetos, acabo por ter de duplicar configurações em todos os repositórios. E quando é preciso ir lá rodar algum, assobias para o lado e mais vale estar quieto.

Continuei a investigar o que utilizar, e acabei por assentar no Infisical. Tem uma boa interface web, suporta OIDC e qualquer protocolo de autenticação. A ferramenta de linha de comandos (CLI) é fácil de utilizar, muito intuitiva, funciona às mil maravilhas e pode ser alojada em qualquer lado, ou simplesmente usamos a solução cloud deles.

O choque de realidade: GitHub Actions vs. A minha máquina

Depois de reescrever o workflow, os resultados foram estupidamente reveladores. Foram precisos mais de 7 minutos para o GitHub Actions correr uma build multi-arquitetura e menos de 1 minuto na minha máquina!

Eu juro que tentei tornar a missão complicada para o meu computador. Apaguei todas as imagens para forçar a reconstrução de todas as camadas (layers), mas ainda assim… o GitHub Actions levou consistentemente mais de 7 minutos e a minha máquina levou consistentemente menos de 1 minuto.

broken

Podemos assumir que o GitHub Actions está a colocar os utilizadores do plano gratuito em máquinas mais limitadas e que, por isso, corre mais lento. Talvez. Mas suspeito que o GitHub Actions se tornou demasiado pesado e que o GitHub está de facto a atravessar um período terrível ao nível de infraestrutura.

Já que comecei com isto, decidi tirar as teimas. Tinha de entender se existe mesmo um problema com o GitHub Actions ou se sou eu que estou a fazer algo de errado. Para tal, decidi investigar como é que as outras plataformas se comportam.

O calvário de testar novas plataformas de CI

Primeiro, decidi testar um miúdo relativamente novo no bairro: o Buildkite. Não quero entrar aqui em grandes detalhes, mas não me entendi com a coisa, ao ponto de desistir ao fim de uma hora a tentar. Ou sou muito burro ou aquilo simplesmente não passa no teste de experiência de utilizador. Quero acreditar que é a segunda opção!

De seguida, decidi experimentar outro que nunca tinha visto, o Octopus. Bom, não era de todo o que estava à procura. Levei mais tempo a tentar descobrir como apagar a conta que criei e, claramente, devo ser muito burro também, visto que não encontrei onde o fazer e acabei a ter de enviar um e-mail ao suporte. Em defesa deles, foram muito rápidos a tratar do assunto.

Já meio frustrado, foi a vez de experimentar algo mais familiar: o CircleCI. Mais uma vez, tropecei numa carrada de problemas relacionados com o Podman e o Docker. O tempo que tenho para alocar a esta investigação é limitado, e isto lembrou-me exatamente da razão pela qual me frustrei com o GitlabCI há muitos anos atrás.

Os ambientes CI alojados na cloud são difíceis de replicar localmente, o que torna a configuração muito mais complicada. O ciclo de feedback é tortuoso: editar o ficheiro, fazer git commit, push, esperar que tudo corra até ao próximo erro e repetir o processo. Não há realmente uma forma simples de encurtar isto!

Por esta altura, já estou nisto há tempo demais. Começo a questionar as minhas escolhas de vida, a pensar se devo tentar Earthly (Earthbuild) e Dagger, ou se simplesmente mando isto tudo à fava.

Ainda criei um workflow com o Dagger, mas não durou muito. Não consigo gostar do Dagger e não consigo perceber a visão da ferramenta. É tão verboso, é preciso tanto trabalho para conseguir fazer seja o que for para, a meu ver, não alcançar nada. Se for para seguir por este caminho, continuo a achar que o Earthbuild é a melhor opção. Cheguei a criar o pacote AUR (que tenciono manter porque gosto da visão do projeto), no entanto, depois de uma breve pausa, ocorreu-me: mas para quê?

Para que raio estou eu a tentar seguir este caminho? Qual é de facto o meu objetivo? Só estou a adicionar mais um nível de complexidade para alcançar tão pouco. Para não falar de que me estou a afastar dos standards da indústria. E eles são standards por alguma razão (ou não!).

De volta aos testes (e aos “standards”)

Uma vez que já cheguei tão longe, não tencionava ficar por aqui. Queria respostas! Voltei a centrar-me no objetivo inicial: comparar diferentes plataformas de CI.

Depois de brigar com os vários passos do workflow do CircleCI, finalmente consegui ter a pipeline a correr. Levou 3m27s no total, com 1m32s no processo de build. Já estamos a falar melhor!

circleci

Os problemas pareceram centrar-se maioritariamente à volta do Podman. Aparentemente, os ambientes de CI não gostam muito de invenções. Na minha máquina uso por defeito o Podman (acho que faz um trabalho muito melhor), mas mais uma vez, o facto de me afastar dos standards da indústria causou problemas.

Fiz algumas alterações no Taskfile para utilizar o Docker. Assim, os ambientes de CI alojados podem usar o Docker, e eu posso continuar usar o Podman através do pacote podman-docker (que funciona como proxy e converte os comandos Docker para Podman). Ninguém se chateia e tudo fica mais simples.

Para ter uma comparação justa, voltei a correr a pipeline no GitHub Actions, mas desta vez removi a instalação do Podman e usei a Action oficial para instalar o Docker Buildx de forma a suportar builds multi-arquitetura. Para meu espanto, a build levou um total de 4m18s e o processo de build acompanhado do release levou 2m48s. Por esta é que eu não estava à espera! Muito melhor, mas ainda assim atrás do CircleCI ou da minha própria máquina.

ghbetter

As diferenças principais? A utilização de Docker e o facto de o Taskfile, em vez de fazer sucessivamente build, tag e push, passar a fazer tudo de uma vez. Utilizei a flag --push e -t diretamente no comando de build:

build_cmd="docker buildx build --push --platform linux/amd64,linux/arm64 -t ${latest_name} -t ${git_name}"

O regresso ao Buildkite (e a saga da memória)

Depois de tantas batalhas, e porque sou mais teimoso do que uma porta, decidi voltar atrás e dar mais uma hipótese ao Buildkite.

Afinal o problema era mesmo meu! Depois de batalhar com o CircleCI (que está muito melhor documentado) e perceber o que estava a causar problemas, só tive de ajeitar algumas coisas no Buildkite. Tudo estava a correr maravilhosamente bem até que:

2.087 go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7
2.102 go: downloading github.com/go-playground/locales v0.14.1
45.41 github.com/ugorji/go/codec: /usr/local/go/pkg/tool/linux_amd64/compile: signal: killed
------
Containerfile:7
--------------------
   5 |
   6 | ARG TARGETARCH
   7 | >>> RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o ./server ./cmd/web/main.go
   8 |
   9 |
--------------------
ERROR: failed to build: failed to solve: ResourceExhausted: process "/bin/sh -c CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o ./server ./cmd/web/main.go" did not complete successfully: cannot allocate memory
task: Failed to run task "release:full": task: Failed to run task "container:push": exit status 102
🚨 Error: The command exited with status 201
user command error: exit status 201

O runner ficou sem memória? Esta história parece não ter fim. Felizmente é possível pedir um servidor com mais músculos, e lá correu! Levou 2m18s para correr a pipeline do princípio ao fim com um servidor LINUX_AMD64_4X16 (4 cores, 16GB Memória). Ok, é o mais rápido do grupo até agora. Mas valerá a pena?

Para tirar as últimas teimas, decidi experimentar mais duas opções simples: o Blacksmith e o GitHub Actions com um runner self-hosted a correr na minha máquina.

O Blacksmith foi rápido de avaliar: não suporta contas pessoais, só organizações. Adeus!

Para o self-hosted runner, como já tenho a maior parte das ferramentas instaladas na minha máquina, comentei as partes da configuração de ambiente no ficheiro e foquei-me somente no build/push. Mas mais uma vez, andar na vanguarda tecnológica traz problemas. A flag --push não está implementada no proxy podman-docker, logo levei com o GitHub Actions a gritar comigo:

Error: unknown flag: --push
See 'podman buildx build --help'
task: Failed to run task "release:full": task: Failed to run task "container:push": exit status 125
Error: task: Failed to run task "container:push": exit status 125

Existe de facto um Pull Request no repositório deles para implementar isto, mas foi aberto há três anos e parece ter ficado ao abandono. Enfim, acho que vou voltar para a ferramenta mãe (o Docker) e deixar o Podman de lado por mais uns tempos. Assim que troquei podmn por docker, o tudo funcionou como esperado, e afinal … O github actions em si nao é assim tao lento com maquinas dcentes! um pouco mais de um minuto para o processo de build e release!

good

Resumo dos Tempos

Para que fique claro de onde vem a minha frustração, eis o resumo de todo o tempo investido e os resultados desta brincadeira para a mesma build multi-arquitetura:

Plataforma / AmbienteTempo TotalTempo de BuildNotas Adicionais
A minha máquina (Inicial)< 1m 00s-Reconstrução total das camadas
GitHub Actions (Inicial)> 7m 00s-Demasiado lento e inconsistente
Buildkite2m 18s-Servidor LINUX_AMD64_4X16 (Upgrade de RAM necessário)
CircleCI3m 27s1m 32s
GitHub Actions4m 18s2m 48sWorkflow otimizado, mas atrás da concorrência
GitHub Actions (self hosted)-1m 8ssem process de configuração

Com estes resultados em mão, parece-me que a minha intuição estava tanto certa como errada! O GitHub Actions está de facto lento demais quando comparado com a concorrência, mas consegue ser bastante rápido quano optimizado e a correr nos runners self-hosted. Isto leva-me a crer que os problemas do GitHub estão maioritariamente ao nível da orquestração e da infraestrutura de rede, pois durante as execuções reparei que o processo ficava muitas vezes estagnado na fase de download dos pacotes de dependências. Seja como for, tanto self-hosted quanto na cloud pública, qualquer dos serviços continuam a ser lentos demais para o meu gosto, o que arrasa por completo os ciclos de feedback e a produtividade!

Conclusão

Não usem ambientes de CI se não precisarem efetivamente deles! Utilizem Makefiles, Taskfiles ou até mesmo simples shell scripts para padronizar o processo da vossa pipeline, e esperem até ao momento em que a necessidade de utilizar CI Runners realmente apareça.

Tenho sido preguiçoso neste aspeto. Como o CI é aquilo que é apregoado na indústria, assumi que era o rumo certo para tudo, ao ponto de o utilizar extensivamente a nível pessoal e profissional. Mas nunca fui muito bom a seguir o rebanho.

Os ambientes de CI adicionam uma extrema complexidade. Quando alguma coisa falha, os ciclos de feedback são agonizantes (fazer a alteração, commit, push, esperar uma eternidade, falhar, repetir). Pelo menos para projetos pessoais, vou deixar completamente de utilizar plataformas de CI. Porquê?

  • O ambiente já lá está: Na minha máquina ou na de qualquer colaborador, as ferramentas já vão estar devidamente instaladas. Não é preciso ficar à espera que o ambiente prepare tudo do zero a cada execução.
  • Confiança e Segurança: O meu computador é um ambiente de alta confiança. Ao contrário dos sistemas alojados na cloud, conheço a configuração e não há telemetria opaca escarrapachada pelo meio.
  • Menos Abstrações: Não preciso de depender de actions de terceiros que, muitas vezes, acabam comprometidas ou abandonadas.
  • Velocidade de Feedback: Desenvolvo e corro as tasks. Se alguma falhar, consigo correr apenas o passo exato de que preciso para corrigir o problema. Acabou o calvário de esperar que a pipeline corra do início ao fim só para descobrir o próximo erro na fila.

Claro que existem exceções ditadas pela escala ou utilidade. Por exemplo, o uso de CI para correr testes num Pull Request para ajudar no processo de revisão, ou quando o projeto cresce a um ponto em que é mais seguro e fácil isolar o processo num runner na cloud do que tentar garantir a segurança das máquinas locais de dezenas de contribuidores diferentes. Nessa fase, a lentidão e a complexidade dos ambientes de CI passam a ser apenas um custo de manutenção perfeitamente justificável. No entanto, independentemente da escala, acredito que todo o workflow deve poder correr em qualquer máquina, com as ações devidamente limitadas por um controlo de acessos adequado! Agora, depender completamente do ambiente de CI para passar de desenvolvimento para produção? Continuo a achar que não é uma boa ideia.

Com tudo isto, o que quero dizer é: sejam críticos nas vossas escolhas, mantenham os processos simples e evitem adicionar ferramentas de que não precisam realmente!

good