Buzeli
buzeliSoluções Digitais
DevSecOps

Build ARM64: 40 min → 8-12 min — eliminando QEMU no GitHub Actions

Publicado em 14 de abril de 2026

Build ARM64: 40 min → 8-12 min eliminando QEMU no GitHub Actions para OKE Kubernetes

O problema: QEMU silencioso no runner x86

Um pipeline de CI/CD no GitHub Actions fazia o build da imagem Docker para um cluster OKE (Oracle Kubernetes Engine) com nodes ARM64 (instâncias OCI A1.Flex Ampere). O runner configurado era self-hosted em máquina x86. O Dockerfile era padrão, o buildx estava configurado corretamente — e o build levava ~40 minutos.

A causa não estava no Dockerfile nem no tamanho da aplicação (Next.js com 350 páginas geradas). Estava em uma linha do workflow:

Copiar
- uses: docker/setup-qemu-action@v3
- run: docker buildx build --platform linux/arm64 .

Quando um runner x86 encontra --platform linux/arm64, o Docker usa QEMU para emular a arquitetura ARM64 em hardware x86. A penalidade de emulação é de 5-10x no tempo de compilação — cada instrução ARM64 é traduzida em tempo real pelo emulador.

O setup-qemu-action não avisa que o build vai ser lento. Ele simplesmente habilita a emulação e o buildx segue. O resultado é um build funcionalmente correto, mas ordens de magnitude mais devagar do que seria em hardware nativo.

O contexto: migração AWS (x86) → OCI ARM64

O projeto havia migrado de AWS ECS (instâncias x86) para OKE com nodes A1.Flex (ARM64 Ampere) para redução de custos. A imagem de produção que rodava em ECS era amd64 — o primeiro build ARM64 foi feito manualmente com Docker buildx no Mac Apple Silicon (que é ARM nativo). Para CI/CD automatizado, o runner self-hosted existente (x86) foi reaproveitado com QEMU para evitar configurar nova infraestrutura.

Durante semanas, deploys de produção levavam ~40 minutos só na etapa de build. O restante do pipeline (push para OCIR, kubectl rollout) era questão de segundos.

A solução: ubuntu-24.04-arm no GitHub Actions

O GitHub oferece runners ARM64 gerenciados desde 2024. O runner ubuntu-24.04-arm executa em hardware ARM real — sem emulação, sem QEMU, sem penalidade de tradução de instruções.

O workflow foi dividido em dois jobs independentes:

Copiar
# Antes — um único job x86 com QEMU para prod e stage
jobs:
  Build-and-Deploy:
    runs-on: self-hosted          # x86
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3   # emulação ARM64
      - uses: docker/setup-buildx-action@v3
      - run: |
          docker buildx build \
            --platform linux/arm64 \        # força ARM64 via QEMU
            --push \
            -t $IMAGE_TAG .
Copiar
# Depois — jobs separados por branch/ambiente
jobs:
  Build-and-Deploy-Stage:
    runs-on: self-hosted           # x86 — stage usa amd64, sem problema
    if: github.ref == 'refs/heads/staging'
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - run: docker buildx build --push -t $IMAGE_TAG .

  Build-and-Deploy-Prod:
    runs-on: ubuntu-24.04-arm      # ARM nativo — sem QEMU
    if: github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - run: |
          docker buildx build \
            --push \             # sem --platform: runner já é ARM64
            -t $IMAGE_TAG .

O ponto crítico: no job de prod, o --platform linux/arm64 foi removido. Quando o runner já é ARM64, especificar a plataforma é redundante — e mantê-la pode confundir o buildx em algumas versões. O build passa a ser nativo sem nenhuma outra mudança.

O que foi removido do job de prod

docker/setup-qemu-action@v3 — desnecessário: runner já é ARM64

--platform linux/arm64 — desnecessário: arquitetura nativa do runner

Set deploy Key — não utilizado: deploy via kubectl, não SSH

Load Centralized Configuration — variáveis migradas para secrets/vars do repositório GitHub

Fix colateral: RBAC do kubectl rollout status

Após o build, o pipeline executa kubectl rollout status para aguardar o deploy completar antes de marcar o job como sucesso. O comando falhava silenciosamente com erro de permissão — a ServiceAccount do CI tinha apenas get, patch e update em deployments.

Copiar
# Erro ao rodar kubectl rollout status
Error from server (Forbidden): deployments.apps "nextjs" is forbidden:
User "system:serviceaccount:myapp:cicd-deployer" cannot list resource
"deployments" in API group "apps" in the namespace "myapp"

O rollout status precisa de list e watch para observar o progresso do rolling update. A Role foi atualizada:

Copiar
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cicd-deployer-role
  namespace: myapp
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: ["apps"]
  resources: ["replicasets"]
  verbs: ["get", "list"]

O segundo gargalo: imagem de 2,1 GB

Com o build resolvido, o próximo gargalo ficou evidente: o downtime durante rolling updates era de 4-5 minutos — não por lentidão do Kubernetes, mas pelo tempo de pull da imagem. Cada novo node no OKE precisava baixar 2,1 GB do OCIR (Oracle Container Registry), o que levava 1 minuto e 34 segundos por pod. Com 2 réplicas e rolling update sequencial, o window de indisponibilidade parcial era consistente.

A redução da imagem é o próximo passo: multi-stage build para excluir node_modules de dev, Next.js output: 'standalone' (reduz drasticamente o que vai para a imagem final) e otimização de layers. Uma imagem de ~500 MB reduziria o pull de 1m34s para ~20s — eliminando praticamente todo o downtime do rolling update.

Resultado

O primeiro deploy após a mudança para ubuntu-24.04-arm confirmou o funcionamento: build bem-sucedido, imagem publicada no OCIR, pods subindo no OKE com a nova tag.

Copiar
# Validação pós-deploy
kubectl get pods -n myapp
# nextjs-7f4cbbdffd-g595h   1/1   Running   0   6m
# nextjs-7f4cbbdffd-gt5jt   1/1   Running   0   5m

kubectl get deployment nextjs -n myapp \
  -o jsonpath='{.spec.template.spec.containers[0].image}'
# registry.example.com/myapp/www:abc1234f
QEMU é uma ferramenta válida para builds pontuais em máquinas de desenvolvimento. Para CI/CD de produção onde cada commit passa pelo pipeline, a penalidade de 5-10x é inaceitável. Runners ARM nativos existem no GitHub Actions e custam o mesmo que runners x86 equivalentes — não há razão para manter QEMU em pipelines de longa duração.

Checklist para migrar seu pipeline

1. Identificar se o build usa --platform com arquitetura diferente do runner

2. Verificar se setup-qemu-action está presente (sinal claro de emulação)

3. Trocar runs-on para ubuntu-24.04-arm (ou runner ARM self-hosted)

4. Remover setup-qemu-action e --platform do job de prod

5. Verificar RBAC da ServiceAccount: rollout status precisa de list + watch

6. Medir tempo de pull da imagem — acima de 30s é candidato a otimização