4 pontos por GN⁺ 2024-03-17 | 1 comentários | Compartilhar no WhatsApp
  • Se você já usa imagens Docker como unidade de implantação, o Nix pode ser um ponto de entrada para experimentar builds determinísticos e fixação de dependências sem mudar drasticamente o workflow
  • Um docker build comum tende a depender do estado da internet pública, como repositórios Ubuntu ou downloads externos, o que dificulta reproduzir a mesma imagem algum tempo depois
  • O Nix conhece as dependências necessárias no nível de pacote, então com dockerTools.buildLayeredImage é possível configurar o reenvio apenas das camadas alteradas
  • O exemplo baseado em Go douglas-adams-quotes mostra que é possível gerar o binário com pkgs.buildGoModule e carregá-lo para implantação como uma imagem Docker comum com nix build .#docker e docker load < ./result
  • Se vários serviços em um monorepo compartilham camadas e o cache do Nix, é possível reduzir tempo e custo de build em reproduções de imagens de commits antigos e em implantações repetidas

Por que acoplar o Nix ao build de imagens Docker

  • O Nix reúne três características: gerenciador de pacotes, linguagem e sistema operacional
    • Você escreve instruções de build de pacotes na linguagem Nix
    • O gerenciador de pacotes Nix pode, com base nessas instruções, criar software, ferramentas, imagens NixOS, imagens de contêiner e mais
  • Não é fácil introduzir o Nix em pipelines CI/CD já existentes
    • O Nix é diferente da forma com que muitos desenvolvedores estão acostumados
    • A barreira de adoção é grande, a menos que seja um ambiente novo, como uma startup recém-criada ou um homelab
  • Organizações que já usam Docker podem começar aplicando o Nix ao build de imagens de contêiner para experimentar sem quebrar muito o fluxo atual de implantação

Onde o build do Docker oscila

  • Docker e conteinerização foram amplamente adotados, e imagens Docker são usadas como um formato de pacote universal de fato na internet
    • Plataformas como Fly.io, Railway e Render usam imagens Docker em vez de imagens arbitrárias de VM ou programas Linux em tarball
  • Builds com Docker nem sempre são determinísticos
    • A maioria dos Dockerfiles vistos na internet costuma funcionar, mas a pequena porcentagem que falha pode virar problema operacional
  • Um dos maiores pontos frágeis é que o build do Docker acessa a internet pública
    • Isso é necessário para baixar pacotes de repositórios Ubuntu
    • Quando você precisa recriar a mesma imagem depois, é difícil restaurar exatamente o estado dos repositórios Ubuntu daquela época
    • O Ubuntu 18.04 está próximo do fim do suporte em seu respectivo ciclo, então você pode só descobrir o que dependia dessa versão depois que ocorrer uma falha
  • Adicionar pacotes a uma imagem Docker de forma simples pode gerar desperdício de espaço
    • Executar apt-get upgrade no começo do build pode substituir arquivos dentro da imagem de contêiner
    • Os arquivos anteriores à substituição podem permanecer na camada, acumulando-se como cópias sombreadas

Como o Nix lida com camadas Docker

  • O Nix conhece de antemão as dependências necessárias e pode dividi-las no menor número possível de camadas Docker
    • Alterar uma linha de código não exige que apt ou npm reinstalem dependências
    • Só o mínimo que realmente mudou precisa ser refletido na atualização da imagem
  • dockerTools é um conjunto de ferramentas para colocar pacotes Nix e suas dependências dentro de imagens Docker
  • As imagens Docker criadas com Nix se dividem em dois grandes modos
    • Imagem sem camadas: coloca o programa, dependências e itens extras como certificados raiz TLS em uma única pasta e os expõe como uma imagem de camada única
    • Imagem em camadas: coloca cada dependência em uma camada separada da imagem, permitindo enviar apenas o que realmente mudou
  • O próprio Docker também tem armazenamento baseado em conteúdo, mas é difícil aproveitá-lo bem apenas com docker build
    • As imagens em camadas do Nix usam camadas por pacote, então dependências como glibc só precisam ser enviadas uma vez
    • Porém, se essa biblioteca precisar de modificação, essa camada terá de ser reenviada

Exemplo de serviço Go: Douglas Adams Quotes

bin = pkgs.buildGoModule {
  pname = "douglas-adams-quotes";
  inherit version;
  src = ./.;
  vendorHash = null;
};
  • Essa definição usa o template de módulos Go para configurar o compilador Go, o compilador C para CGo e o download de dependências externas
    • pname é o nome do pacote
    • version é gerada automaticamente a partir do commit Git do serviço
    • src = ./.; usa o código-fonte do diretório de trabalho atual
    • Se não houver dependências além da biblioteca padrão, vendorHash = null pode ser usado
    • Se houver dependências externas, você pode especificar o hash de todas elas ou usar gomod2nix
  • O pacote é compilado com o seguinte comando
nix build .#bin
  • A imagem Docker em camadas pode ser criada com dockerTools.buildLayeredImage
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • Ao apontar config.Cmd para o binário compilado do servidor, os itens necessários são copiados junto
    • glibc e outros componentes necessários para execução também são incluídos
  • Pacotes extras, como certificados raiz de CA, podem ser adicionados em contents
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  contents = with pkgs; [ cacert ];
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • A imagem é criada e carregada no daemon do Docker com os seguintes comandos
nix build .#docker
docker load < ./result
  • Ao abrir com dive, cada camada adiciona pacotes diferentes do nixpkgs, e no fim os itens são vinculados simbolicamente à raiz da imagem

O efeito de compartilhar camadas em um monorepo

  • Se houver vários serviços no mesmo repositório, as imagens Docker criadas com Nix podem compartilhar camadas
    • Isso acontece sem configuração extra
    • Fazer isso só com Docker costuma levar à criação de várias imagens base comuns, que podem incluir ferramentas e conteúdo desnecessários para alguns serviços
  • Como exemplo, o repositório Xe/x é um monorepo com 10 anos de side projects, experimentos e ferramentas
    • Vários projetos são implantados em cerca de três plataformas
    • O trabalho vinha sendo feito para convergir para uma base comum de implantação usando imagens Docker
  • Ao enviar a atualização de um serviço, parte da atualização compartilhada com a maioria dos outros serviços também é enviada
    • Essa abordagem economiza tempo e dinheiro em vários projetos

O que é difícil acompanhar apenas com cache do Docker

  • Em teoria, usando cache do Docker, também seria possível construir imagens com eficiência comparável à do Nix
  • Mas obter o mesmo efeito com a abordagem do Docker pode tornar as etapas de build difíceis de manter
    • É preciso instalar bibliotecas compartilhadas em camadas separadas
    • É necessário reativar a pilha de rede durante o build, o que enfraquece a reprodutibilidade
    • Também é preciso reajustar caminho de busca, flags do compilador e configurações relacionadas a CGo
  • dockerTools.buildLayeredImage do Nix lida por você com os detalhes de colocar pacotes no contêiner, simplificando a configuração

Reprodução de momentos passados e cache do Nix

  • Uma das maiores vantagens do Nix é poder recompilar exatamente o software de um ponto específico no passado
    • Quando for necessário reproduzir depois um bug em um ambiente específico de cliente, será possível recriar a mesma imagem Docker
    • O build do pacote serve para fixar todo o estado do software e das dependências em um momento específico
  • XeDN é um projeto desenvolvido há anos, e a versão de 14 meses atrás pode ser compilada com o seguinte comando
nix build github:Xe/x/567fdc2#xedn-docker
  • Esse comando compila o alvo xedn-docker de um commit específico do repositório GitHub Xe/x
    • Ao carregá-lo no daemon do Docker, é gerada uma imagem com os mesmos bytes daquela época
    • O exemplo inclui até o Go 1.19
  • Reproduzir o mesmo resultado apenas com builds Docker comuns pode exigir muito custo de armazenamento
  • O cache do Nix armazena a saída de comandos Nix para evitar recompilar depois
    • Assim, não é preciso recompilar pacotes como nokogiri em cada notebook de desenvolvedor
    • É possível receber o resultado já compilado na nuvem
  • O Garnix é usado no CI de todos os projetos com flakes e informa o status de build a cada commit
    • Configurações de máquinas de homelab também são compiladas com Garnix, de modo que, nas atualizações noturnas, a configuração mais recente é obtida do cache do Garnix em vez de ser recompilada localmente

Conclusão como artefato de implantação

  • O Nix pode ser considerado um construtor de imagens Docker melhor do que o construtor de imagens do Docker
  • O Nix faz você especificar o resultado, em vez de listar manualmente os detalhes do procedimento para chegar até ele
  • Em ambientes que já usam Docker, começar pelo build de imagens Docker com Nix é um caminho realista de adoção
  • Imagens Docker criadas com Nix podem compartilhar camadas entre várias partes de um monorepo
  • Graças ao cache binário, não é necessário recompilar código que já foi compilado no passado
  • O artefato final continua sendo uma imagem de contêiner comum, implantável em plataformas como AWS, Google Cloud e Fly.io

1 comentários

 
GN⁺ 2024-03-17
Opiniões no Hacker News
  • Tentei gostar do Nix várias vezes, mas agora sinto que preciso desistir
    Tenho 2 sistemas rodando Nix e morro de medo de mexer neles; no passado, os dois já quebraram e precisei reinstalar tudo do zero. Em teoria, o Nix é idempotente e determinístico, mas, na prática, você precisa entender determinístico em relação a quê; se não conhecer a fundo o comportamento de todas as partes de que depende, acaba encontrando resultados estranhos, erros bizarros e nada úteis, ou então situação sem nenhum feedback
    A documentação, mesmo quando completa e tecnicamente correta, é terrivelmente obscura, e os tutoriais só te levam até 80% do caminho. Se você sai dali, precisa construir por conta própria, e isso exige anos de conhecimento acumulado e frustração, algo que não quero mais encarar. A vantagem do Docker é justamente o caos em si: sabendo mais ou menos shell e o gerenciador de pacotes da distribuição, dá para montar quase qualquer coisa; quando algo dá errado, ferramentas com décadas de história tornam o diagnóstico e a correção muito mais fáceis
    O Nix é como o Emacs: com paciência e conhecimento profundo e visionário, dá para fazer qualquer coisa, mas parece que as únicas opções são mergulhar completamente nele ou observá-lo de longe

    • Passei pelo mesmo caminho e gosto de builds determinísticos, mas acho que o maior problema do Dockerfile é que ele parece determinístico para o desenvolvedor médio
      Se você fizer build duas vezes seguidas na mesma máquina, o resultado sai igual; mas, com o tempo, quando o gerenciador de pacotes e as tags das imagens base forem atualizados, o resultado de um rebuild um mês depois será completamente diferente. Quando a equipe gerencia uns 40 contêineres, consertar contêineres vira uma parte grande do trabalho
      Então, em teoria, o Nix deveria ser perfeito, mas ele é diferente demais: ferramentas de fornecedores não rodam no Nix e, quando aparece um erro, é difícil encontrar rapidamente uma solução na web. Por causa dessa frustração, criei https://www.stablebuild.com, que oferece builds determinísticos baseados em Docker sobre contêineres Ubuntu, Debian e Alpine. Ele é composto por um cache pull-through imutável do Docker Hub, cópias completas diárias dos repositórios de pacotes do Ubuntu/Debian/Alpine, cópias diárias de PPAs populares e do índice PyPI, além de um cache imutável de arquivos/URLs arbitrários
      Na prática, é fácil de usar, fácil de depurar e tem ampla compatibilidade de software; nos contêineres migrados para o StableBuild, houve 0 problemas causados por não determinismo
    • A documentação não é apenas obscura; especialmente no novo CLI e em flakes, muitas vezes ela simplesmente está errada, e nem são edge cases
      Já gastei tempo tentando entender por que nix develop não se comporta como a documentação diz e como fazê-lo se comportar daquele jeito. A documentação diz que, por padrão, você entra no ambiente de tempo de build, mas o padrão real não é isolado; os nomes das opções de linha de comando também são confusos, então tive de extrair conhecimento do código-fonte
      Também gostaria que os edge cases que quebram a reprodutibilidade fossem tratados de forma mais explícita. Código de ponto flutuante é sensível à ordem das operações, e o estado pode vazar por causa da preempção do sistema operacional; se nem o óbvio for explicitado, as pessoas acabam dando um tiro no próprio pé
    • Não é tão ruim assim, mas mesmo em uma configuração padrão do NixOS ainda sobra uma quantidade enorme de estado não reprodutível nas contas de usuário e dentro do sistema
      Usando a configuração “Erase your darlings”, dá para eliminar a maior parte do estado não reprodutível fora das contas de usuário. É meio trabalhoso, mas o que no NixOS não é trabalhoso?
      https://grahamc.com/blog/erase-your-darlings/
      Não me preocupo com o interior das contas de usuário, e não gosto do Home Manager
    • No trabalho uso tanto Docker quanto NixOS, mas nunca encontrei os problemas mencionados acima
      Docker é ok, mas no Mac o desempenho não é grande coisa; já o Nix é bom porque torna trivial instalar e fazer as coisas funcionarem do mesmo jeito em várias máquinas. A documentação do Nix é péssima, mas o ChatGPT-4 foi excelente para resolver problemas com Nix
      Tenho a impressão de que 90% dos problemas com Nix surgiram porque eu estava tentando fazer coisas que não eram do “jeito Nix”
    • Vale a pena experimentar uma distribuição imutável como o Fedora Atomic
      Já usei praticamente todos os gerenciadores de pacotes de distribuições e, mesmo sem fazer nada especial, consegui quebrar todos eles de alguma forma. O Fedora Kinoite tem aguentado bem adicionar/remover camadas, atualizações diárias e até rebases a partir do Silverblue. Pessoalmente, acho que o rpm-ostree vai acabar substituindo o Nix
  • Vejo o Nix e o NixOS em um estado parecido com o do git antes do GitHub.
    A ideia básica se apoia em ciência da computação mais séria do que as abordagens existentes, como SVN ou Docker, e o encanamento interno ainda tem problemas, mas não é pior; porém as ferramentas e a documentação voltadas ao usuário ainda não estão no nível de adoção mainstream.
    Com o lançamento do flox, isso pode ter mudado: https://flox.dev. É quase fluido, e, vindo de ex-DE Shaw, isso nem surpreende.
    O Nix não faz muito sentido sem flakes e nix-command, mas ambos estão documentados como experimentais e vêm desativados por padrão. A documentação está melhorando, mas ainda é insuficiente, e nixlang é incrível quando você aprende bem, mas não é aceitável como barreira de entrada para o mainstream. nix-env -iA foo na prática quase nunca é o comportamento que você quer, então o Nix está mais para uma arma secreta de empresas que conseguem bancar expertise interna do que para o gerenciador de pacotes do jeito que ele é anunciado.
    O flox reduz a barreira para “experimentar uma vez e imediatamente ter uma experiência melhor”. O Nix/NixOS, ou algo parecido, acabará empurrando o Docker para trás como o Git fez com o Subversion, mas isso não vai acontecer até surgir um momento tipo GitHub — e então vai acontecer de uma vez.
    A maioria das reclamações sobre Nix nesta thread está tecnicamente errada, mas é totalmente compreensível e, mais importante, não é culpa do usuário. Sei que está diminuindo a geração que conheceu um mundo sem git/GitHub, mas seria bom ouvir Linus explicando, diante de pessoas que passaram pelos critérios iniciais de contratação do Google, por que elas deveriam se interessar por uma ferramenta que parece complicada.
    https://youtu.be/MjIPv8a0hU8?si=QC0UnHXRdMpp2tI4

    • Aqui é o Ron, do flox.dev; este post fez a equipe inteira rir bastante.
      Estamos construindo isso há algum tempo, e gostaríamos de ouvir se há algo que deveríamos priorizar para torná-lo melhor.
    • Acho que, para uma ferramenta de desenvolvedor ter sucesso, nas tarefas mais comuns um engenheiro precisa ter pelo menos três maneiras de usar a ferramenta errado e ainda assim conseguir “terminar”, mesmo deixando dívida técnica implícita para trás.
      O git é assim, mas o Nix ainda não é, então não tenho tanta certeza de que um momento tipo GitHub vá ajudar.
  • Este post do blog deixa de explicar por que camadas Docker compartilhadas são úteis.
    O motivo é cache: quanto mais imagens compartilham as mesmas camadas, mais coisas podem ser cacheadas, tornando a inicialização de contêineres mais rápida.
    O Docker é fraco nisso porque, para obter os benefícios de cache, ao criar uma imagem você precisa produzir camadas o mais iguais possível às camadas existentes. Por exemplo, se você executar apt-get install python3 hoje e não houver novas atualizações, deveria sair exatamente a mesma camada de ontem, mas camadas Docker são cacheadas por hash de arquivo, então todos os arquivos precisam ser exatamente iguais, inclusive metadados como hora de criação.
    O Nix já armazena dependências por hash, então, com a mesma versão e a mesma configuração, a camada sempre será a mesma.

    • Acho mais correto expressar assim: o formato Dockerfile força uma relação hierárquica entre camadas.
      Dependências normalmente formam um grafo, não uma árvore, então isso fica inconveniente rapidamente. Ferramentas alternativas como Nix, e talvez Bazel, não têm essa restrição e conseguem mapear o grafo de dependências para camadas Docker para obter cache granular. É algo que não dá para expressar com Dockerfile.
    • No Docker, se uma camada estiver em cache, a camada que contém apt-get não é invalidada automaticamente.
      Ela só muda se você usar --no-cache ou se houver alguma alteração em uma camada anterior.
  • Passei os últimos 2 ou 3 dias tentando fazer build de imagens Docker no Darwin, e este texto parece o universo tirando sarro de mim
    Nix é claramente a melhor ferramenta para o objetivo desejado, mas tem alguns cantos escuros e abandonados que sugam a alma. Eu gosto dele, mas às vezes me sinto como o Morty arrastado para as aventuras do Rick na terra dos compiladores

    • O grande problema é o próprio design do Docker
      Docker, em essência, é mais uma prisão para binários Linux. No Linux, você compila o binário, coloca no contêiner e pronto; o código Nix também é simples. Se você consegue compilar o código, criar o contêiner é só mais uma etapa
      Mas Docker exige binários Linux e, no Mac, o Docker Desktop sobe uma VM Linux, faz todo o trabalho dentro dela e depois esconde esse fato. O Nix não faz isso, então há duas opções
      A primeira é fazer compilação cruzada, mas isso exige que até a glibc consiga ser compilada de forma cruzada; mesmo que a maioria das dependências da comunidade funcione, alguns pacotes podem ter autores que não se preocuparam com cross-compilation. Além disso, o Hydra, que preenche o cache padrão do Nix, não faz builds com compilação cruzada, então você pode compilar por muito tempo e acabar falhando
      A segunda é conectar um builder Linux ao Mac e enviar para ele os trabalhos de build x86_64-linux. Pode ser uma máquina física, uma VM ou até um contêiner Docker com NixOS
      A opção 1 parece o jeito correto, mas a 2 é mais prática. Os problemas que você está enfrentando provavelmente vêm de tentar a opção 1, e isso exige bastante experiência não só com Nix, mas também com cross-compilation. Seria bom se o Hydra também fizesse builds de cross-compilation de Darwin para Linux, fornecendo cache e evitando quebras, mas isso também aumentaria os custos. Parece melhor tentar a solução 2
      Acho que havia uma solução oficial: https://ryantm.github.io/nixpkgs/builders/special/darwin-bui...
    • Com Orbstack, isso funciona impecavelmente
      Em algumas stacks, a compilação cruzada não é muito robusta, então compilamos de forma cruzada {aarch64,amd64} x {linux,darwin} com Docker. Estamos rodando vários Dockers sobre Darwin aarch64 para compilar tudo, e a experiência foi boa
    • Será que você está falando desse tipo de aventura de 20 minutos?
      https://github.com/gytis-ivaskevicius/high-quality-nix-conte...
    • O macOS é definitivamente mais áspero; lá uso colima, e funciona razoavelmente bem
      Há um ou dois bugs, mas parecem ser principalmente relacionados a volumes, e ele lida bem com builds de imagens Docker. A parte mais problemática é a velocidade, porque a combinação do hardware com o fato de o Docker precisar emular uma VM Linux acaba dando um baque
  • A experiência de criar uma imagem Docker para uma aplicação Java com Nix não foi muito boa
    Depois que o gradle2nix foi descontinuado, não parece haver uma alternativa clara para aplicações Java baseadas em Gradle. Certa vez pedi a um amigo para tentar criar a menor imagem Docker possível de uma aplicação Spring Boot simples, e o resultado feito com Nix ficou com o dobro do tamanho da imagem feita sem Nix
    O código pode ser visto aqui: https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • O motivo é que entram dois JDKs
      Entram juntos o zulu e o JDK que o gradle inclui como argumento jdk. Se você olhar o gradleGen no nixpkgs, dá para entender o que isso significa. Desculpa pelo gradle2nix; estou trabalhando em uma melhoria menos “gambiarra”
    • Não uso Java há mais de 10 anos, então não consigo ajudar muito, mas já coloquei uma aplicação em um contêiner de 70 MB, incluindo Python e todas as dependências, busybox e tini
      Era mais ou menos assim: https://gist.github.com/takeda/17b6b645ad4758d5aaf472b84447b...
      Fiz link de tudo com musl, compilei o Python desativando todos os pacotes que a aplicação não usava e removi partes não utilizadas do boto3/botocore. Só boto3/botocore já passa de 100 MB
      Pacotes Nix, por padrão, miram o sistema operacional NixOS, então em situações comuns, com bastante espaço em disco, a expectativa é que todos os recursos estejam ativados. Por isso muitas dependências desnecessárias vêm junto. Já as imagens Alpine são projetadas para Docker, e o objetivo é desativar recursos extras nos pacotes, então o resultado é menor
      Para criar imagens pequenas, é preciso desligar o que não é necessário com override. Por exemplo, o zulu inclui coisas como alsa, fontconfig, freetype, xorg, cups, gtk, cairo e ffmpeg. Seu amigo extraiu cuidadosamente só os arquivos necessários e os colocou no contêiner, enquanto do lado do Nix foi incluído o pacote zulu inteiro com todas as dependências
      Primeiro corrija para incluir apenas um JDK; depois dá para reduzir ainda mais o tamanho do JDK com os métodos acima. Usar openjdk_headless talvez seja mais simples
      https://github.com/NixOS/nixpkgs/blob/master/pkgs/developmen...
    • Para um contêiner OCI Java mínimo, é difícil superar o jib
      https://github.com/GoogleContainerTools/jib/tree/master/jib-...
    • openjdk_headless pula as dependências de GTK e X que o Spring não precisa
    • Participei diretamente do desafio e dei uma arrumada no código Nix; o ponto central parece ser criar um JRE bem pequeno
      Em vez do Zulu, mudei para usar como base o build headless do OpenJDK do Nixpkgs, removendo dependências de bibliotecas de GUI, e criei um JRE mínimo customizado com pkgs.jre_minimal e jlink
      O tamanho da imagem ficou em 161 MB, um pouco maior que a imagem demo_jlink. Isso porque o JRE fica com cerca de 90 MB ao incluir todos os módulos realmente necessários para executar a aplicação. A chamada a jdeps no Dockerfile_jlink não detectava todos os módulos e estava criando um JRE apenas com java.base. Se eu colocar apenas java.base no meu JRE mínimo, o tamanho do JRE fica em cerca de 50 MB, e a imagem de contêiner quebrada fica com 117 MB no Podman
      Também removi o copyToRoot incorreto na chamada a dockerTools.buildImage. Só o contexto de string de config.Cmd já coloca o app na imagem, mas o código anterior estava copiando o app mais uma vez. Além disso, troquei para dockerTools.buildLayeredImage, colocando cada store path em uma camada de imagem separada; isso ajuda na escalabilidade de espaço ao compartilhar dependências entre várias imagens de contêiner, mas não afeta um experimento com uma única imagem
      O que resta é, em grande parte, otimizar o tamanho do JRE. A glibc é a próxima maior dependência, com cerca de 30 MB, aparentemente porque o Nixpkgs compila a glibc para oferecer suporte a muitos locales e codificações de caracteres. Não sei se é possível, nem se seria prático, separar isso em uma derivation ou output à parte para tornar a escolha opcional. Se várias imagens forem criadas com dockerTools.buildLayeredImage, partindo do pressuposto de que usam o mesmo commit do Nixpkgs, a glibc e o restante das dependências serão todos compartilhados
      https://github.com/max-privatevoid/hackernews-docker-challen...
  • Se você já adotou Nix, ótimo; seria bom ver o gerenciamento declarativo de pacotes, como Nix ou Guix, se espalhar mais
    Se você já usa Docker, mas quer introduzir Nix gradualmente, também existe a abordagem desta apresentação: https://youtu.be/l17oRkhgqHE
    Em vez de migrar de uma vez a configuração e o build de contêineres para Nix, dá para manter o Dockerfile e fazer com que ele construa a configuração do Nix. A maior desvantagem é que não se aproveitam as camadas de forma alguma; a vantagem é poder mudar o Dockerfile gradualmente enquanto se reutiliza a infraestrutura ou a automação Docker existente

    • Este texto também usa a mesma abordagem
      [1] https://mitchellh.com/writing/nix-with-dockerfiles
    • Um dos eixos do texto é que builds com Docker não são reproduzíveis, enquanto com Nix são
      Mas me parece que boa parte dessa não reprodutibilidade vem do fato de não haver garantia de que uma das camadas do Docker continuará disponível. Nesse caso, também fico curioso se no Nix há garantia de que as versões dos pacotes permanecerão no repositório para sempre
  • Meio do nada, mas a ilustradora Annie Ruygt, que fez as imagens destes slides, também criou os logotipos e a arte de marca de duas startups da YC: RethinkDB (rethinkdb.com) e Pachyderm (pachyderm.io)
    Trabalhei na Pachyderm, que foi fundada por ex-engenheiros da RethinkDB. O trabalho dela é realmente muito bom

  • Há relativamente pouco tempo, por recomendação da equipe de infraestrutura, passei mais ou menos meio dia tentando criar imagens-base de CI com Nix; a imagem ficou enorme e algumas partes não funcionavam por problemas de linkagem
    O que me irritou especialmente foi que, ao criar imagens multi-arquitetura, ele tentava realmente executar alguma coisa de outra arquitetura, e só dava suporte à virtualização de hardware do qemu. A máquina de build e a workstation são VMs, então não têm isso, embora tenham binfmt-misc. Se ele tivesse feito fork/exec de um mkdir arm64 para criar /tmp, teria funcionado; mas uma camada Docker é apenas um arquivo tar, então dá para criar o diretório assim
    echo "tmp uid=0 gid=0 time=0 mode=0755 type=dir" | bsdtar -cf - @-
    É bem provável que essa camada exata já exista em algum lugar, então talvez o usuário nem precisasse baixá-la
    Toda vez que tento Nix, fico com a sensação de que, em mais alguns meses, talvez dê para usá-lo regularmente. O nixpkgs tem quase todos os pacotes que quero e instala bem na workstation. Mas uma exigência como “preciso de bash, python, build-essential e Bazel” não parece ser o objetivo de um builder de imagens Docker. Para colocar um único binário Go em uma imagem Docker, não preciso de Nix. Basta pegar distroless e colocar a aplicação em um arquivo tar, e isso vira um contêiner. Pessoalmente uso rules_oci do Bazel; internamente ele faz mais ou menos isso, com só um pouco mais de inteligência para construir binários para várias arquiteturas, criar o YAML do índice de imagens e fazer push para o registry

    • Deveria ser possível fazer compilação cruzada de binários para outra arquitetura sem realmente executá-los
      Claro, os arquivos de build do pacote precisam dar suporte a isso. Também não me parece correto dizer que o qemu só dá suporte à virtualização de hardware. Dá para usar qemu até em arquiteturas nas quais só é possível emulação por software
      Um exemplo mínimo é abordado aqui: https://discourse.nixos.org/t/how-do-i-get-a-shell-nix-with-...
      Não quero chegar a dizer que deveria ser tão simples quanto usar pkgCross, mas fico curioso sobre quais problemas concretos você encontrou no processo normal
      https://nix.dev/tutorials/cross-compilation.html
    • Você provavelmente está falando da imagem Docker oficial do Nix, que de fato é grande
      Já a usamos em alguns projetos por anos, mas, se o tamanho for um problema, é possível criar imagens bem pequenas contendo apenas o que foi especificado, do modo mencionado no texto
      [1] https://hub.docker.com/r/nixos/nix/tags
    • Compilação cruzada para imagens Docker foi justamente o uso que fiz do Nix
      Usei até musl; ele construiu em CI, de forma rápida e consistente, as menores imagens que já consegui com qualquer ferramenta, e o cache também funcionou bem. Não entendo muito bem o que teria sido executado
    • Fico curioso para ver como ficaram os arquivos finais de Nix e Docker, e se foi preciso usar buildFHSEnv para dar suporte a algum binário estranho de terceiros
      Parece que o Nix precisa de mais textos explicando como fazer uma transição suave, por partes, a partir de sistemas existentes
  • Como engenheiro de plataforma, eu gostaria de gostar do Nix, mas ele não é fácil para todo mundo
    Acho que a experiência do desenvolvedor ainda é bem ruim. Por exemplo, prefiro a experiência do devbox, porque dá para adicionar pacotes com algo como devbox add python@3.11
    Ao olhar um flake.nix de 120 linhas, também fica difícil dizer exatamente que é “mais fácil”
    https://github.com/Xe/douglas-adams-quotes/blob/main/flake.n...

    • Essa comparação é um pouco injusta
      O flake linkado define como compilar um binário Go, define uma imagem Docker que usa esse binário como ponto de entrada e também define um módulo NixOS que cria um serviço systemd para executar o binário Go. Além disso, inclui até um teste NixOS que verifica se esse módulo NixOS cria o serviço systemd conforme esperado
      O framework de testes do NixOS é bem impressionante, e os testes rodam dentro de uma VM QEMU executando NixOS junto. O que tem relação com o artigo linkado é apenas a definição do binário Go e da imagem Docker, além de parte do boilerplate ao redor
    • Essas 120 linhas dificilmente podem ser vistas como um exemplo representativo e, se fosse um único serviço, provavelmente teria sido escrito inline, sem criar um novo módulo
      Além disso, muitas linhas correspondem 1:1 à descrição inline do serviço systemd, então elas não desapareceriam independentemente do sistema usado. Também há ali uma forma elegante de declarar vários sistemas com overrides
      Esse exemplo está mais para “vamos fazer uma coisa trivial como faríamos em um projeto grande e sério”, e, se fosse tratado como algo pontual, acho que poderia ser reduzido para cerca de 40 linhas
    • Essas 120 linhas fazem bastante coisa
    • São 120 linhas de código para lidar com um único binário e uma configuração systemd
      Esse binário e esse arquivo de configuração são a coisa mais próxima de um “flat pack” que se pode obter no Linux, e este exemplo mostra bem o que deixamos passar entre a floresta e as árvores
  • O Guix também tem uma opção Docker fácil e boa chamada guix pack -f docker
    O Guix também tem a vantagem de usar uma linguagem já existente, Guile/Scheme, em vez de uma linguagem própria dedicada
    [1] https://guix.gnu.org/manual/en/html_node/Invoking-guix-pack....