Ciência de Dados com Docker — Parte 1: A New Container

Guilherme Levi Bernini
VLabs
Published in
11 min readMar 22, 2022

Este artigo é o primeiro de uma trilogia que visa trazer conceitos e boas práticas de DevOps para o mundo de Data Science. Ao fim desta jornada você será capaz de desenvolver e implementar aplicações de forma rápida e consistente.

Fonte: https://www.primogif.com/p/3ofSB5PPO4cbZMK796

Este primeiro artigo aborda o what, why e where do Docker, o segundo explicará como integrar o Docker ao seu fluxo de desenvolvimento utilizando o VSCode, e o terceiro te ensinará como criar e simular ambientes mais avançados.

Todos os materiais utilizados nesse artigo e nos futuros estarão disponíveis em: https://github.com/leonardotavares-visagio/docker-in-ds

Conteúdo

Docker in a nutshell
Principais conceitos de contêineres
VM’s vs Containers
The Docker way
Principais comandos do Docker CLI
Criando sua própria receita
Layers e Cache

Docker in a nutshell

“Na minha máquina funciona” — 8º postulado da programação

O universo de desenvolvimento de software é um ambiente extremamente colaborativo e de constante mudança. As ferramentas de versionamento e compartilhamento permitem que novas tecnologias se sustentem em soluções anteriores e, unidas a um mercado de trabalho extremamente aquecido, fomentam uma comunidade pautada na colaboração e no crescimento. Os impactos são evidentes: um mercado que se renova a cada poucos anos e gera cada vez mais valor.

A ultima revolução deste mercado está acontecendo neste exato momento. Você já deve ter ouvido falar de algum destes termos: DevOps, Computação em nuvem, Ciência de Dados & IA e Arquitetura de Microsserviços. Hoje, vamos apresentar uma ferramenta alinhada — e em alguns casos o próprio motor — com os assuntos mais atuais do desenvolvimento de software. Mas antes, um pouco de sofrimento…

Em um belo dia, você acorda inspirado e resolve desenvolver uma aplicação. Para isso, você precisa instalar algumas dependências. Você baixa o instalador da primeira, executa e dá de cara com um erro: a configuração da sua máquina é incompatível. Após algumas horas implementando uma solução emergencial — gambiarra — a instalação é concluída. Ao executar, você descobre que ela depende de outras bibliotecas. O processo se repete até que tudo esteja finalmente instalado. Por fim, após alguns dias configurando, você consegue desenvolver a aplicação.

Ela é perfeita, resolve as dores mais profundas da humanidade. Hora de compartilhá-la com o mundo e receber os louros da conquista. Só um pequeno detalhe: ela não roda na máquina do seu amigo com outro sistema operacional, nem no computador do seu chefe com nenhuma dependência instalada e muito menos naquela instância na nuvem que você alugou no precinho — e que já consumiu todas as suas economias do mês. Seria ótimo se você pudesse empacotar a sua solução junto com todas as configurações e dependências necessárias e enviar para eles utilizarem. Na verdade, você pode…

A tecnologia de contêineres, sendo o Docker a mais utilizada, permite a configuração de um ambiente para desenvolvimento ou deploy de aplicações de forma:

  • Simples: É possível configurar ambientes com um arquivo e poucas linhas de código;
  • Rápida: Utiliza um sistema de cache e layers que agiliza a inicialização de ambientes;
  • Leve: Pois envolve apenas o necessário;
  • Consistente: Faz tudo isso de maneira replicável.

TLDR:
Resumidamente, o Docker torna extremamente fácil instalar, rodar, corrigir e publicar códigos sem se preocupar com a consistência do setup e das dependências. Isso faz com que ele esteja alinhado com as melhores práticas de DevOps e computação na nuvem, além de permitir escalabilidade e reduzir a complexidade.

Principais conceitos de contêineres

The cake is a lie” — Portal

Antes de embarcarmos no Docker, é importante nos familiarizarmos com alguns conceitos:

Um contêiner, como você já deve ter imaginado, é o pacote que engloba apenas a aplicação e suas dependências. Desta maneira, é possível ter várias cópias de um contêiner rodando em uma ou mais máquinas e até mesmo vários contêineres interagindo entre si. A vantagem é que cada tipo de contêiner pode ser especialista em resolver uma determinada tarefa, permitindo inserir, alterar e escalar partes da sua aplicação conforme a necessidade.

O que diferencia contêineres e ao mesmo tempo garante essa replicabilidade é a imagem que deu origem à eles. A imagem é a unidade atômica desta tecnologia e, em poucas palavras, ela nada mais é do que uma “foto” de um contêiner.

Por fim, temos a Dockerfile. Se o contêiner fosse um bolo de cenoura, a Dockerfile seria a receita. Assim como na cozinha, na hora de decidir o prato do dia, você pode decidir se vai desenvolver a sua própria receita ou procurar na internet alguma que satisfaça suas necessidades. Você pode até mesmo misturar a receita de um bolo com a de sorvete e dar origem a um delicioso petit gateau. E o melhor de tudo: não vai precisar se preocupar na hora de compartilhar a sua receita com outras pessoas ou de utilizar a receita de terceiros, todo contêiner originado por uma mesma Dockerfile é exatamente igual aos outros.

No começo, é comum que haja confusão entre os conceitos de imagem e contêiner. Conforme adentrarmos no assunto, a relação entre eles ficará cada vez mais clara! Se em algum ponto da leitura você se sentir inseguro quanto as definições, te encorajo a retornar a esta seção quantas vezes forem necessárias. =]

E agora, um pouco de batalha…

VM’s vs Containers

“It’ssssss timeee” — That UFC guy

Até pouco tempo atrás, as máquinas virtuais dominavam quando o assunto era isolar e hospedar. A mentalidade era abstrair uma determinada parcela da infraestrutura disponível e dedicá-la a uma aplicação, suas dependências e sistema operacional, tendo um hypervisor como meio de campo. Desta maneira, as aplicações eram desenhadas de forma monolítica, embarcando todas as funcionalidades em um único ambiente. Essa era a forma mais eficiente, pois modularizar uma aplicação em parcelas menores e especializadas envolveria dedicar mais computação e armazenamento para os novos sistemas operacionais e seus processos. Porém, um novo xerife chegou à cidade…

Assistant (to the) Regional Sheriff. Fonte: https://tenor.com/view/new-sheriff-in-town-gif-11209577

O surgimento dos contêineres viabilizou um tipo diferente de arquitetura. Fazendo uso de um container engine (falaremos mais adiante sobre o Docker Engine), podemos isolar e hospedar diversas aplicações e suas dependências de maneira mais enxuta, sendo que a interação entre elas é feita de forma simples e rápida. Desta forma, cada domínio se torna uma aplicação por si só, focada na realização de uma única tarefa e sendo passível de alterações e substituições sem afetar diretamente o todo.

Diferenças entre VM’s e Contêineres. Fonte: https://www.weave.works/blog/a-practical-guide-to-choosing-between-docker-containers-and-vms

Se fossemos traçar um head-to-head entre as duas opções, teríamos algo assim:

VM’s:

  • Faz uso de sistemas operacionais para hostear cada aplicação e suas respectivas dependências.
  • Favorece arquiteturas monolíticas

Containers:

  • Através do container engine, associa parcelas da infraestrutura para os ambientes isolados de cada aplicação e suas respectivas dependências
  • Favorece arquiteturas baseadas no conceito de microsserviços
Docker vs. VM. Oil on canvas.

Clubismos a parte, gostaria de salientar a importância de uma análise crítica antes da escolha apressada por um ou outro tipo de arquitetura. Esta decisão terá grande impacto no desenvolvimento e na manutenção da aplicação, sendo fundamental avaliar de forma exaustiva os requisitos funcionais e os recursos disponíveis. Afinal, existem caso em que — pasmem — as boas e velhas VM’s ainda se sobressaem.

Mas chega de enrolação, bora falar do que interessa.

The Docker way

“Is it possible to learn this power?” — Anakin about containers
“Not from a Jedi…” — Palpatine

O Docker envolve um grande ecossistema pautado na criação, utilização, armazenamento e compartilhamento de imagens.

Ecossistema Docker.

Em termos operacionais, podemos falar sobre a santíssima trindade:

  • Docker Daemon: É o servidor responsável por construir, iniciar e monitorar os contêineres.
  • Docker Client (CLI): É a interface em que o usuário insere os comandos para administrar os contêineres. A comunicação com o servidor é feita através de uma REST API. O Docker Engine que citamos anteriormente é a união Docker Daemon + REST API + Docker Client.
  • Docker Registry: É um sistema de armazenamento de imagens. Você pode subir o seu próprio Docker Registry utilizando docker run registry:latest ou interagir com o Docker Hub, que é o registro público mais utilizado. Por padrão, se o Docker não encontrar uma imagem localmente, ele buscará por ela no Docker Hub.
Fonte: https://docs.docker.com/get-started/overview/

O comando citado acima é um exemplo de utilização do Docker Client. A título de curiosidade, vou elencar abaixo os comandos mais utilizados.

Principais comandos do Docker CLI

“The power of containerization in the palm of my hand” — Dr. Otto Octavius

1. docker run <flags> <nome_container> <comando_opcional>
Responsável por executar a imagem criada anteriormente/puxada do Docker Hub. Flags mais utilizadas:

  • Input: it - “Liga” o terminal do host com o do contêiner, permitindo o envio de inputs
  • Volume: v - Configura uma associação entre pastas dentro e fora do contêiner
  • Porta: p - Configura relação entre portas do contêiner e do host

2. docker stop/kill <nome_container>
Responsável por parar a execução de um contêiner

3. docker build <flags> <pasta_com_Dockerfile>
Responsável por construir a imagem a partir do Dockerfile. Flags mais utilizadas:

  • Tagging: -t - Define uma tag
  • Escolha de Dockerfile: -f

4. docker ps
Responsável por mostrar os contêiner em execução

Legal, então vou ter que digitar um comando no terminal sempre que quiser configurar algo dos meus contêineres? Na verdade não. Essa abordagem pode ser interessante pois permite uma certa agilidade, afinal, é possível subir um contêiner totalmente operacional com uma única linha de código. Mas para usufruir de toda a automação e replicabilidade disponibilizada pelo Docker, we have to go back…

Criando sua própria receita

“5 minutos” — Erick Jacquin

Como dito algumas seções atrás, a Dockerfile é a receita de um contêiner. No começo de uma receita, é comum listarmos os ingredientes. Ovos, farinha, leite, os elementos básicos que vamos precisar. Mas imagina só se você pudesse começar uma receita com base em outra. Ao invés de começar o petit gateau tendo que fazer um bolo, você poderia só estender a mão e pegar um pronto. Com o Docker isso é possível — e extremamente comum:

  • FROM: Com este comando, podemos partir de uma imagem pronta.

Quando escolhemos uma imagem, já teremos uma série de diretórios dentro da mesma. Para escolhermos onde será nosso ponto de início, utilizamos o seguinte comando:

  • WORKDIR: Muda o diretório de trabalho

Em seguida, podemos escolher quais arquivos da máquina hospedeira queremos levar para dentro do contêiner:

  • COPY: Copia arquivos/pastas do host para dentro do contêiner

Além de manipular diretórios, pode ser necessário rodar comandos dentro do contêiner, seja para instalar as dependências ou para realizar quaisquer outras ações. Para isso, utilizamos:

  • RUN: Executa um comando dentro do contêiner

Por fim, após concluir todos os passos, podemos comandar nosso contêiner a performar uma atividade para qual ele foi projetado. Desta maneira:

  • CMD: Define o comando a ser executado após a inicialização do contêiner

É importante frisar que esta não é uma lista exaustiva de todos os comandos. Caso você queira se aprofundar nas opções, recomendo a leitura da documentação.

A Dockerfile abaixo possui todos os comandos que citei.

# Seleciona a imagem base
FROM python:3.10-slim

# Seleciona o diretório que estamos utilizando para os comandos
WORKDIR "/app"

# Copia o arquivo de requisitos para dentro do container
COPY ./requirements.txt ./

# Instala as dependências
RUN pip install -r requirements.txt

# Copia todos os arquivos para dentro do container
COPY . .

# Configura o comando padrão para o container
CMD python main.py

Você deve ter notado a repetição do comando COPY. Por que primeiro eu copiei apenas um arquivo e depois todo o restante? Não seria muito mais eficiente copiar tudo logo de cara? Muito pelo contrário. Isso está relacionado com uma outra peculiaridade da Dockerfile que torna ela ainda mais atraente.

Layers e Cache

“Ogres are like containers, they both have layers” — Shrek

A Dockerfile é executada de maneira sequencial. Cada comando (step) cria um layer acima do anterior e o armazena em um cache. Este racional agiliza muito o processo de rebuild. Se um comando é alterado no meio da Dockerfile, apenas ele e os comandos seguintes serão executados novamente enquanto os layers anteriores são carregados do cache.

Para deixar mais claro, considere o exemplo anterior com uma pequena alteração:

# Seleciona a imagem base
FROM python:3.10-slim

# Seleciona o diretório que estamos utilizando para os comandos
WORKDIR "/app"

# Copia todos os arquivos para dentro do container
COPY . .

# Instala as dependências
RUN pip install -r requirements.txt

# Configura o comando padrão para o container
CMD python main.py

O que foi alterado é que utilizamos o comando COPY apenas uma vez para copiar todos os arquivos para dentro do contêiner e depois instalamos as dependências com o RUN. O resultado na prática é o mesmo, ambas dão origem a contêineres idênticos. Se compararmos esta Dockerfile com a anterior durante o primeiro processo de build, o desempenho seria de fato levemente superior.

Porém, a diferença se dá no caso corriqueiro de precisarmos fazer alterações nos arquivos e rebuildar o contêiner. Em ambos os exemplos, os layers anteriores ao comando COPY . . seriam carregados do cache, sendo apenas os comandos seguintes executados novamente. No segundo exemplo, isso envolveria o passo de instalar as dependências, o que adiciona BASTANTE tempo ao processo.

Neste sentido. É uma boa prática ordenar os comandos com base na frequência de atualização de cada um — da menor para a maior — diminuindo o tempo de build.

Posicionamento de layers com base na frequência de alteração. Fonte: https://openliberty.io/blog/2018/06/29/optimizing-spring-boot-apps-for-docker.html

No console, podemos verificar quais layers foram gerados e quais foram extraídos do cache:

-> docker build -t vlabs/ds-docker .Sending build context to Docker daemon
Step 1/6 : FROM python:3.10-slim
---> e4f3ae64bf20
Step 2/6 : WORKDIR "/app"
---> Using cache
---> 7c8b00530b84
Step 3/6 : COPY ./requirements.txt ./
---> Using cache
---> 87e4e97327b9
Step 4/6 : RUN pip install -r requirements.txt
---> Using cache
---> b36ef2a47cc7
Step 5/6 : COPY . .
---> 44a9fd70c768
Step 6/6 : CMD python main.py
---> Running in 783954d786bd

Como podemos observar, apenas os comandos 5 e 6 tiveram que ser executados novamente, agilizando bastante o processo.

Checkpoint

“It’s rewind time” — Uncle Phil’s nephew

Nós discutimos até aqui sobre o que é o Docker, quais as formas de operá-lo para gerenciamento de contêineres e quais as vantagens no uso de contêineres de forma bastante generalizada. Nas próximas semanas vamos entender quais as vantagens de se aplicar estes conceitos e ferramentas no contexto de ciência de dados, desde a fase de desenvolvimento até a implantação e manutenção das aplicações. Fiquem de olho nas nossas redes sociais e sigam o perfil do VLabs aqui no Medium. Até mais! 👋

--

--