1 pontos por GN⁺ 2024-07-13 | 1 comentários | Compartilhar no WhatsApp
  • Imagens de contêiner normalmente são publicadas em registros dedicados, mas é possível usar um bucket S3 como alvo de docker pull ao expô-lo via HTTP e posicionar os arquivos nos caminhos esperados
  • No construtor de imagens customizadas, o processo de gerar uma imagem que pudesse ser baixada por pull em poucos segundos virou um gargalo, e o tempo de push das camadas representava uma parte significativa disso
  • Em um benchmark de upload de uma camada de 198 MiB, o ECR ficou em 24–28 MiB/s, enquanto o S3 marcou 115–190 MiB/s, tornando o S3 até 8 vezes mais rápido
  • A diferença vem do fato de que o S3 permite upload paralelo dos chunks de uma única camada, enquanto o ECR, baseado na OCI Distribution Spec, precisa processar os chunks de forma sequencial
  • Essa abordagem não substitui recursos de registro como docker push, validação de integridade da imagem, varredura de segurança e controle de acesso a repositórios privados, então deve ser vista como uma otimização experimental

Condições para docker pull funcionar a partir de um bucket S3

  • Para usar o S3 como se fosse um registro de contêineres, é preciso expor o bucket via HTTP e enviar os arquivos da imagem para os caminhos que o Docker espera
  • Se essas condições forem atendidas, a imagem pode ser baixada com docker pull como em um registro comum
  • A imagem de demonstração executa cowsay, e funciona com docker run --rm .../cowsay usando a URL do bucket como se fosse o nome da imagem
  • A demonstração usa Cloudflare R2
    • a saída de dados gratuita foi o motivo da escolha
    • R2 e S3 são compatíveis em nível de API, então a imagem é enviada ao R2 com o AWS SDK

Por que considerar S3 em vez de um registro dedicado

  • Imagens de contêiner normalmente são hospedadas em registros dedicados como DockerHub, GitHub Container Registry e ECR
  • O objetivo do construtor de imagens customizadas era sair dos requisitos para uma imagem disponível por pull em poucos segundos
  • Em ambiente AWS, o ECR é a escolha mais fácil, mas na prática surgiu uma grande diferença entre S3 e ECR na velocidade de upload
  • Ao adicionar traces de execução ao código para otimizar desempenho, ficou claro que o tempo de push das camadas para o registro de contêineres era um gargalo importante

Resultado do upload da camada de 198 MiB

  • Um pequeno benchmark comparou o tempo e a taxa de transferência ao enviar uma camada de 198 MiB para o ECR e para o S3
  • As velocidades observadas foram:
    • ECR: mínimo de 24 MiB/s, 8,2 s
    • ECR: máximo de 28 MiB/s, 7,0 s
    • S3: mínimo de 115 MiB/s, 1,7 s
    • S3: máximo de 190 MiB/s, 1,0 s
  • Nos resultados, o S3 foi até 8 vezes mais rápido que o ECR
  • O experimento foi executado dentro da AWS, e tanto o S3 quanto o ECR estavam conectados internamente por VPC, sem passar pela internet pública
    • essas condições oferecem a melhor latência e largura de banda possíveis

A diferença de velocidade criada pelo upload paralelo de chunks

  • O upload para o S3 pode enviar os chunks de uma única camada em paralelo
  • Com largura de banda suficiente, esse upload paralelo aumenta bastante a taxa de transferência
  • A própria documentação da AWS recomenda upload paralelo de chunks para maximizar o uso da largura de banda
  • O ECR implementa a OCI Distribution Spec
    • essa especificação é o padrão que permite que docker pull e docker push funcionem em diferentes registros
    • o push das camadas precisa ocorrer de forma sequencial, e mesmo com upload em chunks o próximo chunk só pode começar depois que o anterior terminar
  • Ao testar upload sequencial também no S3, a taxa de transferência cai para um nível parecido com o do ECR

A estrutura real das requisições de docker pull

  • Internamente, docker pull é composto por várias requisições HEAD e GET
  • Um fluxo de exemplo é:
    • verificar a existência do manifesto da imagem: HEAD /v2/my-image/manifests/latest
    • baixar o manifesto da imagem: GET /v2/my-image/manifests/latest
    • baixar novamente usando o hash do manifesto: GET /v2/my-image/manifests/sha256:...
    • baixar o blob de metadados da imagem: GET /v2/my-image/blobs/sha256:...
    • baixar o blob da camada da imagem: GET /v2/my-image/blobs/sha256:...
  • No fim das contas, docker pull é muito próximo de um processo de baixar os arquivos necessários por HTTP
  • Se um servidor de arquivos estático colocar os arquivos nos caminhos esperados e definir os cabeçalhos Content-Type adequados para cada requisição, é possível fazer pull de imagens de contêiner
  • Um bucket S3 pode atender a essas duas condições e, com uma configuração cuidadosa, funcionar como um registro de contêineres

Limites dessa abordagem experimental

  • Essa abordagem ainda é experimental, e é difícil tirar conclusões fortes antes de investigar mais
  • S3 não é, em sentido estrito, um registro de contêineres
    • não é possível fazer docker push
    • o fato de docker pull funcionar é resultado do encaixe entre a estrutura das requisições HTTP e a forma de servir arquivos estáticos
  • Registros de contêineres tradicionais oferecem muito mais recursos do que simplesmente colocar arquivos em um bucket
    • é possível confiar que imagens enviadas pelo método padrão de push são realmente válidas
    • eles podem oferecer varredura automática de segurança nas camadas e alertas
    • é possível definir permissões de acesso a repositórios privados de forma nativa
  • Se funcionar como esperado, também pode se tornar viável hospedar imagens públicas de contêiner no Cloudflare R2

1 comentários

 
GN⁺ 2024-07-13
Opiniões no Hacker News
  • É uma pena que a OCI Distribution Spec não pareça uma especificação bem projetada
    Pela especificação, o push das camadas precisa ser feito sequencialmente; portanto, mesmo ao fazer upload em chunks, é preciso esperar cada chunk terminar para passar ao próximo. Pelo que foi testado no DockerHub e no GHCR, o upload em chunks em si já está quebrado de qualquer forma, e os clientes tendem a enviar cada blob/camada inteiro de uma vez. A especificação também recomenda um formato de valor de Content-Range que não é compatível com o formato da RFC7233
    É claro que há paralelismo no nível dos blobs, mas não há paralelismo dentro de um blob. Outra reclamação é terem perdido a oportunidade de padronizar a paginação da lista de tags. Como um trecho relacionado acabou sendo removido por engano da especificação [1], cada registro passou a implementar isso à sua maneira
    [1] https://github.com/opencontainers/distribution-spec/issues/4...

    • O Docker e as tecnologias ao redor de contêineres geralmente são assim. Como experiência de usuário, o Docker é excelente; como tecnologia, chega perto de ser uma bagunça
      Não quero dizer que ele mereça ser totalmente condenado. Na prática, foi revolucionário, tornou o uso de namespaces do Linux muito mais fácil do que antes e mudou o mundo para melhor. Só que ele sempre priorizou a experiência do usuário em vez da perfeição técnica, e isso em si não é necessariamente ruim. Assim como há muitas empresas tediosas resolvendo problemas caros com Perl ou CSVs trocados por FTP, tecnologias tediosas, ou até ruins, podem ter muito valor quando entregues em um bom pacote
      Ainda assim, às vezes fica um gosto amargo de pensar que poderia ter sido muito melhor do que é hoje
    • Além disso, não sei se o problema está na especificação OCI ou se a AWS é que é peculiar, mas, diferentemente do GitLab ou do Nexus, o AWS ECR não oferece suporte à criação automática de pastas
      Por exemplo, não dá para usar algo como .dkr.ecr..amazonaws.com/foo/bar/baz:tag; só é possível armazenamento plano, o que deixa nomes de imagens ou tags excessivamente longos. Em teoria, daria para imitar algo parecido criando objetos de repositório do ECR no Terraform, mas isso não é muito bom em pipelines nos quais o caminho final da imagem é dinâmico. Seria preciso conceder ao papel IAM do pipeline de CI uma quantidade desconfortavelmente grande de permissões, e também não agrada a ideia de recursos da AWS serem gerenciados fora do repositório central do Terraform
      [1] https://stackoverflow.com/questions/64232268/storing-images-...
  • A Cloudflare já publicou como open source um servidor de registro de contêineres que usa o R2
    Fico curioso se alguém já usou
    [1]: https://github.com/cloudflare/serverless-registry

    • Parece bom. Mas consta que há um limite de 500 MB por camada
      Para alguns usos isso talvez não seja um grande problema, mas em outros pode ser um critério eliminatório imediato
  • Sou o autor do post. Se alguém souber por que, na especificação OCI, o push de camadas precisa ser sequencial, eu gostaria de saber
    Fico curioso se é apenas um acaso histórico ou se há algum motivo oculto. Para deixar claro: várias camadas obviamente podem ser enviadas em paralelo; aqui estou falando da parte em que o conteúdo de uma única camada precisa ser enviado sequencialmente

    • Talvez seja porque isso simplifique a limpeza. Se você não chegou até o “último” chunk, fica claro que, depois de N+Timeout, é um upload incompleto, então basta removê-lo
      Isso simplifica o detalhe de implementação de como lidar com uploads parciais. Caso contrário, ao fim de cada chunk seria preciso verificar se todos os outros chunks estão presentes e concluir o processamento. Mas isso é detalhe de implementação, e desconfio que tenha sido uma decisão significativa ou intencional de design. A abordagem com S3 parece que funcionaria bem, e já fiz algo parecido em uma empresa que distribuía imagens grandes no passado. US$ 0,10 por GB por mês acabou acumulando bastante
      Você perde os recursos adicionais do ECR, mas, pessoalmente, acho esses recursos bem limitados
    • Nunca lidei com push, mas essa abordagem é bem-vinda. Nos primórdios do Docker, não havia um contêiner de registro privado decente, então colocávamos as imagens atrás do nginx para fazer pull, e por isso achei o texto interessante
    • Implementei um registro compatível com OCI [1] e, como a especificação era complexa, na maior parte seguimos o comportamento da implementação de referência [2] em vez da especificação
      Quando o cliente finaliza o upload de um blob, ele precisa fornecer o digest do blob inteiro. Esse requisito parece ter o objetivo de permitir que o servidor verifique a integridade dos bytes recebidos. Se o servidor só começasse a verificação do digest na requisição HTTP final, teria que reler todo o conteúdo do blob que já escreveu no armazenamento nas requisições HTTP anteriores. Para camadas grandes, essa latência poderia ser difícil de tolerar. Por exigência de um cliente específico, validamos o funcionamento com blobs de até 150 GiB
      Em vez disso, na nossa implementação mantemos o cálculo do digest em andamento ao longo de toda a sequência de requisições. À medida que recebemos os dados do blob em chunks, calculamos o digest ao mesmo tempo em que fazemos streaming para o armazenamento de blobs. Entre cada requisição, serializamos o estado do cálculo do digest dentro da URL de upload devolvida ao cliente no cabeçalho Location. Isso é tratado mais ou menos neste código: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...
      Pelo que sei, a implementação de referência usa a mesma abordagem. Como o cálculo do digest só pode ser feito sequencialmente, o upload também precisa ocorrer sequencialmente
      [1] https://github.com/sapcc/keppel
      [2] https://github.com/distribution/distribution
    • Obrigado pelo post no blog. Você disse que “nos últimos quatro meses colaborou com a Outerbounds desenvolvendo um builder customizado de imagens de contêiner”; embora tenha dito que isso daria outro post, fiquei curioso se poderia compartilhar pelo menos alguns detalhes
      Seria ótimo até mesmo um link para um repositório no GitHub. O contexto é que estou procurando uma forma de criar imagens OCI programaticamente em $PROGRAMMING_LANGUAGE, ou pensando em implementar isso eu mesmo. Algo como o Buildah, mas com uma API para uma linguagem de programação real, não uma interface de linha de comando. Claro que dá para chamar o Buildah como subprocesso, mas é meio trabalhoso, e também é preciso se preocupar em interagir com o estado interno do Buildah ou limpá-lo; além disso, hoje o Buildah não oferece suporte a Mac
    • Não me vem à cabeça um motivo óbvio; talvez seja algo relacionado a carga
      Acho que no passado adicionei push paralelo ao Docker, mas talvez eu esteja confundindo pull com push. Revendo depois, meu trabalho foi na paralelização da verificação, não no push final. Se você especifica sobre qual camada uma camada “fica em cima”, é possível que o ID referenciado precise já existir
  • É um caso de uso bem legal
    Pessoalmente, eu simplesmente uso o Nexus. Funciona bem o suficiente e dá suporte desde imagens OCI até pacotes apt e repositórios customizados Maven, NuGet e npm. Mas a configuração e o uso de recursos são um pouco incômodos, especialmente no lado das políticas de limpeza: https://www.sonatype.com/products/sonatype-nexus-repository
    Ainda assim, é muito bom que docker pull seja “simplesmente” um conjunto de requisições HEAD e GET. Eu gostaria de ver mais tecnologias tomando esse tipo de decisão sensata: usar o que já funciona há muito tempo e não complicar à toa. É surpreendente que não existam mais armazenamentos simples de contêineres com autenticação e limpeza. Nexus e Harbor são ambos bem complexos para uso real

    • Uso Gitea apenas para pacotes. Ele lida com Docker, npm, Python etc.
      É surpreendente que ninguém nesta thread tenha mencionado isso
  • O Distribution da CNCF, antigo Docker Registry, inclui um recurso para apoiar o registro em URLs assinadas do CloudFront que buscam do S3 [1]
    https://distribution.github.io/distribution/storage-drivers/...

  • Fico curioso para saber qual é o problema com https://github.com/distribution/distribution

    • Eu não tinha visto isso antes e, de fato, ele dá suporte a S3. Mas fico curioso se ele entrega downloads diretamente do S3 ao cliente, ou se usa o S3 apenas como backend de armazenamento próprio e, na hora do pull, na prática atua como um proxy
  • Esse método parece bem caro, e teria sido bom se o texto também abordasse custos. Tenho curiosidade tanto sobre S3 quanto sobre R2

    • A camada padrão do S3 custa, por GB armazenado, um quinto do ECR
      O custo de tráfego de saída para a internet pública é o mesmo, mas repositórios ECR públicos têm a exceção de oferecer tráfego de saída gratuito para uso dentro da AWS
    • O custo, no fim, é o custo do S3. Varia conforme a região e a camada de armazenamento, mas o custo de armazenamento por GB, de GET/PUT e de largura de banda pode ser consultado no site da AWS: https://aws.amazon.com/s3/pricing/
  • Fora de ferramentas de desenvolvimento, não uso muito Docker, mas nunca entendi por que registries privados de contêineres precisam existir
    Parece simplesmente rent-seeking. Fico curioso para saber qual é a vantagem prática em relação a criar algo como um arquivo de imagem gerenciado por você mesmo e usá-lo como quiser

    • Você não é obrigado a usar. Dá para usar docker save e docker import
      docker save alpine:3.19 > alpine.tar
      docker load < alpine.tar
      Mas aí você passa a ter que gerenciar esse arquivo tar, e todos os sistemas precisam saber onde ele está e como acessá-lo. Ou então você pode não reinventar a roda e usar o método que o Docker já oferece
    • É bem provável que existam imagens que você não queira expor ao mundo. Normalmente, essas imagens precisam ser acessíveis por infraestrutura como clusters k8s ou runners de CI/CD
      Por isso, você precisa criar seu próprio registry ou pagar alguém para hospedá-lo. Claro que, se você só usa imagens para desenvolvimento, nada disso importa e basta armazená-las na máquina de desenvolvimento
    • Pelo mesmo motivo que não trocamos arquivos por e-mail em vez de usar um repositório de código
      Porque é preciso ter um repositório central com todas as versões anteriores e de fácil acesso para vários consumidores. Você não quer, depois de buildar o app, empurrá-lo manualmente para todos os lugares onde ele pode ser executado. Basta buildar uma vez, fazer push para um repositório central e fazer todo mundo apontar para esse repositório
      Também não é obrigatório pagar por hospedagem de repositório privado. Há muitas ferramentas que você pode hospedar por conta própria
    • Registries privados em nuvem são muito úteis em projetos que têm requisitos obrigatórios de autenticação/autorização relacionados a imagens Docker
      É possível configurar tudo por ambiente com Terraform, Bicep ou Pulumi
    • A questão também é como gerenciar isso. Se quiser usar as mesmas ferramentas usadas para imagens públicas, basta operar um container registry
  • O ECR realmente parece ter sido projetado para permitir o upload de camadas de imagem em várias partes
    Entre as APIs relacionadas do ECR estão a InitiateLayerUpload API, chamada ao iniciar o upload de cada camada de imagem; a UploadLayerPart API, chamada para cada chunk da camada (máximo de 20 MB); e a PutImage API, que faz push do manifesto da imagem contendo as referências às camadas depois do upload da camada. O estranho é que os chunks das camadas precisam ser enviados com codificação base64, o que aumenta os dados em cerca de 33%

    • Já usei essa API diretamente e, infelizmente, ela também exigia upload ordenado
  • A ideia de usar a disposição dos caminhos de arquivo como meio de controle de endpoints é interessante
    Mas fico curioso sobre como lidar com o cabeçalho Docker-Content-Digest. Ele não é obrigatório, mas é recomendado incluí-lo nas respostas, muitos clientes esperam isso e podem rejeitar camadas que não tenham esse cabeçalho. Além disso, você pode acabar deixando de fora recursos como a referrers API da especificação OCI 1.1. A implementação parece ser bem complicada