Docker: não é um mau conselho

Nos comentários do meu artigo Docker: mau conselho houve muitos pedidos para explicar por que o Dockerfile descrito nele era tão terrível.

Resumo da série anterior: Dois desenvolvedores compõem um Dockerfile em um prazo apertado. No processo, o Ops Igor Ivanovich vai até eles. O Dockerfile resultante é tão ruim que a IA está à beira de um ataque cardíaco.

Docker: não é um mau conselho

Agora vamos descobrir o que há de errado com este Dockerfile.

Então, uma semana se passou.

Dev Petya encontra Ops Igor Ivanovich na sala de jantar tomando uma xícara de café.

P: Igor Ivanovich, você está muito ocupado? Eu gostaria de descobrir onde fizemos besteira.

AI: Isso é bom, você não encontra frequentemente desenvolvedores interessados ​​em exploração.
Primeiro, vamos concordar em algumas coisas:

  1. Ideologia Docker: um contêiner – um processo.
  2. Quanto menor o recipiente, melhor.
  3. Quanto mais você tirar do cache, melhor.

P: Por que deveria haver um processo em um contêiner?

AI: 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. Digamos que você tenha vários aplicativos em execução em um contêiner ou o aplicativo principal não esteja em execução com pid 1. Se o processo morrer, o Docker não saberá disso.

Se você não tiver mais dúvidas, mostre-nos seu Dockerfile.

E Petya mostrou:

FROM ubuntu:latest

# Копируем исходный код
COPY ./ /app
WORKDIR /app

# Обновляем список пакетов
RUN apt-get update 

# Обновляем пакеты
RUN apt-get upgrade

# Устанавливаем нужные пакеты
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Устанавливаем bundler
RUN gem install bundler

# Устанавливаем nodejs используется для сборки статики
RUN curl -sL https://deb.nodesource.com/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Устанавливаем зависимости
RUN bundle install --without development test --path vendor/bundle

# Чистим за собой кэши
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
# Запускаем скрипт, при старте контейнера, который запустит все остальное.
CMD ["/app/init.sh"]

AI: Ah, vamos colocar isso em ordem. Vamos começar com a primeira linha:

FROM ubuntu:latest

Você pega a etiqueta latest. Usando uma etiqueta latest leva a consequências imprevisíveis. Imagine, o mantenedor da imagem constrói uma nova versão da imagem com uma lista diferente de software, esta imagem recebe a tag mais recente. E seu contêiner, na melhor das hipóteses, para de ser construído e, na pior, você detecta bugs que não existiam antes.

Você tira uma imagem com um sistema operacional completo com muitos softwares desnecessários, o que aumenta o volume do contêiner. E quanto mais software, mais falhas e vulnerabilidades.

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

P: Sim, claro, temos um registro, você configura.

AI: Então, do que estou falando?... Ah, sim, os volumes... A carga na rede também está crescendo. Para uma única imagem isso não é perceptível, mas quando há construção, testes e implantação contínuos, é perceptível. E se você não tiver o modo de Deus na AWS, também receberá uma conta cósmica.

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

P: Ah, entendo. Como e onde posso visualizar as imagens disponíveis? Como posso saber qual deles preciso?

AI: Geralmente as imagens são tiradas de dockerhub, não confunda com pornhub :). Geralmente existem várias montagens para uma imagem:
Alpino: as imagens são coletadas em uma imagem Linux minimalista, de apenas 5 MB. Sua desvantagem: ele é compilado com sua própria implementação libc, os pacotes padrão não funcionam nele. Encontrar e instalar o pacote necessário levará muito tempo.
Scratch: imagem base, não usada para construir outras imagens. Destina-se exclusivamente à execução de dados binários preparados. Ideal para executar aplicativos binários que incluem tudo o que você precisa, como aplicativos GO.
Baseado em qualquer sistema operacional, como Ubuntu ou Debian. Bem, não creio que haja necessidade de explicação.

AI: Agora precisamos instalar todos os extras. pacotes e limpe seus caches. E você pode jogá-lo fora imediatamente atualização apt-get. Caso contrário, a cada build, apesar da tag fixa da imagem base, serão obtidas imagens diferentes. A atualização dos pacotes na imagem é tarefa do mantenedor e vem acompanhada da alteração da tag.

P: Sim, tentei fazer, ficou 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/*

AI: Nada mal, mas também há algo em que trabalhar. Olha, aqui está este comando:

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

... não exclui dados da imagem final, mas apenas cria uma camada adicional sem esses dados. Corretamente 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 não é tudo. O que você tem aí, Ruby? Então você não precisa copiar o projeto inteiro no início. Basta copiar Gemfile e Gemfile.lock.

Com esta abordagem, a instalação do pacote não será executada para cada alteração na fonte, mas apenas se o Gemfile ou Gemfile.lock for alterado.

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

E por fim, lembra que no início falei sobre a ideologia Docker “um contêiner – um processo”? Isso significa que não é necessário um supervisor. Você também não deve instalar o systemd, pelos mesmos motivos. Essencialmente, o próprio Docker é um supervisor. E quando você tenta executar vários processos nele, é como executar vários aplicativos em um processo supervisor.
Ao compilar, você criará uma única imagem e, em seguida, iniciará o número necessário de contêineres para que um processo seja executado em cada um.

Mas mais sobre isso mais tarde.

P: Acho que entendo. 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"]

Podemos substituir o lançamento de daemons ao iniciar o contêiner?

AI: Sim, está certo. A propósito, você pode usar CMD e ENTRYPOINT. E descobrir qual é a diferença é o seu dever de casa. Há um bom sobre esse assunto no Habré artigo.

Então, vamos em frente. Você baixa um arquivo para instalar o nó, mas não há garantia de que ele conterá o que você precisa. Precisamos adicionar validação. Por exemplo, assim:

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/* 

Usando a soma de verificação, você pode verificar se baixou o arquivo correto.

P: Mas se o arquivo for alterado, a compilação falhará.

AI: Sim, e por incrível que pareça, isso também é uma vantagem. Você saberá que o arquivo foi alterado e poderá ver o que foi alterado nele. Nunca se sabe, eles adicionaram, digamos, um script que exclui tudo o que pode alcançar ou cria um backdoor.

P: Obrigado. Acontece que 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. É hora de correr, preciso fazer mais 10 commits hoje.

Igor Ivanovich, parando com o olhar o colega apressado, toma um gole de café forte. Depois de pensar por alguns segundos sobre o SLA de 99.9% e o código livre de bugs, ele faz uma pergunta.

AI: Onde você armazena os logs?

P: Claro, em production.log. Aliás, sim, mas como podemos acessá-los sem ssh?

AI: Se você deixá-los nos arquivos, uma solução já foi inventada para você. O comando docker exec permite executar qualquer comando em um contêiner. Por exemplo, você pode fazer cat para logs. E usando a chave -isto e executar o bash (se instalado no contêiner) lhe dará acesso interativo ao contêiner.

Mas você não deve armazenar logs em arquivos. No mínimo, isso leva ao crescimento descontrolado do contêiner e ninguém gira as toras. Todos os logs devem ser enviados para stdout. Lá eles já podem ser visualizados usando o comando logs do docker.

P: Igor Ivanovich, talvez eu possa colocar os logs em um diretório montado, em um nó físico, como dados do usuário?

AI: É bom que você não tenha esquecido de remover os dados carregados no disco do nó. Você também pode fazer isso com logs, mas não se esqueça de configurar a rotação.
É isso, você pode correr.

P: Igor Ivanovich, você pode me aconselhar sobre o que ler?

AI: Primeiro, leia recomendações de desenvolvedores Docker, quase ninguém conhece o Docker melhor do que eles.

E se você quiser conseguir um estágio, acesse intensivo. Afinal, teoria sem prática está morta.

Fonte: habr.com

Adicionar um comentário