Três truques simples para reduzir as imagens do docker

imagem

Quando se trata de criar contêineres Docker, é melhor sempre se esforçar para minimizar o tamanho das imagens. Imagens que usam as mesmas camadas e pesam menos - mais rapidamente transferidas e implantadas.


Mas como controlar o tamanho quando cada execução da instrução RUN cria uma nova camada? Além disso, você ainda precisa de artefatos intermediários antes de criar a própria imagem ...


Você provavelmente sabe que a maioria dos arquivos do Docker tem seus próprios recursos bastante estranhos, por exemplo:


 FROM ubuntu RUN apt-get update && apt-get install vim 

Bem, por que o && está aqui? Não é mais fácil executar duas instruções RUN , como aqui?


 FROM ubuntu RUN apt-get update RUN apt-get install vim 

Iniciando com o Docker versão 1.10, os operadores COPY , ADD e RUN adicionam uma nova camada à imagem. No exemplo anterior, duas camadas foram criadas em vez de uma.


imagem


Camadas como o git confirma.


As camadas do Docker preservam as diferenças entre a versão anterior e a atual da imagem. E, como o git commit, eles são úteis se você os compartilha com outros repositórios ou imagens. De fato, ao solicitar uma imagem do registro, apenas as camadas ausentes são carregadas, o que simplifica a separação de imagens entre contêineres.


Mas, ao mesmo tempo, cada camada ocorre e, quanto mais, mais pesada é a imagem final. Os repositórios Git são semelhantes nesse aspecto: o tamanho do repositório aumenta com o número de camadas, porque ele deve armazenar todas as alterações entre confirmações. Costumava ser uma boa prática combinar várias instruções RUN na mesma linha, como no primeiro exemplo. Mas agora, infelizmente, não.


1. Combine várias camadas em uma usando a montagem em fases das imagens do Docker


Quando o repositório Git cresce, você pode simplesmente resumir todo o histórico de alterações em um commit e esquecê-lo. Aconteceu que algo semelhante pode ser implementado no Docker - por meio de montagem em fases.


Vamos criar um contêiner Node.js.


Vamos começar com o index.js :


 const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => { console.log(`Example app listening on port 3000!`) }) 

e package.json :


 { "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } } 

Empacote o aplicativo com o seguinte Dockerfile :


 FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"] 

Crie uma imagem:


 $ docker build -t node-vanilla . 

Verifique se tudo funciona:


 $ docker run -p 3000:3000 -ti --rm --init node-vanilla 

Agora você pode seguir o link: http: // localhost: 3000 e ver "Hello World!"


No Dockerfile agora temos os operadores COPY e RUN ; portanto, Dockerfile o aumento em pelo menos duas camadas, em comparação com a imagem original:


 $ docker history node-vanilla IMAGE CREATED BY SIZE 075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0B bc8c3cc813ae /bin/sh -c npm install 2.91MB bac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B 500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B 78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0B b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB 

Como você pode ver, a imagem final aumentou em cinco novas camadas: uma para cada operador em nosso Dockerfile . Vamos agora experimentar a construção do Docker em fases. Usamos o mesmo Dockerfile , composto por duas partes:


 FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8 COPY --from=build /app / EXPOSE 3000 CMD ["index.js"] 

A primeira parte do Dockerfile cria três camadas. Em seguida, as camadas são combinadas e copiadas para o segundo e o estágio final. Mais duas camadas são adicionadas à imagem acima. Como resultado, temos três camadas.


imagem


Vamos tentar. Primeiro, crie um contêiner:


 $ docker build -t node-multi-stage . 

Verificando o histórico:


 $ docker history node-multi-stage IMAGE CREATED BY SIZE 331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0B bdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0B f8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MB b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB 

Veja se o tamanho do arquivo mudou:


 $ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB 

Sim, tornou-se menor, mas ainda não significativamente.


2. Nós removemos todos os desnecessários do contêiner usando


A imagem atual nos fornece Node.js, yarn , npm , bash e muitos outros binários úteis. Além disso, é baseado no Ubuntu. Assim, ao implantá-lo, obtemos um sistema operacional completo com muitos binários e utilitários úteis.


No entanto, não precisamos deles para executar o contêiner. A única dependência necessária é o Node.js.


Os contêineres do Docker devem suportar a operação de um processo e conter o conjunto mínimo necessário de ferramentas para executá-lo. Um sistema operacional inteiro não é necessário para isso.


Para que possamos tirar tudo disso, exceto o Node.js.


Mas como


O Google já apresentou uma solução semelhante - GoogleCloudPlatform / distroless .


A descrição para o repositório diz:


Imagens sem distração contêm apenas o aplicativo e suas dependências. Não há gerenciadores de pacotes, shells ou outros programas que geralmente são encontrados na distribuição padrão do Linux.


É disso que você precisa!


Execute o Dockerfile para obter uma nova imagem:


 FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"] 

Coletamos a imagem como de costume:


 $ docker build -t node-distroless . 

O aplicativo deve funcionar bem. Para verificar, execute o contêiner:


 $ docker run -p 3000:3000 -ti --rm --init node-distroless 

E vá para http: // localhost: 3000 . A imagem ficou mais fácil sem binários extras?


 $ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB 

Assim mesmo! Agora ele pesa apenas 76,7 MB, tanto quanto 600 MB a menos!


Tudo é legal, mas há um ponto importante. Quando o contêiner estiver em execução e você precisar verificar, é possível conectar usando:


 $ docker exec -ti <insert_docker_id> bash 

Conectar-se a um contêiner em execução e iniciar o bash muito semelhante à criação de uma sessão SSH.


Mas como distroless é uma versão simplificada do sistema operacional original, não há binários adicionais nem, na verdade, um shell!


Como conectar-se a um contêiner em execução se não houver shell?


O mais interessante é que nada.


Isso não é muito bom, pois apenas binários podem ser executados em um contêiner. E o único que pode ser iniciado é o Node.js:


 $ docker exec -ti <insert_docker_id> node 

De fato, há uma vantagem nisso, porque se algum invasor puder obter acesso ao contêiner, ele causará muito menos dano do que se tivesse acesso ao shell. Em outras palavras, menos binários - menos peso e maior segurança. Mas, ao custo de depuração mais complexa.


Aqui, note-se que não vale a pena conectar e depurar contêineres no ambiente do produto. É melhor contar com sistemas de registro e monitoramento configurados corretamente.


Mas e se ainda precisarmos de depuração e ainda assim desejarmos que a imagem do docker seja a menor?


3. Reduza as imagens básicas com o Alpine


Você pode substituir sem distração por uma imagem alpina.


O Alpine Linux é uma distribuição leve e orientada para a segurança, baseada no musl libc e no busybox . Mas não tomaremos uma palavra, mas verificaremos.


Execute o Dockerfile usando o node:8-alpine :


 FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8-alpine COPY --from=build /app / EXPOSE 3000 CMD ["npm", "start"] 

Crie uma imagem:


 $ docker build -t node-alpine . 

Verifique o tamanho:


 $ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB 

Na saída, temos 69,7 MB - isso é ainda menos do que uma imagem sem distração.


Vamos verificar se é possível conectar-se a um contêiner de trabalho (no caso da imagem de distribuição, não foi possível fazer isso).


Inicie o contêiner:


 $ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000! 

E conecte:


 $ docker exec -ti 9d8e97e307d7 bash OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown 

Sem sucesso. Mas talvez o contêiner tenha sh 'ell ...:


 $ docker exec -ti 9d8e97e307d7 sh / # 

Ótimo! Conseguimos conectar-se ao contêiner e, ao mesmo tempo, sua imagem também é menor. Mas aqui havia algumas nuances.


Imagens alpinas são baseadas no muslc, uma biblioteca padrão alternativa para C. Enquanto a maioria das distribuições Linux, como Ubuntu, Debian e CentOS, são baseadas no glibc. Acredita-se que essas duas bibliotecas fornecem a mesma interface para trabalhar com o kernel.


No entanto, eles têm objetivos diferentes: glibc é o mais comum e rápido, enquanto o muslc ocupa menos espaço e é escrito com um viés de segurança. Quando um aplicativo compila, como regra geral, ele compila em uma biblioteca específica C. Se você precisar usá-lo com outra biblioteca, precisará recompilar.


Em outras palavras, a construção de contêineres nas imagens Alpine pode levar a eventos inesperados, pois a biblioteca C padrão usada nela é diferente. A diferença será notada ao trabalhar com binários pré-compilados, como extensões Node.js. para C ++.


Por exemplo, o pacote PhantomJS finalizado não funciona no Alpine.


Então, qual é a imagem básica para escolher?


Aparência alpina, sem distração ou baunilha - é claro, é melhor decidir de acordo com a situação.


Se você está lidando com prod e a segurança é importante, talvez o distroless seja o mais apropriado.


Cada binário adicionado à imagem do Docker adiciona um certo risco à estabilidade de todo o aplicativo. Esse risco pode ser reduzido com a instalação de apenas um binário no contêiner.


Por exemplo, se um invasor puder encontrar uma vulnerabilidade em um aplicativo em execução com base em uma imagem sem distração, ele não poderá executar o shell no contêiner porque não estava lá!


Se, por algum motivo, o tamanho da imagem do Docker for extremamente importante para você, definitivamente vale a pena examinar mais de perto as imagens baseadas em Alpine.


Eles são realmente pequenos, mas à custa da compatibilidade. O Alpine usa uma biblioteca C padrão ligeiramente diferente, muslc, então, às vezes, os problemas aparecem. Exemplos estão disponíveis nos seguintes links: https://github.com/grpc/grpc/issues/8528 e https://github.com/grpc/grpc/issues/6126 .


Imagens de baunilha são ideais para teste e desenvolvimento.


Sim, eles são grandes, mas parecem o máximo possível em uma máquina completa com o Ubuntu instalado. Além disso, todos os binários no sistema operacional estão disponíveis.


Resuma o tamanho das imagens recebidas do Docker:


node:8 681MB
node:8 com construção incremental de 678MB
gcr.io/distroless/nodejs 76.7MB
node:8-alpine 69,7MB


Separando palavras do tradutor


Leia outros artigos em nosso blog:


Backups com estado em Kubernetes


Fazendo backup de um grande número de projetos da web heterogêneos


Bot de telegrama para Redmine. Como simplificar a vida para si e para as pessoas

Source: https://habr.com/ru/post/pt437372/


All Articles