Construindo Imagens Docker Multi Plataforma através do GitHub Actions

CI GITHUB ACTIONS RASPBERRY PI

Tentando configurar um serviço utilizando Docker em um Raspberry Pi, deparei-me com tempos de build astronómicos (parei de contar após 20 minutos), muitas vezes resultando em erros por incompatibilidade ou indisponibilidade de rede.

Este post apresenta uma solução completa e automatizada para este problema, utilizando o GitHub Actions para automatizar o build das imagens em multiplas plataformas e disponibilizando elas no Docker Hub.

Docker no Raspberry Pi

O Docker é uma excelente ferramenta para criação e gerenciamento de containers, simplificando muito o processo de setup de uma topologia de um projeto e todas suas dependencias (DB, cache, server de arquivos estáticos, etc…).

Em 2016, foi anunciado no Blog do Raspberry Pi de que a nova versão do raspbian/jessie contaria com suporte oficial ao Docker, permitindo então que este fosse instalado com relativa facilidade:

$ curl -sSL https://get.docker.com | sh

No entanto, devido à capacidade de processamento do Raspberry Pi, especialmente nas versões de hardware mais antigas, o processo de construir as imagens dos containers é extremamente demorado.

Em um dos casos, passei mais de 20 minutos esperando o comando concluir, apenas para ver ele sendo cancelado devido a um problema temporário de conexão com os repositórios do NPM.

Builds Multi Plataforma

A solução que encontrei para este problema foi atravéz do comando buildx, que já vem disponível na CLI a partir da versão 19.03 do Docker.

Este commando permite a criação de builders, capazes de construirem imagens a partir de um Dockerfile. Este commando, aliado ao QEMU, é capaz de gerar imagens em multiplas plataformas, incluindo arm/v6 e arm/v7, que são as arquiteturas suportadas pelo Raspberry Pi.

Testei este método gerando uma imagem e armazenando ela num arquivo .tar:

# Create Builder

$ docker buildx create --name mybuilder

# Use QEMU to support ARM archtectures

$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

# Inspect the builder (the bootstrap flag also initiates its instance)

$ docker buildx inspect --bootstrap

# Build docker image and save it to dist/myproject.tar

$ docker buildx build --platform linux/arm/v6 -t myproject:latest -o type=docker,dest=- . > dist/myproject.tar

Depois de pronta, a imagem pode ser copiada para o Raspberry Pi e carregada através dos comandos:

$ docker load -i myproject.tar

# Tag the image to make it easier to reference

$ docker image tag <SHA digest> myproject:latest

Build automático através do GitHub Actions

O procedimento apresentado até então já é suficiente para cobrir a utilização básica, no entanto, como ele é manual, fica um pouco chato para um desenvolvimento contínuo de um projeto.

Uma maneira de resolver isto é automatizar e incluir este procedimento nas rotinas de Integração Contínua (CI). O GitHub Actions é perfeito para isto, sendo extremamente fácil de configurar e proporcionando uma excelente integração com o repositório do projeto.

O GitHub Actions nos permite configurar Workflows. Estes representam uma lista de ações que serão executadas em sequência. Estes são definidos através de arquivos .yml, definidos no diretório .github, na raíz do repositório:

$ mkdir -p .github/workflows
$ touch docker.yml

Conteúdo do arquivo:

name: Build Docker images

on:
  push:
    branches: [ main ]
    workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Setup QEMU
        id: qemu
        uses: docker/[email protected]
        with:
          platforms: linux/amd64,linux/arm/v6

      - name: Set up Docker Buildx
        id: buildx
        uses: docker/[email protected]

      - name: Login to Docker Hub
        uses: docker/[email protected]
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          platforms: linux/amd64,linux/arm/v6
          push: true
          tags: <repo>/<project>:latest

      - name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}
  • A primeira diretiva name atribui um nome ao Workflow.

  • A diretiva on define as condições nas quais o workflow será executado:
    • push - Define que o workflow será executado toda vez que houver push no branch main (Se o repositório for mais antigo, é bem provável que este seja o master).
    • workflow_dispatch - Permite executar o workflow manualmente, através da aba Actions na página principal do projeto.
  • Diretiva jobs define as tarefas a serem executadas e os seus parâmetros:
    • build - o nome do job a ser definido.
    • runs-on - permite escolher qual distribuição será utilizada na máquina virtual (provisionada automaticamente) que executará as ações.
      • Optei pelo ubuntu-18.04 pois é uma versão bem madura e estável.
    • steps - passos a serem executados.
      • name - Nome do passo.
      • id - ID do passo. Permite ser referenciado no restante do arquivo de configuração.
      • uses - Repositório da ação a ser executada.
      • with - Parâmetros a serem passados à ação.

Descrição das ações:

  • action/checkout - Faz um checkout do repositório na VM.

  • docker/setup-qemu-action - Faz o setup do QEMU:
    • platforms define as plataformas a serem configuradas no QEMU.
  • docker/setup-buildx-action - Faz o setup do buildx.

  • docker/login-action - Fazer o login no Docker Hub:
    • Permitirá fazer um push da imagem gerada para o repositório da Docker, definido no próximo passo.
    • Como este arquivo estará commitado no nosso repositório, não é uma boa ideia simplesmente colocar credenciais em plain text, mesmo que o repositório seja privado!
    • Para fazer isto de maneira segura, é possível adicionar as credenciais como Segredos (Secrets) no GitHub e referenciá-las através de variáveis prefixadas em secrets..
  • build-push-action - Gera a imagem:
    • Mais uma vez, as plataformas são específicadas em platforms.
    • tags especifica uma tag a ser aplicada às imagens geradas:
      • Utilizo apenas a tag :latest, que será sempre sobrescrita (Ainda não explorei muito, mas imagino que seja possível utilizar variáveis para aplicar a mesma tag definida no repositório Git).
  • A diretriz run serve para executar um comando diretamente no shell da VM. Neste caso, o echo serve para imprimir o hash da imagem gerada no passo anterior. Serve como exemplo de como se referir a uma etapa anterior (steps.<id_do_passo>).

Para descobrir outras ações disponíveis, é possível consultar o GitHub Marketplace.