Docker: conselhos não prejudiciais

Nos comentários do meu artigo Docker: conselhos prejudiciais houve muitos pedidos para explicar por que o Dockerfile descrito é tão ruim.

Resumo do episódio anterior: dois desenvolvedores enfrentando um prazo apertado criam um Dockerfile. Durante o processo, Ops Igor Ivanovich entra. O Dockerfile final é tão ruim que o Igor fica à beira de um infarto.

Docker: conselhos não prejudiciais

Agora vamos entender o que há de errado com esse Dockerfile.

Então, passou uma semana.

Dev Petya se encontra na cantina para um café com Ops Igor Ivanovich.

P: Igor Ivanovich, você está muito ocupado? Gostaria de entender onde erramos.

II: Isso é bom, não é comum encontrar desenvolvedores que se importam com a operação.
Para começar, vamos estabelecer algumas coisas:

  1. A ideologia do Docker: um contêiner — um processo.
  2. Quanto menor o contêiner, melhor.
  3. Quanto mais se pegar do cache, melhor.

P: E por que deve haver um processo único em um contêiner?

II: O Docker, ao iniciar um contêiner, monitora o estado do processo com pid 1. Se o processo morrer, o Docker tenta reiniciar o contêiner. Suponha que você tenha vários aplicativos em um contêiner ou que o aplicativo principal não esteja rodando com pid 1. Se o processo morrer, o Docker não ficará ciente disso.

Se não há mais perguntas, mostre seu Dockerfile.

E Petya mostrou:

FROM ubuntu:latest

# Copiando o código fonte
COPY .\/\/app
WORKDIR /app

# Atualizando a lista de pacotes
RUN apt-get update

# Atualizando pacotes
RUN apt-get upgrade

# Instalando pacotes necessários
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Instalando bundler
RUN gem install bundler

# Instalando nodejs utilizado para a construção do estático
RUN curl -sL https:\/\/deb.nodesource.com\/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Instalando dependências
RUN bundle install --without development test --path vendor\/bundle

# Limpando os caches
RUN rm -rf \/usr\/local\/bundle\/cache\/*.gem
RUN apt-get clean
RUN rm -rf \/var\/lib\/apt\/lists\/* \/tmp\/* \/var\/tmp\/*
RUN rake assets:precompile
# Executando o script ao iniciar o contêiner, que iniciará todo o resto.
CMD ["\/app\/init.sh"]

II: Ah, vamos resolver isso passo a passo. Comecemos pela primeira linha:

FROM ubuntu:latest

Você está usando a tag latest. O uso da tag latest pode levar a consequências imprevisíveis. Imagine que o mantenedor da imagem cria uma nova versão da imagem com uma lista de software diferente, essa imagem recebe a tag latest. E seu contêiner, na melhor das hipóteses, deixa de construir, e na pior, você pega bugs que antes não existiam.

Você está usando uma imagem com um sistema operacional completo e muitos softwares desnecessários, o que aumenta o tamanho do contêiner. E quanto mais softwares, mais brechas e vulnerabilidades.

Além disso, quanto maior a imagem, mais espaço ela ocupa no host e no registro (você está guardando as imagens em algum lugar?).

P: Sim, claro, temos um registro, você mesmo o configurou.

IA: Então, sobre o que eu estava falando?.. Ah, sim, volumes… A carga na rede também aumenta. Para uma imagem única isso é imperceptível, mas quando há uma construção contínua, testes e deploys, isso se torna notável. E se você não tem o modo Deus no AWS, ainda receberá uma conta astronômica.

Por isso, é necessário escolher a imagem mais adequada, com a versão exata e o mínimo de softwares. Por exemplo, pegue: FROM ruby:2.5.5-stretch

P: Oh, entendi. E como e onde posso ver as imagens disponíveis? Como saber qual eu preciso?

IA: Normalmente, as imagens são obtidas do dockerhub, não confunda com porn hub :). Para uma imagem geralmente existem várias compilações:
A Microsoft anunciou a expansão da funcionalidade do subsistema WSL2.: imagens construídas sobre uma base Linux minimalista, apenas 5 MB. Sua desvantagem: foi construída com uma implementação própria da libc, os pacotes padrão não funcionam. Buscar e instalar o pacote necessário levará um bom tempo.
Scratch: imagem base, não usada para compilar outras imagens. Destina-se exclusivamente à execução de binários e dados preparados. É ideal para executar aplicações binárias que incluem tudo o que é necessário, como aplicações em go.
Baseada em algum sistema operacional, como Ubuntu ou Debian. Bem, aqui não precisa explicar muito.

IA: Agora precisamos instalar todos os pacotes adicionais e limpar os caches. E logo podemos descartar apt-get upgrade. Se não, a cada construção, apesar do tag fixo da imagem base, serão geradas diferentes imagens. A atualização de pacotes na imagem é uma tarefa do mantenedor, que vem acompanhada de uma mudança na tag.

P: Sim, eu tentei fazer isso, consegui assim:

WORKDIR /app
COPY ./ /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

IA: Não está ruim, mas há coisas a serem melhoradas aqui. Veja, este comando:

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

… não remove os dados da imagem final, mas apenas cria uma camada adicional sem esses dados. Está correto assim:

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Mas isso ainda não é tudo. O que você tem aí, Ruby? Então não é necessário copiar todo o projeto no início. Basta copiar o Gemfile e o Gemfile.lock.

Com essa abordagem, o bundle install não será executado a cada alteração nos arquivos de origem, mas apenas se o Gemfile ou o Gemfile.lock mudarem.

Os mesmos métodos funcionam para outras linguagens com gerenciadores de dependências, como npm, pip, composer e outros baseados em um arquivo de lista de dependências.

E, por fim, você se lembra de que no início eu falei sobre a ideologia do Docker "um contêiner - um processo"? Isso significa que o supervisor não é necessário. Também não é preciso instalar o systemd, pelos mesmos motivos. Na prática, o Docker é o próprio supervisor. E quando você tenta executar vários processos nele, é como executar vários aplicativos dentro de um único processo do supervisor.
Ao construir, você criará uma única imagem e depois iniciará o número necessário de contêineres, para que em cada um funcione um processo.

Mas falaremos sobre isso mais tarde.

P: Parece que entendi. Veja o que acontece:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

E o lançamento dos serviços definidos será feito ao iniciar o contêiner?

IA: Sim, isso mesmo. Aliás, você pode usar tanto CMD quanto ENTRYPOINT. Entender a diferença entre eles é sua tarefa de casa. Há um bom artigo sobre isso no Habr. artigo.

Então, vamos continuar. Você está baixando o arquivo para instalar o node, mas não há garantia de que ele terá o que você precisa. Precisamos adicionar validação. Por exemplo, assim:

EXECUTE curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c - 
    && bash setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle 
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean 
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Com a soma de verificação, você poderá verificar se baixou o arquivo correto.

P: Mas se o arquivo mudar, a construção não passará.

IA: Sim, e estranhamente, isso também é um ponto positivo. Você saberá que o arquivo mudou e poderá verificar o que foi alterado. Vai saber se, por exemplo, adicionaram um script que apaga tudo que alcança ou cria uma porta dos fundos.

P: Obrigado. Então, o Dockerfile final ficará assim:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c - 
    && bash setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle 
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean 
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

P: Igor Ivanovich, obrigado pela ajuda. Já preciso ir, tenho que fazer mais 10 commits hoje.

Igor Ivanovich, parando o colega apressado com um olhar, toma um gole do seu café forte. Pensando por alguns segundos sobre SLA de 99,9% e código sem bugs, ele faz uma pergunta.

IA: Onde vocês armazenam os logs?

P: Claro, no production.log. A propósito, como vamos acessá-los sem ssh?

IA: Se vocês mantiverem os logs em arquivos, já existe uma solução para vocês. O comando docker exec permite executar qualquer comando no contêiner. Por exemplo, vocês podem usar cat para os logs. E, usando a chave -it e iniciando o bash (se ele estiver instalado no contêiner), vocês terão acesso interativo ao contêiner.

Mas não é bom armazenar logs em arquivos. No mínimo, isso leva ao crescimento descontrolado do contêiner, pois ninguém rotaciona os logs. Todos os logs devem ser enviados para stdout. Você pode visualizá-los lá usando o comando docker logs.

P: Igor Ivanovich, e se tirássemos os logs para um diretório montado, na nodiva física, como os dados dos usuários?

IA: É bom que você não tenha se esquecido de tirar os dados gravados no disco da nodiva. Com os logs, também dá para fazer isso, só não se esqueça de configurar a rotação.
Pronto, você pode ir.

P: Igor Ivanovich, você pode me recomendar algo para ler?

II: Para começar, leia as recomendações dos desenvolvedores do Docker, é improvável que alguém conheça o Docker melhor do que eles.

E se você quiser ganhar experiência prática, vá para o intensivo. Afinal, teoria sem prática é morta.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster