Com o lançamento de acesso antecipado do Docker Scout , o Docker Hub está finalmente começando a visualizar imagens internas. Isso é ótimo! Por que o Docker Hub não fez isso anos atrás? Por causa de seu modelo de negócios.
Antes de prosseguirmos, vamos revisar brevemente do que são feitas as imagens do Docker.
Quando você baixa uma imagem do Docker executando docker pull
na linha de comando, a CLI do Docker exibe o progresso do download de cada camada à medida que extrai a imagem. Se você já baixou uma imagem do Docker, provavelmente já a viu:
Se você já trabalhou com Docker antes, provavelmente já definiu algumas dessas camadas para suas próprias imagens. Os desenvolvedores definem implicitamente essas camadas quando escrevem Dockerfiles. Por exemplo, cada linha neste Dockerfile produz uma camada:
FROM ubuntu:22.04 # install apt dependencies RUN apt-get update RUN apt-get install -y iputils-ping python3 python3-pip # install python dependencies RUN pip3 install numpy # cleanup apt lists RUN rm -rf /var/lib/apt/lists/* CMD ["/bin/bash"]
Camadas são diretórios arquivados do Linux – são tarballs do sistema de arquivos. O Docker baixa todas as suas camadas de imagem e descompacta cada uma delas em um diretório separado. Quando você inicia um contêiner a partir de uma imagem do Docker usando o comando docker run
, o daemon do Docker combina as camadas da imagem para formar um contêiner.
Grande parte do valor agregado do Docker como produto é que ele abstrai esses detalhes, permitindo que os usuários aproveitem os benefícios dos contêineres em camadas sem pensar em como eles funcionam. Mas todas as abstrações vazam, e o Docker não é exceção – às vezes, você precisa abrir a cortina.
As camadas de imagem do Docker contêm a história de origem de cada binário presente no sistema de arquivos do contêiner. A primeira linha em uma imagem Docker é “a linha FROM”. Ele define a imagem do Docker (e, portanto, as camadas da imagem) sobre a qual o Dockerfile é construído.
Ao inspecionar as camadas do Dockerfile atual e as camadas de sua cadeia de imagens pai, os desenvolvedores podem determinar de onde veio cada arquivo no sistema de arquivos raiz de um contêiner. Esta é uma informação muito valiosa. Ajuda os desenvolvedores:
Imagine clicar nas camadas de uma visualização para rastrear alterações de arquivos nas versões de imagem do Docker. Quando uma verificação de segurança automatizada identifica uma vulnerabilidade em uma de suas imagens, imagine usar uma ferramenta de inspeção de camada para identificar como a vulnerabilidade foi introduzida.
Tamanhos excessivos de imagem podem custar muito dinheiro às empresas. Muitos pipelines de CI/CD extraem imagens do Docker para cada solicitação pull, portanto, downloads demorados de imagens do Docker podem desacelerar os pipelines, tornando os desenvolvedores menos eficientes e desperdiçando tempo de CPU. Como as organizações normalmente pagam os custos de infraestrutura por hora, cada hora desperdiçada é uma despesa desnecessária.
Além do desperdício de recursos de computação, as imagens inchadas do Docker podem levar a custos excessivos de transferência de rede. Se uma organização fizer download de imagens do Docker em regiões da AWS ou da AWS para a Internet aberta, o inchaço da imagem do Docker se traduzirá diretamente em desperdício de gastos com infraestrutura. Isso aumenta rapidamente.
É fácil introduzir acidentalmente inchaço nas camadas de imagem do Docker. O Dockerfile descrito anteriormente contém um exemplo clássico - a camada da linha 4 salva 40 MB de arquivos no disco, que são excluídos posteriormente pela camada da linha 11. Devido à forma como as imagens do Docker funcionam, esses dados ainda fazem parte da imagem, adicionando 40 MB de tamanho de imagem desnecessário.
Este é um exemplo simples - na verdade, foi retirado diretamente da documentação do Docker. Em Dockerfiles mais complexos, esse erro pode ser muito mais difícil de detectar.
Pode ser difícil interagir com as camadas de imagem do Docker usando a linha de comando, mas antes do Docker Scout ser lançado recentemente, a linha de comando era onde você encontraria o que há de mais moderno. Aqui estão as duas abordagens básicas.
Este é o método Unix simples e faça você mesmo. Tudo que você precisa é de um host executando um daemon Docker. É uma abordagem simples:
docker create
para iniciar um contêiner a partir da imagem.docker inspect
para encontrar os diretórios de camadas do novo contêiner.cd
nesses diretórios na linha de comando e raciocine sobre as camadas em sua cabeça.
Isso é um incômodo. Digamos que estejamos tentando rastrear algum inchaço de imagem em uma imagem do Docker que estamos usando há alguns meses, mas que recentemente cresceu significativamente em tamanho.
Primeiro, criamos um contêiner a partir da imagem:
where-the@roadmap-ends ~ $ docker create --name example where-the-roadmap-ends 339b8905b681a1d4f7c56f564f6b1f5e63bb6602b62ba5a15d368ed785f44f55
Em seguida, docker inspect
nos informa onde os diretórios de camadas da imagem baixada foram parar em nosso sistema de arquivos:
where-the@roadmap-ends ~ $ docker inspect example | grep GraphDriver -A7 "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47-init/diff:/var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff:/var/lib/docker/overlay2/j0dekt7y8xgix11n0lturmf8t/diff:/var/lib/docker/overlay2/zd57mz6l4zrsjk9snc2crphfq/diff:/var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff:/var/lib/docker/overlay2/8c639b22627e0ad91944a70822b442e5bff848968263a37715a293a15483c170/diff", "MergedDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/merged", "UpperDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/diff", "WorkDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/work" }, "Name": "overlay2"
As camadas que queremos examinar nesta investigação são a lista de diretórios “LowerDir”. Os outros diretórios não fazem parte da imagem do Docker em si — podemos ignorá-los.
Então, analisamos a lista de diretórios "LowerDir" na linha de comando:
where-the@roadmap-ends ~ $ docker inspect example | grep GraphDriver -A7 | grep LowerDir | awk '{print $2}' | sed 's|"||g' | sed 's|,||g' | sed 's|:|\n|g' /var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47-init/diff /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff /var/lib/docker/overlay2/j0dekt7y8xgix11n0lturmf8t/diff /var/lib/docker/overlay2/zd57mz6l4zrsjk9snc2crphfq/diff /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff /var/lib/docker/overlay2/8c639b22627e0ad91944a70822b442e5bff848968263a37715a293a15483c170/diff
Esta é a lista de camadas da imagem, em ordem, com a camada mais baixa primeiro. Agora precisamos correlacionar manualmente esses diretórios de camadas com as linhas do Dockerfile que os produziram.
Infelizmente, o Docker não nos oferece uma maneira de extrair diretamente essas linhas de uma imagem do Docker – isso é o melhor que podemos obter usando docker history
:
where-the@roadmap-ends ~ $ docker history where-the-roadmap-ends IMAGE CREATED CREATED BY SIZE COMMENT 6bbac081b2a7 2 hours ago CMD ["/bin/bash"] 0B buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #… 0B buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c pip3 install numpy # buildkit 70MB buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c apt-get install -y iputils-pi… 343MB buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c apt-get update # buildkit 40.1MB buildkit.dockerfile.v0 <missing> 8 months ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 8 months ago /bin/sh -c #(nop) ADD file:550e7da19f5f7cef5… 69.2MB
Usando esta saída, podemos identificar quais camadas possuem diretórios e qual comando Dockerfile criou a camada (mostrado na coluna CREATED BY).
O comando docker history
exibe as camadas em seu contêiner na mesma ordem em que docker inspect
lista os diretórios das camadas. Sabendo disso, podemos mesclar manualmente as duas saídas para ver quais camadas são maiores que outras, qual comando Dockerfile as criou e qual diretório contém cada camada.
Aqui está o conteúdo da Camada A, que executa um apt-get update
:
where-the@roadmap-ends ~ $ du -hs /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff/var/lib/apt/lists 38.2M /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff/var/lib/apt/lists
Comparado ao conteúdo da camada B, isso exclui os arquivos que sobraram da camada A:
where-the@roadmap-ends ~ $ du -hs /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff/var/lib/apt/lists 4.0K /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff/var/lib/apt/lists
O diretório /var/lib/apt/lists
existe em ambas as camadas, mas na camada B, o diretório quase não usa espaço.
Isso ocorre porque o diretório da camada B contém “arquivos whiteout”, que o Docker usa para marcar arquivos para exclusão do sistema de arquivos contêiner final.
Por causa disso, apesar dos arquivos terem sido “excluídos” na Camada B, eles ainda existem na Camada A e, portanto, contribuem para o tamanho geral da sua imagem – 38,2 MB de inchaço desnecessário.
Agora, não foi tão fácil? 😉
O método manual é tão complexo e complicado que a comunidade de código aberto criou uma ferramenta específica para essa tarefa – chamada dive
. Dive é uma ferramenta CLI que recebe uma imagem como entrada, analisa seus sistemas de arquivos e apresenta uma interface de usuário interativa baseada em texto em seu terminal. Ele correlaciona as camadas da imagem para você, permitindo inspecionar os diretórios das camadas com mais facilidade.
Quando executado na imagem Docker do Dockerfile acima, fica assim:
┃ ● Layers ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ Aggregated Layer Contents ├─────────────────────────────────────────────────────────────────────────────── Cmp Size Command └── var 69 MB FROM acee8cf20a197c9 └── lib 40 MB RUN /bin/sh -c apt-get update # buildkit └── apt 343 MB RUN /bin/sh -c apt-get install -y iputils-ping python3 python3-pip # buildkit └── lists 70 MB RUN /bin/sh -c pip3 install numpy # buildkit ├── auxfiles 0 B RUN /bin/sh -c rm -rf /var/lib/apt/lists/* # buildkit ├── lock ├── partial │ Layer Details ├─────────────────────────────────────────────────────────────────────────────────────────── ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_main_binary-arm64_Packages.lz4 Tags: (unavailable) ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_universe_binary-arm64_Packages.lz4 Id: 2bc27a99fd5750414948211814da00079804292360f8e2d7843589b9e7eb5eee ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_InRelease Digest: sha256:6e6fb36e04f3abf90c7c87d52629fe154db4ea9aceab539a794d29bbc0919100 ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_main_binary-arm64_Packages.lz4 Command: ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_multiverse_binary-arm64_Packages.lz4 RUN /bin/sh -c apt-get update # buildkit ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_restricted_binary-arm64_Packages.lz4 ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_universe_binary-arm64_Packages.lz4 │ Image Details ├─────────────────────────────────────────────────────────────────────────────────────────── ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_main_binary-arm64_Packages.lz4 Image name: where-the-roadmap-ends ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_multiverse_binary-arm64_Packages.lz4 Total Image size: 522 MB ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_restricted_binary-arm64_Packages.lz4 Potential wasted space: 62 MB ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_universe_binary-arm64_Packages.lz4 Image efficiency score: 90 % ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_main_binary-arm64_Packages.lz4 Count Total Space Path ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_multiverse_binary-arm64_Packages.lz4 2 28 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy_universe_binary-arm64_Pack ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_restricted_binary-arm64_Packages.lz4 2 7.3 MB /usr/bin/perl └── ports.ubuntu.com_ubuntu-ports_dists_jammy_universe_binary-arm64_Packages.lz4 2 4.4 MB /usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.30 2 2.9 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy_main_binary-arm64_Packages 2 2.0 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_main_binary-arm64_ 2 1.7 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_universe_binary-ar
O Dive é uma ferramenta maravilhosa e estou feliz que exista, mas às vezes é insuficiente. Interfaces baseadas em texto não são as mais fáceis de usar – às vezes Grafana é melhor que top
.
Além disso, imagens grandes podem sobrecarregar as capacidades do Dive. Ao inspecionar imagens grandes, o Dive consome muita memória – às vezes, o kernel interrompe o processo do Dive antes de gerar qualquer dado.
Do ponto de vista do desenvolvedor, sempre fez sentido esperar que existissem visualizações da camada de imagem do Docker no Docker Hub. Afinal, nossos dados de imagem Docker já residem no backend do Docker Hub.
Muitas vezes me imaginei inspecionando o interior da imagem do Docker em meu navegador, usando uma UI parecida com esta:
Com o Docker Scout, parece que o Docker está caminhando nessa direção como empresa. Hoje, se eu navegar até a imagem mais recente do Postgres no Docker Hub, sou recebido com isto:
Como desenvolvedor, isso é emocionante. Essa nova IU me permite navegar visualmente pelos detalhes da camada da imagem, destacando vulnerabilidades e problemas de tamanho da imagem exatamente como eu desejo.
Quando o Docker Hub foi lançado, ele tinha dois tipos de usuários:
E o Docker teve que abordar esses dois tipos de usuários de maneira diferente.
Depois que o Docker foi lançado em 2013, os desenvolvedores imediatamente quiseram usar seu produto. Os contêineres Docker padronizam o empacotamento e a implantação do software de todos. Os contêineres rapidamente se tornaram populares na comunidade de desenvolvedores de software.
Os editores de software, no entanto, estavam mais hesitantes. Por que eles deveriam carregar seu software em uma plataforma cuja principal utilidade é eliminar a diferenciação entre produtos de software?
O Docker teve que conquistá-los para tornar o Docker Hub um sucesso. Portanto, em vez de se concentrar em atrair desenvolvedores, o design e o conjunto de recursos do Docker Hub foram direcionados aos editores de software.
Para os editores, o Docker Hub era um site de marketing. Isso lhes deu um lugar para anunciar seus produtos. Ele não permitiu que os desenvolvedores vissem os detalhes internos da imagem do Docker, assim como as concessionárias de automóveis não permitiam que você desmontasse seus blocos de motor. Os detalhes internos de engenharia não eram exibidos, eram escondidos, de modo que os compradores permaneciam concentrados nos produtos em si, e não em como eram fabricados.
O Docker Hub carecia de recursos voltados para o desenvolvedor para otimização do tamanho da imagem e introspecção da cadeia de suprimentos porque o Docker já conquistou desenvolvedores sem esses recursos. Na verdade, esses recursos tendem a fazer com que os editores de software fiquem mal - e eles eram os usuários que o Docker Hub ainda precisava conquistar para ter sucesso.
Essa dinâmica de atender editores e desenvolvedores de software fez do Docker Hub uma plataforma bilateral.
Plataformas bilaterais são empresas que possuem duas categorias diferentes de usuários e precisam que ambos participem da plataforma para que tudo funcione. Normalmente, é a motivação para usar a plataforma que divide os usuários em diferentes grupos.
Embora os dois grupos de usuários possam não realizar transações diretas entre si, as plataformas bilaterais são como mercados: elas não criam valor a menos que os fornecedores apareçam para vender e os compradores apareçam para comprar.
Na indústria de tecnologia, as plataformas bilaterais são onipresentes. Quando bem sucedidos, estes modelos de negócio tendem a produzir efeitos de rede que impulsionam as empresas para o crescimento sustentado e a rentabilidade. A partir de certo ponto, o tamanho da plataforma e a posição estabelecida em um espaço atraem novos usuários para a plataforma.
Depois de saber o que está procurando, as plataformas bilaterais são fáceis de detectar. Aqui estão três exemplos.
No LinkedIn, existem dois tipos de usuários: funcionários e gerentes de contratação. Ambos os grupos participam por razões diferentes – os funcionários querem empregos e os gestores de contratação querem contratar pessoas.
Nenhum grupo conseguiu o que queria do site até que o outro grupo começasse a participar. Depois que um número suficiente de cada grupo se inscreveu, ele se tornou o local padrão para novos membros de qualquer grupo, e o crescimento do site se perpetuou, até ser comprado pela Microsoft por US$ 26 bilhões.
No YouTube , existem criadores de conteúdo e existem espectadores. Os espectadores acessam o site para ver vídeos – os criadores de conteúdo publicam no site em busca de boas vibrações, fama e fortuna.
Depois que o YouTube estabeleceu uma reputação como um lugar onde os criadores de conteúdo poderiam ter sucesso, mais e mais criadores de conteúdo apareceram – e à medida que geravam mais conteúdo, mais espectadores passaram a visitá-lo. Depois que a plataforma cresceu além de um determinado tamanho, os criadores de conteúdo e os espectadores não tiveram escolha a não ser continuar a usá-la – cada grupo precisava do outro e eles só podiam se encontrar por meio do YouTube.
Para que o Docker Hub fosse relevante, era necessário que os editores de software fizessem upload de imagens e os desenvolvedores fizessem download delas. Assim que um número suficiente de editores enviassem imagens para o Docker Hub, ele se tornaria o local padrão de onde os desenvolvedores poderiam extrair. À medida que a plataforma continuasse a crescer, o Docker Hub consolidaria seu domínio como o único registro que governaria todos eles.
Pelo menos esse era o plano. Na realidade, o Docker (a empresa) declinou e o Docker Hub nunca se tornou “o único”. Ao construir uma plataforma bilateral, as startups têm um produto, mas precisam encontrar duas vezes a adequação do produto ao mercado. Para Docker, esse fardo era demais para suportar – no final, ele dividiu o Docker ao meio .
Agora, depois de vender seu negócio empresarial , a Docker se reinventou, concentrando seu serviço de assinatura exclusivamente em desenvolvedores e seus empregadores. O Docker Hub não é mais uma plataforma bilateral, é um simples SaaS – os clientes pagam uma taxa mensal em troca de enviar e extrair suas imagens.
Isso muda o cálculo. Não existe mais uma persona de usuário “editor” para o Docker agradar – eles estão todos interessados nos desenvolvedores. Para o Docker Hub, isso abre caminho para a funcionalidade de inspeção de camada de imagem do Docker Scout. Para aqueles que observam de longe, isso demonstra o acoplamento sutil e fundamental entre o modelo de negócios de uma startup e sua oferta de produtos.
Também publicado aqui.