Este repositório contem um exemplo simples de uma livraria virtual construída usando uma arquitetura de microsserviços.
O exemplo foi projetado para ser usado em uma aula prática sobre microsserviços, que pode, por exemplo, ser realizada após o estudo do Capítulo 7 do livro Engenharia de Software Moderna.
O objetivo da aula é permitir que o aluno tenha um primeiro contato com microsserviços e com tecnologias normalmente usadas nesse tipo de arquitetura, tais como Django, REST e Docker.
Como nosso objetivo é didático, na livraria virtual estão à venda apenas três livros, conforme pode ser visto na próxima figura, que mostra a interface Web do sistema. Além disso, a operação de compra apenas simula a ação do usuário, não efetuando mudanças no estoque. Assim, os clientes da livraria podem realizar apenas duas operações: (1) listar os produtos à venda; (2) calcular o frete de envio.
No restante deste documento vamos:
- Descrever o sistema, com foco na sua arquitetura.
- Apresentar instruções para sua execução local, usando o código disponibilizado no repositório.
- Descrever duas tarefas práticas para serem realizadas pelos alunos, as quais envolvem:
- Tarefa Prática #1: Implementação de uma nova operação em um dos microsserviços
- Tarefa Prática #2: Criação de containers Docker para facilitar a execução dos microsserviços.
- Tarefa Extra: Criação de docker compose para facilitar o manuseio dos containers docker.
A micro-livraria possui três microsserviços:
- Front-end: microsserviço responsável pela interface com usuário, conforme mostrado na figura anterior.
- Shipping: microserviço para cálculo de frete.
- Inventory: microserviço para controle do estoque da livraria.
Os microsserviços estão implementados em Python e javaScript, usando o Django REST Framework (DRF), em python para execução dos serviços no back-end e JavaScript, Css e Html para execucao do Front-end.
No entanto, você conseguirá completar as tarefas práticas mesmo se nunca programou em Python e/ou JavaScript. O motivo é que o nosso roteiro já inclui os trechos de código que devem ser copiados para o sistema.
Para facilitar a execução e entendimento do sistema, também não usamos bancos de dados ou serviços externos.
Nessa adaptação, a comunicação entre o front-end e o back-end é baseada em uma API REST, que é uma abordagem comum em sistemas web.
O protocolo utilizado para essa comunicação é HTTP/HTTPS, garantindo uma troca de informações eficiente e segura.
Optamos por usar HTTP/HTTPS no back-end para a comunicação entre os serviços, pois, além de ser amplamente suportado, é uma solução consolidada e simples para o desenvolvimento de APIs REST. A comunicação entre os microsserviços acontece por meio de chamadas HTTP, em que as requisições são feitas com métodos como GET
, POST
, PUT
e DELETE
, e os dados são geralmente trocados no formato JSON.
A escolha por HTTP/HTTPS para a implementação da API REST se deu pela simplicidade e facilidade de integração entre os microsserviços, mantendo a eficiência na comunicação e a compatibilidade com diversas ferramentas e plataformas. Embora gRPC ofereça benefícios como maior desempenho em alguns cenários, o uso de HTTP/HTTPS é mais que suficiente para as necessidades da nossa aplicação.
A seguir vamos descrever a sequência de passos para você executar o sistema localmente em sua máquina. Ou seja, todos os microsserviços estarão rodando na sua máquina.
IMPORTANTE: Você deve seguir esses passos antes de implementar as tarefas práticas descritas nas próximas seções.
-
Faça um fork do repositório. Para isso, basta clicar no botão Fork no canto superior direito desta página.
-
Vá para o terminal do seu sistema operacional e clone o projeto (lembre-se de incluir o seu usuário GitHub na URL antes de executar)
git clone https://github.com/<SEU_USUÁRIO>/micro-livraria.git
-
É também necessário ter o
Python
e opip
instalado na sua máquina. Se você não tem, siga as instruções para instalação contidas nessa página para instalar o python e nessa página para instalar o pip, que eh um gerenciador de pacotes python. -
Em um terminal, vá para o diretório no qual o projeto foi clonado e instale as dependências necessárias para execução dos microsserviços:
cd micro-livraria python -m venv venv source ven/bin/activate pip install -r requirements.txt
-
Inicie os microsserviços através dos comandos abaixo:
-
Rodando o microserviço de shipping
nohup python services/shipping_service/manage.py runserver &> iventory.log &
-
Rodando o microserviço de inventory
nohup python services/inventory_service/manage.py runserver 8001 &> service.log &
-
Rodando o microserviço do frontend
nohup python -m http.server 5000 --directory services/frontend &> frontend.log &
-
-
Para fins de teste, efetue uma requisição para o microsserviço reponsável pela API do backend.
-
Se tiver o
curl
instalado na sua máquina, basta usar:curl -i -X GET http://localhost:8001/api/products
-
Caso contrário, você pode fazer uma requisição acessando, no seu navegador, a seguinte URL:
http://localhost:8001/api/products
.
- Teste agora o sistema como um todo, abrindo o front-end em um navegador: http://localhost:5000. Faça então um teste das principais funcionalidades da livraria.
Nesta primeira tarefa, você irá implementar uma nova operação no serviço Inventory
. A operação que vamos implementar é chamada search_product_by_id
e vai permitir que um cliente obtenha os detalhes de um produto específico, baseado no seu ID.
A tarefa está dividida em dois passos principais: definir a operação e configurá-la para ser acessível via uma nova rota.
Para começar, você deve criar uma função que lidere a busca de um produto pelo seu ID no arquivo views.py
. Vamos carregar os dados de um arquivo JSON, que nesse caso simula o nosso banco de dados, e retornar o produto correspondente ao ID informado. Caso o produto não seja encontrado, retornaremos uma mensagem de erro.
-
Abra o arquivo
services/inventory_service/inventory/views.py
: -
Adicione a seguinte função no arquivo, logo após a função já existente de
search_all_products
:# Função para buscar um produto pelo ID @api_view(['GET']) def search_product_by_id(request, id): # Obter o diretório atual e encontrar o caminho para o arquivo products.json current_dir = os.path.dirname(__file__) file_path = os.path.join(current_dir, '..', 'products.json') # Abrir o arquivo products.json e carregar os dados with open(file_path, 'r') as file: data = json.load(file) # Procurar o produto com o ID fornecido product = next((item for item in data if item["id"] == int(id)), None) # Se o produto for encontrado, retorná-lo if product: return Response(product) # Caso contrário, retornar uma mensagem de erro com status 404 else: return Response({"error": "Product not found"}, status=404)
-
Explicando o código:
@api_view(['GET']):
Esse decorador define que a função responderá a requisições HTTP GET.os.path.dirname(__file__)
eos.path.join(...):
Estamos localizando o caminho correto do arquivoproducts.json
.json.load(file):
Lê o conteúdo do arquivo JSON e converte para uma lista de dicionários.next((item for item in data if item["id"] == int(id)), None):
Este trecho percorre os itens do JSON e retorna o produto cujo id corresponde ao parâmetro fornecido. Se não encontrar, retornaNone
.- Resposta HTTP: Caso o produto seja encontrado, retornamos seus dados com
Response(product)
. Se não for encontrado, retornamos um erro404
com uma mensagem `"Product not found".
Agora que a função search_product_by_id
foi definida, precisamos garantir que ela seja acessível a partir de uma URL específica. Para isso, vamos adicionar uma rota no arquivo urls.py
que vai mapear a URL solicitada para a função.
-
Abra o arquivo
services/inventory_service/inventory/urls.py
: -
Adicione a seguinte rota para o novo endpoint:
from django.urls import path from .views import search_all_products, search_product_by_id # Definindo as URLs que mapeiam para as funções de visualização urlpatterns = [ # Rota para buscar todos os produtos path('products/', search_all_products, name='search_all_products'), # Rota para buscar um produto específico pelo ID path('products/<int:id>/', search_product_by_id, name='search_product_by_id'), ]
-
Explicando o Código:
path('products/', search_all_products):
Essa rota já existente mapeia a URLproducts/
para a função que retorna todos os produtos.path('product/<int:id>/', search_product_by_id):
A nova rota define que ao acessar uma URL do tipo/product/1/
, a funçãosearch_product_by_id
será chamada, com o valor 1 sendo passado como o parâmetro id.- Neste exemplo,
id
é um número inteiro, como especificado pelo<int:id>
na URL. O Django automaticamente valida o tipo de dado e repassa esse valor para a função.
Finalize, efetuando uma chamada no novo endpoint da API: http://localhost:8001/products/1
Para ficar claro: até aqui, apenas implementamos a nova operação no backend. A sua incorporação no frontend ficará pendente, pois requer mudar a interface Web, para, por exemplo, incluir um botão "Pesquisar Livro".
IMPORTANTE: Se tudo funcionou corretamente, dê um COMMIT & PUSH (e certifique-se de que seu repositório no GitHub foi atualizado).
git add --all
git commit -m "Tarefa prática #1 - Microservices"
git push origin main
Nesta segunda tarefa, você irá criar um container Docker para o seu microserviço. Os containers são importantes para isolar e distribuir os microserviços em ambientes de produção. Em outras palavras, uma vez "copiado" para um container, um microsserviço pode ser executado em qualquer ambiente, seja ele sua máquina local, o servidor de sua universidade, ou um sistema de cloud (como Amazon AWS, Google Cloud, etc).
Como nosso primeiro objetivo é didático, iremos criar apenas uma imagem Docker para exemplificar o uso de containers.
Caso você não tenha o Docker instaldo em sua máquina, é preciso instalá-lo antes de iniciar a tarefa. Um passo-a-passo de instalação pode ser encontrado na documentação oficial.
Crie um arquivo chamado Dockerfile
dentro da raiz da pasta do micro servico shipping_service
com o comando:
touch services/shipping_service/Dockerfile
copie o requirements.txt
para dentro do shipping_service
com o comando cp
:
cp requirements.txt services/shipping_service/requirements.txt
Como ilustrado na próxima figura, o Dockerfile é utilizado para gerar uma imagem. A partir dessa imagem, você pode criar várias instâncias de uma aplicação. Com isso, conseguimos escalar o microsserviço de Shipping
de forma horizontal.
No Dockerfile, você precisa incluir cinco instruções
FROM
: tecnologia que será a base de criação da imagem.WORKDIR
: diretório da imagem na qual os comandos serão executados.COPY
: comando para copiar o código fonte para a imagem.RUN
: comando para instalação de dependências.EXPOSE
: comando para expor uma porta disponivel no ambiente.CMD
: comando para executar o seu código quando o container for criado.
Ou seja, nosso Dockerfile terá as seguintes linhas:
# Imagem base derivada do Python
FROM python:3.12
# Diretório de trabalho
WORKDIR /app
# Comando para copiar os arquivos para a pasta /app da imagem
COPY . /app
# Comando para instalar as dependências
RUN pip install --no-cache-dir -r requirements.txt
#Comando para expor uma porta disponivel no ambiente.
EXPOSE 8000
# Comando para inicializar (executar) a aplicação
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Agora nós vamos compilar o Dockerfile e criar a imagem. Para isto, execute o seguinte comando em um terminal do seu sistema operacional (esse comando precisa ser executado na raiz do projeto; ele pode também demorar um pouco mais para ser executado).
docker build -t micro-livraria/shipping -f services/shipping_service/Dockerfile ./
onde:
docker build
: comando de compilação do Docker.-t micro-livraria/shipping
: tag de identificação da imagem criada.-f service/shipping_service/Dockerfile
: Caminho ate o dockerfile a ser compilado.
O ./
no final indica que estamos executando os comandos do Dockerfile tendo como referência a raiz do projeto.
Lembra de quando inciamos os micro servicos com o comando abaixo?
nohup python services/shipping_service/manage.py runserver &> iventory.log &
Se executarmos o container que nós construímos, ele terá conflito, pois teremos dois processos do computador tentando acessar a mesma porta. Para isso não ocorrer, precisamos parar o processo executando o comando:
lsof -i :8000
Este comando retornará o processo que usa a porta 8000, tendo uma saída similar a esta:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 12345 user 4u IPv4 123456 0t0 TCP *:8000 (LISTEN)
O PID é o ID do processo. Usamos o comando kill
para encerrar o processo:
kill <PID>
nesse caso ficticio, a resposta do comando retorunou o procesos python de PID (id do processo) igual a 1234
, o comando seria:
kill 12345
Preste MUITA atenção ao usar o comando kill
. Caso você coloque o PID errado, pode acabar encerrando processos muito importantes do seu sistema.
Por fim, para executar a imagem criada no passo anterior (ou seja, colocar novamente o microsserviço de Shipping no ar), basta usar o comando:
docker run -ti --name shipping -p 8000:8000 micro-livraria/shipping
onde:
docker run
: comando de execução de uma imagem docker.-ti
: habilita a interação com o container via terminal.--name shipping
: define o nome do container criado.-p 8000:8000
: redireciona a porta 8000 do container para sua máquina.micro-livraria/shipping
: especifica qual a imagem deve-se executar.
Se tudo estiver correto, você irá receber a seguinte mensagem em seu terminal:
Shipping Service running
E o Controller pode acessar o serviço diretamente através do container Docker.
Mas qual foi exatamente a vantagem de criar esse container? Agora, você pode levá-lo para qualquer máquina ou sistema operacional e colocar o microsserviço para rodar sem instalar mais nada (incluindo bibliotecas, dependências externas, módulos de runtime, etc). Isso vai ocorrer com containers implementados em JavaScript, como no nosso exemplo, mas também com containers implementados em qualquer outra linguagem.
IMPORTANTE: Se tudo funcionou corretamente, dê um COMMIT & PUSH (e certifique-se de que seu repositório no GitHub foi atualizado).
git add --all
git commit -m "Tarefa prática #2 - Docker"
git push origin main
Neste exercício extra, vamos implementar a comunicação entre contêineres Docker utilizando o Docker Compose. Essa ferramenta permite definir e gerenciar múltiplos contêineres em um único arquivo YAML, simplificando a configuração e o gerenciamento de aplicações que dependem de vários serviços. Com o Docker Compose, você pode especificar imagens, volumes, redes e variáveis de ambiente, possibilitando o início de todos os serviços com um único comando.
Se você ainda não tiver o Docker Compose instalado em sua máquina, pode fazê-lo seguindo as instruções na documentação oficial.
Repita a tarefa prática dois
com o microserviço inventory_service
.
Com base na lógica utilizada, tente seguir o mesmo passo a passo implementando cada etapa. Lembre-se de alterar os nomes conforme necessário.
Fique atento à utilização da porta para microserviço inventory_service
. Em vez de usar a porta 8000, utilizaremos a porta 8001
.
no final do processo teremos um Dockerfile seguindo essa estrutura:
# Imagem base derivada do Python
FROM python:3.12
# Diretório de trabalho
WORKDIR /app
# Comando para copiar os arquivos para a pasta /app da imagem
COPY . /app
# Comando para instalar as dependências
RUN pip install --no-cache-dir -r requirements.txt
#Comando para expor uma porta disponivel no ambiente.
EXPOSE 8001
# Comando para inicializar (executar) a aplicação
CMD ["python", "manage.py", "runserver", "0.0.0.0:8001"]
Para criar o Dockerfile do microserviço frontend
, utilizaremos comandos diferentes, pois a execução desse microserviço se dá por meio de um servidor Nginx, uma solução amplamente utilizada para hospedar aplicações web em HTML, JavaScript e CSS.
FROM
: usa a imagem oficial do Nginx como base, especificamente a versão leve (alpine), que é otimizada para ser menor e mais rápida.WORKDIR
: define o diretório de trabalho dentro do container como /usr/share/nginx/html, que é onde o Nginx procura os arquivos estáticos para servir.RUN
: remove os arquivos estáticos padrão que vêm com a imagem do Nginx. Isso é necessário para evitar conflitos e garantir que apenas seus arquivos estáticos sejam usados.COPY
: copia todos os arquivos do diretório atual (onde o Dockerfile está) para o diretório de trabalho definido no container (/usr/share/nginx/html). Aqui, você está transferindo os arquivos HTML, CSS e JavaScript da sua aplicação.EXPOSE
: expõe a porta 5000 para o mundo externo. Essa é a porta que você usará para acessar a aplicação em execução, permitindo que outros serviços ou usuários se conectem.CMD
: inicia o servidor Nginx em primeiro plano (daemon off), o que é essencial para que o container permaneça ativo e escutando requisições. Sem isso, o container encerraria imediatamente após a execução.
# Use a imagem oficial do Nginx como base
FROM nginx:alpine
# Defina o diretório de trabalho como /usr/share/nginx/html
WORKDIR /usr/share/nginx/html
# Remova os arquivos estáticos padrão do nginx
RUN rm -rf ./*
# Copie seus arquivos estáticos do site para dentro do contêiner
COPY . .
# Exponha a porta 80 para o mundo exterior
EXPOSE 5000
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]
Ao final, teremos os três Dockerfiles, cada um na raiz de seu respectivo diretório. Para subir todos os contêineres em conjunto, usaremos o Docker Compose. Para isso, criaremos o arquivo docker-compose.yml
na raiz do projeto.
touch docker-compose.yml
No docker-compose.yml, você precisa incluir várias chaves e suas respectivas configurações:
services
: define os serviços que compõem sua aplicação, permitindo agrupar diferentes componentes, como frontend e backend, em containers separados.build
: especifica como construir a imagem Docker para um serviço, contendo configurações para o contexto e o arquivo Dockerfile.context
: determina o diretório que contém o Dockerfile e os arquivos necessários para a construção da imagem, devendo ser relativo ao local do arquivo docker-compose.yml.dockerfile
: indica o nome do arquivo Dockerfile a ser utilizado na construção da imagem; por padrão, o Docker procura um arquivo chamado Dockerfile, mas você pode definir um nome diferente.ports
: mapeia as portas do host para as portas do container, permitindo acesso aos serviços, no formato <porta_host>:<porta_container>.command
: define o comando a ser executado quando o container é iniciado, personalizando o que acontece ao inicializar o serviço, como iniciar um servidor ou executar um script.
Teremos um Dockerfile seguindo essa estrutura:
version: '3.8' # Define a versão do Docker Compose a ser utilizada
services: # Início da definição dos serviços
inventory_service: # Serviço de inventário
build: # Configurações para construir a imagem
context: ./services/inventory_service # Diretório onde está o Dockerfile
dockerfile: Dockerfile # Nome do arquivo Dockerfile
ports: # Configurações de portas
- "8001:8001" # Mapeia a porta 8001 do host para a porta 8001 do container
command: python manage.py runserver 0.0.0.0:8001 # Comando para iniciar o serviço
shipping_service: # Serviço de envio
build: # Configurações para construir a imagem
context: ./services/shipping_service # Diretório onde está o Dockerfile
dockerfile: Dockerfile # Nome do arquivo Dockerfile
ports: # Configurações de portas
- "8000:8000" # Mapeia a porta 8000 do host para a porta 8000 do container
command: python manage.py runserver 0.0.0.0:8000 # Comando para iniciar o serviço
frontend: # Serviço frontend
build: # Configurações para construir a imagem
context: ./services/frontend # Diretório onde está o Dockerfile
dockerfile: Dockerfile # Nome do arquivo Dockerfile
ports: # Configurações de portas
- "5000:80" # Mapeia a porta 5000 do host para a porta 80 do container
O projeto estará com esta estrutura:
micro-livraria
│
├── docker-compose.yml
├── LICENSE
├── README.md
└── services
├── frontend
│ ├── Dockerfile
│ ├── img
│ │ ├── design.png
│ │ ├── esm.png
│ │ └── refactoring.png
│ ├── index.css
│ ├── index.html
│ └── index.js
├── inventory_service
│ ├── Dockerfile
│ ├── inventory
│ │ ├── init.py
│ │ ├── pycache
│ │ │ └── arquivos de compilação
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── inventory_service
│ │ ├── init.py
│ │ ├── settings.py
│ │ └── urls.py
│ ├── manage.py
│ ├── products.json
│ └── requirements.txt
└── shipping_service
├── Dockerfile
├── manage.py
├── requirements.txt
├── shipping
│ ├── init.py
│ ├── pycache
│ │ └──
│ ├── tests.py
│ ├── urls.py
│ └── views.py
└── shipping_service
├── init.py
├── pycache
│ └── arquivos de compilação
├── settings.py
└── urls.py
12 directories, 48 files
verifique se a estrura esta a mesma do seu projeto, o comando tree
pode te ajudar nisso! (para instalar rode o comando sudo apt install tree
e depois rode tree
)
Verifique se a estrutura está a mesma do seu projeto. O comando tree
pode te ajudar nisso!
(Para instalá-lo, rode o comando sudo apt install tree
e, em seguida, execute tree
.)
Agora, comite as novas mudanças feitas seguindo os comandos que você já conhece!
Nesta aula, trabalhamos em uma aplicação baseada em microsserviços. Apesar de pequena, ela ilustra os princípios básicos de microsserviços, bem como algumas tecnologias importantes quando se implementa esse tipo de arquitetura.
No entanto, é importante ressaltar que em uma aplicação real existem outros componentes, como bancos de dados, balanceadores de carga e orquestradores.
A função de um balanceador de carga é dividir as requisições quando temos mais de uma instância do mesmo microsserviço. Imagine que o microsserviço de frete da loja virtual ficou sobrecarregado e, então, tivemos que colocar para rodar múltiplas instâncias do mesmo. Nesse caso, precisamos de um balanceador para dividir as requisições que chegam entre essas instâncias.
Já um orquestrador gerencia o ciclo de vida de containers. Por exemplo, se um servidor para de funcionar, ele automaticamente move os seus containers para um outro servidor. Se o número de acessos ao sistema aumenta bruscamente, um orquestrador também aumenta, em seguida, o número de containers. Kubernetes é um dos orquestradores mais usados atualmente.
Se quiser estudar um segundo sistema de demonstração de microsserviços, sugerimos este repositório, mantido pelo serviço de nuvem do Google.
Este exercício prático, incluindo o seu código, foi elaborado por Rodrigo Brito, aluno de mestrado do DCC/UFMG, como parte das suas atividades na disciplina Estágio em Docência, cursada em 2020/2, sob orientação do Prof. Marco Tulio Valente.
O código deste repositório possui uma licença MIT. O roteiro descrito acima possui uma licença CC-BY.