Sobre Gitlab e Pages

Posted on July 6, 2021

Como falei, eu esqueci de falar sobre como colocar o um site gerado com hakyll online using o GitLab.

O que é GitLab?

GitLab é host de repositórios git assim como GitHub, com a diferença que a plataforma em si é código aberto (escrita em Ruby, assim como o próprio GitHub) e tem várias opções de hosting, incluindo Cloud e, é claro, no seu próprio servidor. Para servir o GitLab do seu próprio servidor eles oferecem dois opções a Community Edition (que é livre e gratuita) e a Enterprise Edition (que você paga, mas tem acesso a mais recursos).

Além disso o GitLab tem uma série de ferramentas para gerenciamento de projetos e DevOps. E isso bem antes do próprio GitHub oferecer estes serviços. Nós vamos usar dois aqui: GitLab CI/CD e GitLab Pages.

GitLab CI/CD

O GitLab CI/CD é um serviço de Continuous Integration/Develivery oferecido pelo GitLab. Com um arquivo YAML na raiz do seu projeto você pode descrever uma série de etapas que vão ser executadas todo vez que um determinado evento ocorrer no repositório (como um push em um branch determinado ou a criação de um Merge Request).

Você também específica o ambiente em que estas instruções vão ser executadas, começando por uma imagem do docker. Você também pode configurar variáveis de ambiente e arquivos secretos, caso precise.

O GitLab oferece este serviço de graça, mas é claro que algumas limitações. A instância tem pouca memória, capacidade de processamento e só pode rodar por 1 hora.

Você pode conectar seu próprio runner (uma instância capaz de rodar tarefas de CI/CD) no GitLab ou contratar algum runner deles se precisar de mais do que isso.

Bom, eu não queria gastar dinheiro com o blog então eu tenho que ficar no free tier.

Isso é relevante porque o Hakyll exige que você compile seu aplicação antes de poder gerar as páginas, e o processo de compilação de Haskell é um processo computacionalmente bem caro. Claro, o executável é muito rápido, mas isso porque o compilador faz um ótimo trabalho, que custa bastante processamento. Mas já voltamos nesse tópico.

GitLab Pages

Bom, o GitLab Pages é um serviço do GitLab para servir páginas estáticas. E advinha quem tem páginas estáticas para serem servidas? This guy! (aponta para si mesmo).

E o GitLab Pages se integra ao GitLab CI/CD, portanto deixa você gerar seu site executando qualquer tipo de SSG que você queria e então coleta os arquivos gerados e coloca num servidor web de tal forma que todo mundo possa ver.

Isso é muito útil. Especialmente pra documentação de projetos. No meu caso para o blog.

Um passo importante é escolher o nome do projeto que hospeda o site, por incrível que pareça. O meu usuário é janilcgarcia, o GitLab mapeia a página para um projeto da seguinte forma:

  • se o nome do projeto for $usuario.gitlab.io (janilcgarcia.gitlab.io no meu caso), a página é servida neste endereço (janilcgarcia.gitlab.io)
  • se não for, é servido em $usuario.gitlab.io/$nome_​do​_​projeto (se o for "blog" por exemplo, estaria mapeado para janilcgarcia.gitlab.io/blog)

Mas tem um detalhe, o GitLab suporta o conceito de Grupos. Grupos são uma forma de organizar pessoas e projetos relacionados. Cada grupo ganha um domínio na forma $grupo.gitlab.io (no meu caso https://safelydysfunctional.gitlab.io) para servir páginas estáticas também. Usando isso você pode conseguir domínios legais de onde servir seu site. O mapeando para grupos funciona de forma parecida com o descrito acima pra usuários.

No site do GitLab Pages você pode configurar sua própria página de acordo com a ferramenta que você queira usar. Mas a ideia é seguinte:

# Você começa pela imagem base para a compilação das suas páginas estáticas
image: imagem/base/docker

# aqui você descreve as etapas para compilar as páginas
pages:
  # neste bloco você coloca os passos que quer rodar para compilar o site
  script:
    - ssg compile -o public/

  # a saída contendo as páginas estáticas TEM que ser o diretório public
  # na raiz do seu projeto
  artifacts:
    paths:
      - public

  # com only você pode escificar quais branches ativam essa tarefa
  only:
    - master

Hakyll e CI/CD

Lembra como eu tinha dito que a compilação com Haskell é demorada. Pois bem, se você fizer a coisa óbvia que é pegar uma imagem do Haskell no docker, compilar o projeto e usar o executável compilado para construir as páginas estáticas, a tarefa vai falhar. Depois de uma hora. E eu aprendi isso da forma mais difícil.

O site está rodando, então você sabe que eu encontrei outro caminho, certo?

Pois bem. Por um tempo eu estava criando uma imagem com a executável pré-compilado, assim eu podia rodar o compilador no meu PC, que tem capacidade de processamento de sobra e depois fazendo o upload para o Docker Hub. Essa abordagem funciona muito bem, pois o executável não muda com frequência, só quando você muda alguma coisa no código.

Porém tem a desvantagem de ter que criar uma imagem no meu PC, enviar para o Docker Hub e ter de fazer toda vez que altero o código. Mas eu estava ciente de uma outra abordagem que a galera do Haskell anda usando: o nix.

Nix

Nix é o gerenciador de pacotes do NixOS, mas ele roda em outras distribuições também (acredito que até mesmo no macOS). O grande diferencial do Nix é que ele isola os pacotes do sistemas em diretórios específicos e depois encaixa os pacotes de maneira que seja totalmente compatível quando você precisa. Isso permite que você tem múltiplas versões dos mesmos pacotes disponíveis no seu sistema. E para controlar essa integração, o nix usa um uma linguagem declarativa (puramente funcional). Dessa forma todo ambiente do SO passa a ser um estado imutável e instalar um pacote nada mais é do que aplicar uma função que atualiza este ambiente para um novo estado contendo um pacote. O companheiro perfeito para o Haskell, nem mesmo o SO em si tem efeitos colaterais.

Parece ótimo, certo? Mas documentação é escassa, ajuda é díficil de encontrar e é muito díficil entender como o gerenciador de pacotes funciona.

Felizmente encontrei um tutorial que me aponta pra direção certa: https://cah6.github.io/technology/nix-haskell-1/.

Depois de batalhar e pesquisar um pouco acabei com a configuração presente no repositório com três arquivos (blog.nix, default.nix e shell.nix):

blog.nix descreve um pacote novo baseado no projeto usando cabal

{ mkDerivation, base, binary, bytestring, data-default, directory
, filepath, fsnotify, hakyll, http-types, lib, pandoc, pandoc-types
, process, tagsoup, text, wai, wai-app-static, wai-extra, warp
, pythonPackages
}:
mkDerivation {
  pname = "blog";
  version = "0.1.0.0";
  src = ./.;
  isLibrary = false;
  isExecutable = true;
  executableHaskellDepends = [
    base binary bytestring data-default directory filepath fsnotify
    hakyll http-types pandoc pandoc-types process tagsoup text wai
    wai-app-static wai-extra warp
  ];
  executableSystemDepends = [ pythonPackages.pygments ];
  license = lib.licenses.gpl3;
  hydraPlatforms = lib.platforms.none;
}

default.nix descreve como compilar esse projeto e define o ambiente onde fazer essa compilação

let
  pinnedPkgs = import ./pkgs-from-json.nix { json = ./release-21.05.json; };
in
pinnedPkgs.pkgs.haskellPackages.callPackage ./blog.nix { }

e shell.nix descreve que o nix-shell pode rodar dentro do mesmo ambiente que o de compilação. É aqui que você colocaria o hoogle e o haskell-language-server se precisasse.

{ }:
(import ./default.nix).env

Agora, se você quer saber como isso funciona, eu sou a pessoa errada para perguntar. Eu sei que funciona e é isso.

Um detalhe que tive que dar um jeito de implementar foi instalar o pygments dentro do ambiente de compilação do projeto. Para fazer isso eu tive que modificar o arquivo blog.nix e adicionar a propriedade executableSystemDepends contendo pythonPackages.pygments, o nome do pacote do pygments no nix.

Eu acredito que talvez esse não seja o lugar certo para esse pacote ser instalado (apesar que é sim uma dependência do projeto). Talvez eu devesse ter colocado em shell.nix, mas até eu descobrir como isso funciona vai ficar assim. E o mais importante de tudo: ele funciona desse jeito, então vamos tentar não mexer muito em time que tá ganhando.

Nix no GitLab

Uma vez que você tenha criado um ambiente funcional no nix, usá-lo no GitLab CI/CD é fácil. Isso foi tudo que precisei:

image: nixos/nix

pages:
  before_script:
    - nix-build default.nix

  script:
    - nix-shell --run 'result/bin/site build'

  artifacts:
    paths:
      - public

  only:
    - master

Simples, não? Usa a imagem nixos/nix disponível no Docker Hub (que é mínima, contendo apenas o gerenciador de pacotes - nem bash parece que vem), compila o gerador do site usando a descrição em default.nix e depois gera o site, armazenando o resultado no diretório public, que depois vai ser coletado pelo GitLab Pages e vai pra web.

Conclusão

A parte mais difícil disso aqui tudo é fazer o Nix funcionar. GitLab CI e GitLab Pages são muito fáceis e muito flexíveis. E esse post encerra a saga do Hakyll por enquanto.

É isso aí, até a próxima.