Introdução ao Docker: simplificando o DevOps
Publicados: 2022-03-11Se você gosta de baleias, ou está simplesmente interessado em entrega contínua rápida e indolor de seu software para produção, então convido você a ler este Tutorial introdutório do Docker. Tudo parece indicar que os contêineres de software são o futuro da TI, então vamos dar um mergulho rápido com as baleias contêiner Moby Dock e Molly.
O Docker, representado por um logotipo com uma baleia de aparência amigável, é um projeto de código aberto que facilita a implantação de aplicativos dentro de contêineres de software. Sua funcionalidade básica é habilitada por recursos de isolamento de recursos do kernel Linux, mas fornece uma API amigável ao usuário. A primeira versão foi lançada em 2013 e desde então se tornou extremamente popular e está sendo amplamente utilizada por muitos grandes players, como eBay, Spotify, Baidu e muito mais. Na última rodada de financiamento, o Docker conseguiu enormes US$ 95 milhões e está a caminho de se tornar um item básico dos serviços de DevOps.
Analogia de transporte de mercadorias
A filosofia por trás do Docker pode ser ilustrada com a seguinte analogia simples. Na indústria de transporte internacional, as mercadorias devem ser transportadas por diferentes meios, como empilhadeiras, caminhões, trens, guindastes e navios. Essas mercadorias vêm em diferentes formas e tamanhos e têm diferentes requisitos de armazenamento: sacos de açúcar, latas de leite, plantas etc. Historicamente, era um processo doloroso, dependendo da intervenção manual em cada ponto de trânsito para carga e descarga.
Tudo mudou com a aceitação de contêineres intermodais. Como eles vêm em tamanhos padrão e são fabricados com o transporte em mente, todas as máquinas relevantes podem ser projetadas para lidar com isso com o mínimo de intervenção humana. O benefício adicional dos contêineres selados é que eles podem preservar o ambiente interno, como temperatura e umidade, para mercadorias sensíveis. Como resultado, o setor de transporte pode parar de se preocupar com as próprias mercadorias e se concentrar em levá-las de A a B.
E é aqui que o Docker entra e traz benefícios semelhantes para a indústria de software.
Como é diferente das máquinas virtuais?
De relance, as máquinas virtuais e os contêineres do Docker podem parecer semelhantes. No entanto, suas principais diferenças se tornarão aparentes quando você der uma olhada no diagrama a seguir:
Os aplicativos executados em máquinas virtuais, além do hipervisor, exigem uma instância completa do sistema operacional e quaisquer bibliotecas de suporte. Os contêineres, por outro lado, compartilham o sistema operacional com o host. O hipervisor é comparável ao mecanismo de contêiner (representado como Docker na imagem) no sentido de gerenciar o ciclo de vida dos contêineres. A diferença importante é que os processos executados dentro dos contêineres são exatamente como os processos nativos no host e não apresentam nenhuma sobrecarga associada à execução do hipervisor. Além disso, os aplicativos podem reutilizar as bibliotecas e compartilhar os dados entre os contêineres.
Como ambas as tecnologias possuem pontos fortes diferentes, é comum encontrar sistemas combinando máquinas virtuais e containers. Um exemplo perfeito é uma ferramenta chamada Boot2Docker descrita na seção de instalação do Docker.
Arquitetura do Docker
Na parte superior do diagrama de arquitetura há registros. Por padrão, o registro principal é o Docker Hub que hospeda imagens públicas e oficiais. As organizações também podem hospedar seus registros particulares, se desejarem.
No lado direito temos imagens e containers. As imagens podem ser baixadas de registros explicitamente ( docker pull imageName
) ou implicitamente ao iniciar um contêiner. Depois que a imagem é baixada, ela é armazenada em cache localmente.
Recipientes são as instâncias de imagens - eles são a coisa viva. Pode haver vários contêineres em execução com base na mesma imagem.
No centro, está o daemon do Docker responsável por criar, executar e monitorar contêineres. Ele também cuida da construção e armazenamento de imagens. Por fim, no lado esquerdo, há um cliente Docker. Ele fala com o daemon via HTTP. Soquetes Unix são usados quando na mesma máquina, mas o gerenciamento remoto é possível via API baseada em HTTP.
Instalando o Docker
Para obter as instruções mais recentes, você deve sempre consultar a documentação oficial.
O Docker é executado nativamente no Linux, portanto, dependendo da distribuição de destino, pode ser tão fácil quanto sudo apt-get install docker.io
. Consulte a documentação para obter detalhes. Normalmente, no Linux, você precede os comandos do Docker com sudo
, mas vamos ignorá-lo neste artigo para maior clareza.
Como o daemon do Docker usa recursos de kernel específicos do Linux, não é possível executar o Docker nativamente no Mac OS ou Windows. Em vez disso, você deve instalar um aplicativo chamado Boot2Docker. O aplicativo consiste em uma máquina virtual VirtualBox, o próprio Docker e os utilitários de gerenciamento Boot2Docker. Você pode seguir as instruções de instalação oficiais para MacOS e Windows para instalar o Docker nessas plataformas.
Usando o Docker
Vamos começar esta seção com um exemplo rápido:
docker run phusion/baseimage echo "Hello Moby Dock. Hello Molly."
Devemos ver esta saída:
Hello Moby Dock. Hello Molly.
No entanto, muito mais aconteceu nos bastidores do que você imagina:
- A imagem 'phusion/baseimage' foi baixada do Docker Hub (se já não estivesse no cache local)
- Um contêiner baseado nesta imagem foi iniciado
- O comando echo foi executado dentro do container
- O contêiner foi interrompido quando o comando foi encerrado
Na primeira execução, você pode notar um atraso antes que o texto seja impresso na tela. Se a imagem tivesse sido armazenada em cache localmente, tudo levaria uma fração de segundo. Detalhes sobre o último contêiner podem ser recuperados executando docker ps -l
:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES af14bec37930 phusion/baseimage:latest "echo 'Hello Moby Do 2 minutes ago Exited (0) 3 seconds ago stoic_bardeen
Fazendo o próximo mergulho
Como você pode ver, executar um comando simples no Docker é tão fácil quanto executá-lo diretamente em um terminal padrão. Para ilustrar um caso de uso mais prático, no restante deste artigo, veremos como podemos utilizar o Docker para implantar um aplicativo de servidor web simples. Para manter as coisas simples, vamos escrever um programa Java que trata requisições HTTP GET para '/ping' e responde com a string 'pong\n'.
import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class PingPong { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/ping", new MyHandler()); server.setExecutor(null); server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { String response = "pong\n"; t.sendResponseHeaders(200, response.length()); OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); } } }
Dockerfile
Antes de criar sua própria imagem do Docker, é uma boa prática verificar primeiro se existe uma existente no Docker Hub ou em qualquer registro privado ao qual você tenha acesso. Por exemplo, em vez de instalarmos o Java nós mesmos, usaremos uma imagem oficial: java:8
.

Para construir uma imagem, primeiro precisamos decidir sobre uma imagem base que vamos usar. É indicado pela instrução FROM . Aqui, é uma imagem oficial do Java 8 do Docker Hub. Vamos copiá-lo em nosso arquivo Java emitindo uma instrução COPY . Em seguida, vamos compilá-lo com RUN . A instrução EXPOSE indica que a imagem estará fornecendo um serviço em uma determinada porta. ENTRYPOINT é uma instrução que queremos executar quando um container baseado nesta imagem é iniciado e o CMD indica os parâmetros padrão que vamos passar para ele.
FROM java:8 COPY PingPong.java / RUN javac PingPong.java EXPOSE 8080 ENTRYPOINT ["java"] CMD ["PingPong"]
Depois de salvar essas instruções em um arquivo chamado “Dockerfile”, podemos construir a imagem do Docker correspondente executando:
docker build -t toptal/pingpong .
A documentação oficial do Docker tem uma seção dedicada às melhores práticas para escrever o Dockerfile.
Contêineres em execução
Quando a imagem foi construída, podemos trazê-la à vida como um contêiner. Existem várias maneiras de executar contêineres, mas vamos começar com uma simples:
docker run -d -p 8080:8080 toptal/pingpong
onde -p [port-on-the-host]:[port-in-the-container] denota o mapeamento de portas no host e no contêiner, respectivamente. Além disso, estamos dizendo ao Docker para executar o contêiner como um processo daemon em segundo plano, especificando -d . Você pode testar se o aplicativo do servidor web está em execução tentando acessar 'http://localhost:8080/ping'. Observe que nas plataformas em que o Boot2docker está sendo usado, você precisará substituir 'localhost' pelo endereço IP da máquina virtual em que o Docker está sendo executado.
No Linux:
curl http://localhost:8080/ping
Em plataformas que requerem Boot2Docker:
curl $(boot2docker ip):8080/ping
Se tudo correr bem, você deverá ver a resposta:
pong
Viva, nosso primeiro contêiner personalizado do Docker está vivo e nadando! Também podemos iniciar o contêiner em um modo interativo -i -t . No nosso caso, substituiremos o comando entrypoint para que seja apresentado um terminal bash. Agora podemos executar os comandos que quisermos, mas sair do contêiner o interromperá:
docker run -i -t --entrypoint="bash" toptal/pingpong
Há muitas outras opções disponíveis para usar na inicialização dos contêineres. Vamos cobrir mais alguns. Por exemplo, se quisermos manter os dados fora do contêiner, podemos compartilhar o sistema de arquivos do host com o contêiner usando -v . Por padrão, o modo de acesso é leitura-gravação, mas pode ser alterado para modo somente leitura anexando :ro
ao caminho do volume intra-contêiner. Os volumes são particularmente importantes quando precisamos usar informações de segurança como credenciais e chaves privadas dentro dos contêineres, que não devem ser armazenadas na imagem. Além disso, também pode impedir a duplicação de dados, por exemplo, mapeando seu repositório Maven local para o contêiner para evitar que você baixe a Internet duas vezes.
O Docker também tem a capacidade de vincular contêineres. Os contêineres vinculados podem conversar entre si mesmo se nenhuma das portas estiver exposta. Isso pode ser feito com –link other-container-name . Abaixo está um exemplo combinando os parâmetros mencionados acima:
docker run -p 9999:8080 --link otherContainerA --link otherContainerB -v /Users/$USER/.m2/repository:/home/user/.m2/repository toptal/pingpong
Outras operações de contêiner e imagem
Sem surpresa, a lista de operações que podem ser aplicadas aos contêineres e imagens é bastante longa. Por brevidade, vejamos apenas alguns deles:
- stop - Interrompe um contêiner em execução.
- start - Inicia um contêiner parado.
- commit - Cria uma nova imagem a partir das alterações de um contêiner.
- rm - Remove um ou mais contêineres.
- rmi - Remove uma ou mais imagens.
- ps - Lista contêineres.
- imagens - Lista imagens.
- exec - Executa um comando em um contêiner em execução.
O último comando pode ser particularmente útil para fins de depuração, pois permite que você se conecte a um terminal de um contêiner em execução:
docker exec -i -t <container-id> bash
Docker Compose para o mundo dos microsserviços
Se você tiver mais do que apenas alguns contêineres interconectados, faz sentido usar uma ferramenta como docker-compose. Em um arquivo de configuração, você descreve como iniciar os contêineres e como eles devem ser vinculados entre si. Independentemente da quantidade de contêineres envolvidos e suas dependências, você pode ter todos eles funcionando com um comando: docker-compose up
.
Docker na natureza
Vamos analisar três estágios do ciclo de vida do projeto e ver como nossa baleia amiga pode ajudar.
Desenvolvimento
O Docker ajuda você a manter seu ambiente de desenvolvimento local limpo. Em vez de ter várias versões de diferentes serviços instalados, como Java, Kafka, Spark, Cassandra, etc., você pode simplesmente iniciar e parar um contêiner necessário quando necessário. Você pode dar um passo adiante e executar várias pilhas de software lado a lado, evitando a mistura de versões de dependência.
Com o Docker, você pode economizar tempo, esforço e dinheiro. Se o seu projeto for muito complexo de configurar, “dockerize” ele. Passe pela dor de criar uma imagem do Docker uma vez e, a partir deste ponto, todos podem iniciar um contêiner em um piscar de olhos.
Você também pode ter um “ambiente de integração” rodando localmente (ou em CI) e substituir stubs por serviços reais rodando em containers Docker.
Teste / Integração Contínua
Com o Dockerfile, é fácil obter compilações reproduzíveis. Jenkins ou outras soluções de CI podem ser configuradas para criar uma imagem do Docker para cada compilação. Você pode armazenar algumas ou todas as imagens em um registro privado do Docker para referência futura.
Com o Docker, você só testa o que precisa ser testado e tira o ambiente da equação. A execução de testes em um contêiner em execução pode ajudar a manter as coisas muito mais previsíveis.
Outro recurso interessante de ter contêineres de software é que é fácil criar máquinas escravas com a configuração de desenvolvimento idêntica. Pode ser particularmente útil para testes de carga de implantações em cluster.
Produção
O Docker pode ser uma interface comum entre desenvolvedores e equipe de operações, eliminando uma fonte de atrito. Ele também incentiva a mesma imagem/binários a serem usados em todas as etapas do pipeline. Além disso, poder implantar um contêiner totalmente testado sem diferenças de ambiente ajuda a garantir que nenhum erro seja introduzido no processo de compilação.
Você pode migrar facilmente os aplicativos para a produção. Algo que antes era um processo tedioso e esquisito agora pode ser tão simples quanto:
docker stop container-id; docker run new-image
E se algo der errado ao implantar uma nova versão, você sempre poderá reverter ou alterar rapidamente para outro contêiner:
docker stop container-id; docker start other-container-id
… garantido para não deixar nenhuma bagunça para trás ou deixar as coisas em um estado inconsistente.
Resumo
Um bom resumo do que o Docker faz está incluído em seu próprio lema: Build, Ship, Run.
- Build - O Docker permite que você componha seu aplicativo a partir de microsserviços, sem se preocupar com inconsistências entre os ambientes de desenvolvimento e produção e sem se prender a nenhuma plataforma ou linguagem.
- Ship - Docker permite projetar todo o ciclo de desenvolvimento, teste e distribuição de aplicativos e gerenciá-lo com uma interface de usuário consistente.
- Run - O Docker oferece a capacidade de implantar serviços escaláveis de forma segura e confiável em uma ampla variedade de plataformas.
Divirta-se nadando com as baleias!
Parte deste trabalho é inspirado em um excelente livro Using Docker de Adrian Mouat.