DEV Community

Rodolpho Alves
Rodolpho Alves

Posted on • Originally published at Medium on

Cell CMS — Utilizando Docker para criar Containers

Cell CMS — Utilizando Docker para criar Containers

Photo by Jonas Smith on Unsplash

No último postfalamos sobre geração de logs , utilizando o Serilog e o ApplicationInsights para termos um monitoramento robusto, com métricas de performance e erros! No post de hoje vamos permitir que nossa API seja executada em um Container através do Docker! Além disso vamos dar uma olhada em como o Visual Studio 2019 permite a integração, durante o desenvolvimento, com os containers executados pelo Docker Desktop.

Os branches para o código de hoje serão 2: feature/dockerfile e feature/docker-compose.

O que são Containers e o qual problema eles resolvem

Mas na minha máquina funciona! — Vários desenvolvedores, circa sempre.

Quem nunca, em algum ponto, não soltou a exclamação acima? Quando estamos desenvolvendo costumamos estar no “melhor ambiente” possível. Todas as dependências estão instaladas (e normalmente atualizadas), temos todas as nossas ferramentas de Debug e temos acesso administrativo ao sistema operacional.

Eis que chega a hora de publicar a aplicação e… não dá certo. É erro aqui, erro ali, configuração faltante e assim vai. Isso pra não falarmos de problemas de rede, que ficam mais para o lado do pessoal de operações, normalmente.

Com a chegada de maior poder computacional começamos a utilizar Máquinas Virtuais para executar nossos sistemas de maneira mais isolada. Com máquinas virtuais ainda temos todo o trabalho de manter um sistema operacional e configura-lo, mas temos um isolamento melhor das dependências.

Nas clouds/vps temos essa opção que normalmente acaba sendo chamada de IAAS (Infrastructure As A Service), basicamente é assim:

“Aqui está uma máquina virtual com X processamento, Y ram e Z storage. A gente garante que ela fica no ar 99.99999% do tempo mas você fica responsável por instalar dependências, configurar e aplicar atualizações.”

Porém ainda temos um problema. Usar uma VM , completa, ainda requer muita manutenção e configuração , especialmente se nosso foco for agilidade. Eis que surgem o conceito de PAAS (Platform As A Service). Basicamente nesse modelo seu fornecedor de cloud/vps se encarrega de atualizar, manter e configurar o ambiente onde seu sistema será executado. Basicamente é assim:

“Aqui está um ambiente baseado em Linux, com NGINX instalado e com limites de X Y Z. A gente garante que estará sempre atualizado e ficará no ar 99.99999999% do tempo mas você ficará responsável por copiar sua aplicação e as configurações necessárias”

O pulo do gato aqui é que agora essa plataforma não necessariamente é dedicada a você. Não entrarei em detalhes mas neste modelo mas, através de configurações do sistema base e da máquina virtual, o fornecedor pode dedicar cotas de performance diferentes a aplicações diferentes e segregar o que está instalado para cada aplicação. Isso permite um custo mais baixo para o fornecedor e para você cliente!

E então temos, recentemente, a popularização do conceito de Containers. De maneira resumida:

Containers são “pacotes” para distribuição da sua aplicação junto com o ambiente e as dependências necessárias para sua execução. Ao contrário de máquinas virtuais, que simulam o Hardware, os Containers focam em simular o Sistema Operacional.

Em termos práticos: Você cria uma Imagem que irá conter todos os passos e dependências para rodar sua aplicação, como:

  1. Compilar sua aplicação
  2. Com o resultado da compilação, copiar para uma pasta X (por exemplo onde seu WebServer estará servindo)
  3. Aplicar as configurações necessárias (seja por variáveis de ambiente dentro da imagem, cópia de arquivos ou linha de comando. Podemos inclusive utilizar apt get para instalar dependências embarcadas)
  4. Executar a sua aplicação

Com a Imagem pronta você finalmente pode executa-la. Nesse ponto temos o Container da sua aplicação.

Agora vem a questão de um milhão de reais:

Mas Rodolpho, como criamos essas imagens!?

Meus amigos, como quase tudo em TI, temos diversas tecnologias para criarmos Imagens, algumas são:

  • Docker (o queridinho da vez!)
  • Rkt
  • Containerd

Então vamos falar, finalmente, sobre Docker!

Docker, Imagens, Containers e o DockerHub

Moby, o mascote do Docker (fonte: https://www.docker.com/sites/default/files/d8/2019-07/Moby-logo.png)

Antes de partirmos para os detalhes técnicos, vamos conhecer um pouco sobre a empresa. Docker é a desenvolvedora de duas soluções que facilitam, bastante, a utilização de containers por desenvolvedores:

  • Docker Desktop : Ferramenta para facilitar a instalação e uso de containers em ambiente Windows e Mac OS
  • DockerHub : Repositório central para publicação de imagens (um NuGet ou NPM para imagens Docker)

Existem várias outras contribuições da empresa para a comunidade Open Source e na criação da OCI (Open Container Initiative), porém essa parte escapa da ideia do post de focarmos em como utilizar! Recomendo uma boa lida no site da Docker e do OCI, vale a pena!

Anyway, vamos para a parte prática! Não vou abordar neste post como instalar e configurar o Docker Desktop porém se você precisa deste passo a passo pode conferir um tutorial muito bom no própio site da Docker.

Partiremos daqui, Docker Desktop instalado e pronto para rodar!

Vamos abrir um terminal e digitar a sugestão do Docker Desktop.

Executando a imagem do Tutorial

Após rodar o comando você poderá acessar http://localhost e acompanhar o propio tutorial do pessoal da Docker! Porém, vamos destrinchar aqui o comando que foi executado:

  • docker indica que vamos utilizar o docker-cli 😅
  • run indica que vamos executar uma imagem
  • -dp 80:80 indica duas coisas: -d sinaliza que o daemon execute em background e -p 80:80 indica que queremos que a porta 80 do sistema atual seja mapeada para a porta 80 do container (ou seja, quando acessarmos a porta 80 da nossa máquina a chamada será encaminhada para a porta 80 do container)
  • docker/getting-started sinaliza a imagem a ser executada. Podemos identificar que será uma imagem com o nome de getting-started e virá do repositório/usuário docker. Note que não descrevemos nenhuma tag (através de :tag no fim da imagem) então a tag latest será utilizada

Existem outros argumentos importantes e que utilizaremos bastante, eles são:

  • --volume ou -v: Permite mapearmos uma pasta/volume da nossa máquina para dentro do container
  • --env ou -e : Permite que definamos valores para variáveis de ambiente dentro do container
  • --network: Permite que defininamos uma rede (virtual) onde o container será conectado

A referência completa dos parâmetros pode ser encontrada aqui. Agora vamos discutir o local de onde pegamos a imagem: DockerHub.

O DockerHub é o Registry (ou seja um repositório de imagens) oficial da Docker. Existem outros e é possível executar seu próprio registry (inclusive atrás de Containers!), mas o mais famoso é o DockerHub. Nele podemos encontrar imagens para várias aplicações, como:

Antes de falarmos sobre como as imagens são criadas, vamos abordar mais uma questão:

“Como desenvolvedor, qual a vantagem de utilizar Containers ao invés de instalar tudo na minha máquina!?”

Do ponto de vista técnico eu diria que a principal vantagem é poder aproximar ao máximo seu ambiente de desenvolvimento ao ambiente de produção mantendo as mesmas versões das dependências. Inclusive, você pode garantir que todos os desenvolvedores estarão utilizando as mesmas versões de dependências para seu projeto!

Outra vantagem em qualidade de “vida”, especialmente no caso de quem utiliza o Windows, é que containers podem ser criados e deletados sob demanda e facilmente. Assim quando você estiver trabalhando em outro projeto basta parar ou remover os containers e os recursos serão liberados.

Por exemplo, eu gosto de experimentar com diferentes bancos de dados (inclusive armazeno vários docker-compose em meu GitHubpara facilitar o scaffold de projetos) e se eu fosse instalar todos em meu PC (que também uso para jogar! hahaha) seria um inferno de performance. Ter uma VM para cada um? Haja disco! Então, pra mim, a flexibilidade de poder subir e descer estes containers é algo que me ajudou bastante a estudar e utilizar diferentes bancos de dados em vários projetos .NET!

Bem, vamos agora ver como estas imagens são criadas?

Criando o Dockerfile para nossa API

Vamos realizar o processo da maneira fácil, utilizando o Visual Studio 2019 (também dá para fazer no 2017), mas pegarei o resultado e comentarei passo-a-passo o que está sendo feito 😄.

Abra a Solution no Visual Studio e no Solution Explorer clique com o direito sobre a API e escolha Add > Docker Support… e, quando aparecer o Dialog, escolha a opção de Linux Containers

Após finalizar o processo o Visual Studio fará as seguintes operações:

  1. Um novo arquivo, Dockerfile, será criado na raiz do projeto
  2. Uma nova entrada será adicionada no launchSettings.json do projeto
  3. Algumas novas linhas serão adicionadas no .csproj do projeto
  4. Um novo arquivo .dockerignore será criado na raiz da solution

Vamos por partes, primeiro olhando o mais importante de todos: o Dockerfile.

O Dockerfile é o passo-a-passo para que o Docker Daemon (processo que realiza a “compilação” das imagens) saiba de onde começar, o que realizar e para onde jogar os resultados. É uma receita de bolo a ser executada para disponibilizar nosso aplicativo.

https://medium.com/media/94df0ea7345472f920daea8ea38d2762/href

Basicamente o Dockerfile vai conter todos os passos que você executaria para publicar sua API através da linha de comando. Para quem está acostumado a executar as coisas sempre pelo Visual Studio os comandos podem parecer estranhos, mas rapidamente nos acostumamos com eles.

No começo da minha jornada com Docker passei duas semanas utilizando apenas o Visual Studio Code e o Powershell, para ficar mais familiar ao processo “manual” de compilar e distribuir aplicações .NET.

Algo interessante é que o padrão do Visual Studio já vem com uma otimização muito boa chamada de Multi-Stage Builds. Isso permite que o Docker utilize um cache entre cada passo do Dockerfile e dessa maneira tenha de recompilar apenas partes alteradas da Imagem.

Por exemplo: O step 8 (Restore dos NuGets) só será refeito caso ocorra alguma alteração nas dependências do csproj, que é copiado no step 7!

A segunda alteração, no launchSettings.json , nos permite executar e depurar o nosso container através do Visual Studio.

LaunchSettings.json com Docker

A adição das propriedades httpPort e sslPort nos permitem garantir que o container executado para Debug responda sempre nestas mesmas portas! Pessoalmente não sei por qual motivo estas configurações não são adicionadas automaticamente assim como são para um projeto executado da maneira convencional.

A alteração no .csproj permite a integração entre o Visual Studio e o Docker Desktop , vamos dar uma olhada:

.csproj com as Tags adicionais. A DockerfileTag grifada foi adicionada manualmente e será explicada quando formos publicar a imagem!

Finalmente o .dockerignore adicionado:

Algumas linhas foram adicionadas

O .dockerignore é para o Docker o que o .gitignore é para o Git. Um .dockerignore bem escrito permite que apenas os arquivos estritamente necessários sejam copiados para a nossa imagem.

Finalmente podemos ver a integração sendo executada através do Containers View. Caso ele não esteja visível por padrão em seu Visual Studio o menu para habilita-lo é View > Other Windows > Containers. O Container View nos permite visualizar rapidamente quais containers estão sendo executados , seus logs , portas expostas, arquivos dentro do container e as variáveis de ambiente definidas!

Logs do Container expostos no Containers View

A adição do Docker à API é simples assim utilizando o Visual Studio. Porém leia cuidadosamente o Dockerfile para entender o que está sendo feito.

Procure no GitHub exemplos de outros Dockerfiles de outras aplicações, dê uma boa olhada nos comandos do Docker. Existem vários conceitos (como Volumes e Networks) que precisam bem mais do que um post para serem explicados e são melhor vistos na prática do que na teoria! Eventualmente farei um post mais focado nos detalhes do Docker e, um dia, sobre Kubernetes. Porém o foco desta série é o Cell CMS ponta-a-ponta.

Criando o Orquestrador (Docker Compose) para a API

Antes de falarmos sobre a integração com o Visual Studio, vamos ver o que é um “Docker Compose”:

Um docker compose é um documento em formatoyaml que descreve quais imagens deve ser executadas , suas dependências , suas configurações e como devem se comunicar.

Apesar da ideia do Cell CMS ser uma aplicação “lobo solitário”, vamos adicionar suporte á Orquestração de Containers ao nosso Projeto! Será mais para demonstrar a utilidade do Docker Compose, e sua integração com o Visual Studio, durante o desenvolvimento. De maneira resumida a vantagem de adicionar um docker compose à sua Solution é garantir que desenvolvedores com Docker Desktop facilmente tenham um ambiente pronto para desenvolver (e debugar) assim que abrirem seu projeto.

Ou seja: junto com o seu código você armazenará um documento que descreve as dependências necessárias do ambiente de sua aplicação. Apesar de não serem muito recomendados para produção os composes facilitam, e muito, os ambientes de desenvolvimento e testes.

Vamos lá! Com o Solution Explorer aberto clique em sua Api com botão direito e escolha Add > Container Orchestrator Support… e escolha “Docker Compose” no primeiro dialog.

Para este exemplo escolha Docker Compose, em outro post falaremos sobre Kubernetes!

Caso você tenha editado o Dockerfile o seguinte Dialog será apresentado, caso queira substituir o arquivo clique em Yes.

O Visual Studio irá gerar as seguintes alterações no projeto (e solution):

  1. Adicionar um novo projeto .dcproj à solution
  2. Gerar um arquivo .docker-compose.yml, este será nosso compose para o ambiente normal
  3. Gerar um arquivo .docker-compose.override.yml, este será nosso completamento (para ambientes de debug) ao compose principal
  4. Adicionar, ao projeto da API, uma referência ao .dcproj

Vamos focar nos arquivos .yml, a sintaxe é bem simples e fácil de decorar e achar referências, o que é bom pois o suporte do Visual Studio aos arquivos deixa a desejar 😅

Vamos dar uma olhada nos arquivos gerados:

docker-compose.yml

docker-compose.override.yml

Note a semelhança dos parâmetros das chaves do nosso docker-compose com os parâmetros do docker run. Isso não é mera coincidência! No fundo podemos encarar que o docker-compose é uma maneira de organizar e unificar diversos docker run.

Finalmente note, através do Containers View ou do Dashboard do Docker Desktop, que o Visual Studio automaticamente botou todos serviços que estão no docker-compose.yml para rodar:

Para arrumar as portas, e mantendo a integração que nos permite Debug através do Visual Studio, seguimos a mesma ideia da alteração do launchSettings.json. Como queremos isso apenas para ambientes de desenvolvimento vamos abrir o arquivo docker-compose.override.yml e adicionar um mapeamento explícito para portas do nosso container:

Achando o override no Solution Explorer

Alteração para necessária

Relembrando a sintaxe lá do docker run: À esquerda temos a porta do host (sua máquina) e à direita temos a porta do container (nossa api). No caso a configuração está dizendo que sempre que acessarmos http://localhost :5000 e https://localhost :5001 na máquina local a chamada será redirecionada para a posta 80/443 dentro do container!

Agora vamos confirmar que o Debug funciona? Para demonstrar vou adicionar um Breakpoint em um dos meus métodos do Startup.cs e mandar o Visual Studio iniciar o Debug:

Breakpoint (no host) sendo ativado pelo container.

Note que meu IConfiguration está devidamente populado com as configurações do appsettings.json, appsettings.Development.json, variáveis de ambiente e meu secrets.json. O mapeamento dos appsettingsocorrem durante a criação da nossa imagem, porém o mapeamento do secrets.json é um “bonus” do Scaffold que o Visual Studio fez do docker-compose.override.yml. Em específico a linha de volume que mapeia o diretório ${APPDATA}/Microsoft/UserSecrets da minha máquina (host) para o diretório /root/.microsoft/usersecrets do container, com o modo ro (readonly).

Para demonstrar como podemos alterar as configurações presentes no IConfiguration através de variáveis de ambiente nos containers vou renomear nossa database sqlite para “outraStorage.db” através da ConnectionString no docker-compose.override.yml:

Alterando, por ambiente, a ConnectionString da Database

Colocando um breakpoint e analisando o IConfiguration podemos ver os diversos providers e ver que o valor definido pelo docker-compose está lá:

Identificando o Provider de Variáveis de Ambiente

Encontrando nossas alterações dentro do Provider!

Este é o básicão sobre o Docker Compose. Como sempre recomendo a leitura da documentação para ver melhor os detalhes e até dicas mais avançadas! A documentação está disponível aqui.

Considerações Finais

Hoje vimos, bem superficialmente, o que são Containers, quais suas utilidades e como podemos utilizar o Docker Desktop junto do Visual Studio para facilitar nossa vida durante o desenvolvimento de aplicações. Este foi, acredito, o maior post da série até agora e ainda assim ficou extremamente superficial. Espero que o post tenha sido o suficiente para atiçar a curiosidade daqueles que ainda estavam indecisos sobre as vantagens de containers!

Prometo fazer um post algum dia indo nos pormenores do Docker e, inclusive, trazendo exemplos mais complexos de Orquestração (como, por exemplo, subir uma api, um worker, Um frontend e um banco ao mesmo tempo) mas no momento estou dedicado em avançar com o Cell CMS!

Para o próximo post vamos dar uma olhada em Testes Unitários de maneira produtiva e prática! Utilizando as bibliotecas xUnit, AutoFixture, Moq e FluentAssertions.

Obrigado por lerem mais este post, até a próxima e abraços!

Top comments (0)