Compreendendo as camadas do Docker

27

Temos o seguinte bloco em nosso Dockerfile:

RUN yum -y update
RUN yum -y install epel-release
RUN yum -y groupinstall "Development Tools"
RUN yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Foi-me dito que devemos unir esses RUNcomandos para reduzir as camadas de janela de encaixe criadas:

RUN yum -y update \
    && yum -y install epel-release \
    && yum -y groupinstall "Development Tools" \
    && yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Sou muito novato no docker e não tenho certeza de entender completamente as diferenças entre essas duas versões da especificação de vários comandos RUN. Quando alguém uniria RUNcomandos em um único e quando faz sentido ter vários RUNcomandos?

alecxe
fonte

Respostas:

35

Uma imagem do docker é na verdade uma lista vinculada de camadas do sistema de arquivos. Cada instrução em um Dockerfile cria uma camada do sistema de arquivos que descreve as diferenças no sistema de arquivos antes e após a execução da instrução correspondente. O docker inspectsubcomando pode ser usado em uma imagem de janela de encaixe para revelar sua natureza de ser uma lista vinculada de camadas do sistema de arquivos.

O número de camadas usadas em uma imagem é importante

  • ao empurrar ou puxar imagens, pois afeta o número de uploads ou downloads simultâneos que ocorrem.
  • ao iniciar um contêiner, como as camadas são combinadas para produzir o sistema de arquivos usado no contêiner; quanto mais camadas estiverem envolvidas, pior será o desempenho, mas os diferentes back-ends do sistema de arquivos são afetados de maneira diferente por isso.

Isso tem várias conseqüências sobre como as imagens devem ser construídas. O primeiro e mais importante conselho que posso dar é:

Conselho # 1 Verifique se as etapas de compilação em que seu código-fonte está envolvido chegam o mais tarde possível no Dockerfile e não estão vinculadas aos comandos anteriores usando a &&ou a ;.

A razão para isso é que todas as etapas anteriores serão armazenadas em cache e as camadas correspondentes não precisarão ser baixadas repetidamente. Isso significa compilações e lançamentos mais rápidos, provavelmente o que você deseja. Curiosamente, é surpreendentemente difícil fazer um uso otimizado do cache do docker.

Meu segundo conselho é menos importante, mas acho muito útil do ponto de vista da manutenção:

Conselho nº 2 Não escreva comandos complexos no Dockerfile, mas use scripts que devem ser copiados e executados.

Um Dockerfile seguindo este conselho pareceria

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh

e assim por diante. O conselho de vincular vários comandos com &&apenas um escopo limitado. É muito mais fácil escrever com scripts, onde você pode usar funções etc. para evitar redundância ou para fins de documentação.

As pessoas interessadas pelos pré-processadores e dispostas a evitar a pequena sobrecarga causada pelas COPYetapas e na verdade estão gerando on-the-fly um Dockerfile em que o

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh

sequências são substituídas por

RUN base64 --decode … | sh -x

onde é a versão codificada em base64 de apt_setup.sh.

Meu terceiro conselho é para pessoas que desejam limitar o tamanho e o número de camadas ao possível custo de construções mais longas.

Conselho nº 3 Use o withidiom para evitar arquivos presentes nas camadas intermediárias, mas não no sistema de arquivos resultante.

Um arquivo adicionado por alguma instrução do docker e removido por alguma instrução posterior não está presente no sistema de arquivos resultante, mas é mencionado duas vezes nas camadas do docker que constituem a imagem do docker em construção. Uma vez, com nome e conteúdo completo na camada resultante da adição da instrução e uma vez como aviso de exclusão na camada resultante da remoção da instrução.

Por exemplo, suponha que precisamos temporariamente de um compilador C e alguma imagem e considere o

# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc

(Um exemplo mais realista criaria algum software com o compilador, em vez de apenas afirmar sua presença com o --versionsinalizador.)

O snippet do Dockerfile cria três camadas, a primeira contém o pacote gcc completo, de modo que, mesmo que não esteja presente no sistema de arquivos final, os dados correspondentes ainda fazem parte da imagem da mesma maneira e precisam ser baixados, carregados e descompactados sempre que o arquivo imagem final é.

O withidiom é uma forma comum na programação funcional para isolar a propriedade e a liberação de recursos da lógica que o utiliza. É fácil transpor esse idioma para shell-script, e podemos reformular os comandos anteriores como o script a seguir, para ser usado COPY & RUNcomo no Conselho # 2.

# with_c_compiler SIMPLE-COMMAND
#  Execute SIMPLE-COMMAND in a sub-shell with gcc being available.

with_c_compiler()
(
    set -e
    apt-get install -y gcc
    "$@"
    trap 'apt-get --purge autoremove -y gcc' EXIT
)

with_c_compiler\
    gcc --version

Comandos complexos podem ser transformados em função para que possam ser alimentados ao with_c_compiler. Também é possível encadear chamadas de várias with_whateverfunções, mas talvez não seja muito desejável. (Usando recursos mais esotéricos do shell, certamente é possível tornar os with_c_compilercomandos complexos aceitos, mas em todos os aspectos é preferível agrupar esses comandos complexos em funções.)

Se quisermos ignorar o Conselho nº 2, o snippet do Dockerfile resultante será

RUN apt-get install -y gcc\
 && gcc --version\
 && apt-get --purge autoremove -y gcc

o que não é tão fácil de ler e manter por causa da ofuscação. Veja como a variante do shell script enfatiza a parte importante, gcc --versionenquanto a &&variante encadeada enterra a parte no meio do ruído.

Michael Le Barbier Grünewald
fonte
11
Você poderia incluir o resultado do tamanho da caixa após criar usando um script e usando os vários comandos em uma instrução RUN?
030
11
Parece-me uma má idéia misturar a configuração da base de imagem (ou seja, o material do SO) e até as bibliotecas com a configuração da fonte que você escreveu. Você diz "Certifique-se de que as etapas de compilação em que seu código-fonte esteja envolvido cheguem o mais tarde possível". Existe algum problema em tornar essa parte um artefato completamente independente?
JimmyJames
11
@ 030 O que você quer dizer com tamanho da “caixa”? Não faço ideia de qual caixa você está se referindo.
Michael Le Barbier Grünewald
11
Eu quis dizer o tamanho da imagem do docker
030
11
@JimmyJames Depende muito do seu cenário de implantação. Se assumirmos um programa compilado, a “coisa certa a fazer” seria empacotá-lo e instalar as dependências desse pacote e o próprio pacote como duas etapas distintas, quase próximas da final. Isso para maximizar a utilidade do cache da janela de encaixe e evitar o download de camadas repetidas vezes com os mesmos arquivos. Acho mais fácil compartilhar receitas de criação para criar imagens de janela de encaixe do que construir longas cadeias de imagens de dependência, porque a última dificulta a reconstrução.
Michael Le Barbier Grünewald
13

Cada instrução que você cria no Dockerfile resulta em uma nova camada de imagem sendo criada. Cada camada traz dados adicionais que nem sempre fazem parte da imagem resultante. Por exemplo, se você adicionar um arquivo em uma camada, mas removê-lo em outra camada posteriormente, o tamanho da imagem final incluirá o tamanho do arquivo adicionado na forma de um arquivo "whiteout" especial, embora você o tenha removido.

Digamos que você tenha o seguinte Dockerfile:

FROM centos:6

RUN yum -y update 
RUN yum -y install epel-release

O tamanho da imagem resultante será

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

Por outro lado, com o Dockerfile "semelhante":

FROM centos:6

RUN yum -y update  && yum -y install epel-release

O tamanho da imagem resultante será

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

Você obterá um tamanho ainda menor, se você limpar o cache do yum em uma única instrução RUN.

Então, você deseja manter o equilíbrio entre legibilidade / facilidade de manutenção e número de camadas / tamanho da imagem.

oryades
fonte
4

As RUNinstruções representam cada camada. Imagine que alguém baixa um pacote, o instala e gostaria de removê-lo. Se alguém usar três RUNinstruções, o tamanho da imagem não diminuirá, pois existem camadas separadas. Se alguém executar todos os comandos usando uma RUNinstrução, o tamanho da imagem do disco poderá ser reduzido.

030
fonte