├── services ├── frontend │ ├── img │ │ ├── esm.png │ │ ├── design.png │ │ └── refactoring.png │ ├── index.css │ ├── index.html │ └── index.js ├── controller │ ├── shipping.js │ ├── inventory.js │ └── index.js ├── inventory │ ├── products.json │ └── index.js └── shipping │ └── index.js ├── .prettierrc ├── cypress ├── Dockerfile └── readme.md ├── .editorconfig ├── proto ├── shipping.proto └── inventory.proto ├── LICENSE ├── package.json ├── .gitignore └── README.md /services/frontend/img/esm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aserg-ufmg/micro-livraria/HEAD/services/frontend/img/esm.png -------------------------------------------------------------------------------- /services/frontend/img/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aserg-ufmg/micro-livraria/HEAD/services/frontend/img/design.png -------------------------------------------------------------------------------- /services/frontend/img/refactoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aserg-ufmg/micro-livraria/HEAD/services/frontend/img/refactoring.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /cypress/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | ENV NO_UPDATE_NOTIFIER=1 4 | 5 | EXPOSE 3000 6 | EXPOSE 5000 7 | 8 | WORKDIR /app 9 | 10 | ADD . . 11 | RUN npm install --silent --loglevel=error 12 | 13 | CMD ["npm", "run", "exec"] 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /proto/shipping.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service ShippingService { 4 | rpc GetShippingRate(ShippingPayload) returns (ShippingResponse) {} 5 | } 6 | 7 | message ShippingPayload { 8 | string cep = 1; 9 | } 10 | 11 | message ShippingResponse { 12 | float value = 1; 13 | } 14 | -------------------------------------------------------------------------------- /cypress/readme.md: -------------------------------------------------------------------------------- 1 | # Imagem para testes com Cypress 2 | 3 | Para gerar uma nova imagem, basta rodar o seguinte comando na raiz do projeto: 4 | 5 | ```bash 6 | docker build -t micro-livraria -f cypress/Dockerfile . 7 | ``` 8 | 9 | Para executar a aplicação completa via Docker 10 | ```bash 11 | docker run -ti -p 3000:3000 -p 5000:5000 micro-livraria 12 | ``` -------------------------------------------------------------------------------- /proto/inventory.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service InventoryService { 4 | rpc SearchAllProducts(Empty) returns (ProductsResponse) {} 5 | } 6 | 7 | message Empty{} 8 | 9 | message ProductResponse { 10 | int32 id = 1; 11 | string name = 2; 12 | int32 quantity = 3; 13 | float price = 4; 14 | string photo = 5; 15 | string author = 6; 16 | } 17 | 18 | message ProductsResponse { 19 | repeated ProductResponse products = 1; 20 | } 21 | -------------------------------------------------------------------------------- /services/controller/shipping.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const protoLoader = require('@grpc/proto-loader'); 3 | 4 | const packageDefinition = protoLoader.loadSync('proto/shipping.proto', { 5 | keepCase: true, 6 | longs: String, 7 | enums: String, 8 | arrays: true, 9 | }); 10 | 11 | const ShippingService = grpc.loadPackageDefinition(packageDefinition).ShippingService; 12 | const client = new ShippingService('127.0.0.1:3001', grpc.credentials.createInsecure()); 13 | 14 | module.exports = client; 15 | -------------------------------------------------------------------------------- /services/controller/inventory.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const protoLoader = require('@grpc/proto-loader'); 3 | 4 | const packageDefinition = protoLoader.loadSync('proto/inventory.proto', { 5 | keepCase: true, 6 | longs: String, 7 | enums: String, 8 | arrays: true, 9 | }); 10 | 11 | const InventoryService = grpc.loadPackageDefinition(packageDefinition).InventoryService; 12 | const client = new InventoryService('127.0.0.1:3002', grpc.credentials.createInsecure()); 13 | 14 | module.exports = client; 15 | -------------------------------------------------------------------------------- /services/inventory/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Refactoring", 5 | "author": "Martin Fowler", 6 | "quantity": 10, 7 | "price": 79.92, 8 | "photo": "/img/refactoring.png" 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Engenharia De Software Moderna", 13 | "author": "Marco Tulio Valente", 14 | "quantity": 10, 15 | "price": 65.9, 16 | "photo": "/img/esm.png" 17 | }, 18 | { 19 | "id": 3, 20 | "name": "Design Patterns", 21 | "author": "Erich Gamma, John Vlissides, Richard Helm, Ralph Johnson", 22 | "quantity": 10, 23 | "price": 67.99, 24 | "photo": "/img/design.png" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /services/inventory/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const protoLoader = require('@grpc/proto-loader'); 3 | const products = require('./products.json'); 4 | 5 | const packageDefinition = protoLoader.loadSync('proto/inventory.proto', { 6 | keepCase: true, 7 | longs: String, 8 | enums: String, 9 | arrays: true, 10 | }); 11 | 12 | const inventoryProto = grpc.loadPackageDefinition(packageDefinition); 13 | 14 | const server = new grpc.Server(); 15 | 16 | // implementa os métodos do InventoryService 17 | server.addService(inventoryProto.InventoryService.service, { 18 | searchAllProducts: (_, callback) => { 19 | callback(null, { 20 | products: products, 21 | }); 22 | }, 23 | }); 24 | 25 | server.bindAsync('127.0.0.1:3002', grpc.ServerCredentials.createInsecure(), () => { 26 | console.log('Inventory Service running at http://127.0.0.1:3002'); 27 | server.start(); 28 | }); 29 | -------------------------------------------------------------------------------- /services/shipping/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const protoLoader = require('@grpc/proto-loader'); 3 | 4 | const packageDefinition = protoLoader.loadSync('proto/shipping.proto', { 5 | keepCase: true, 6 | longs: String, 7 | enums: String, 8 | arrays: true, 9 | }); 10 | 11 | const shippingProto = grpc.loadPackageDefinition(packageDefinition); 12 | 13 | const server = new grpc.Server(); 14 | 15 | // implementa os métodos do ShippingService 16 | server.addService(shippingProto.ShippingService.service, { 17 | GetShippingRate: (_, callback) => { 18 | const shippingValue = Math.random() * 100 + 1; // Random value from R$1 to R$100 19 | 20 | callback(null, { 21 | value: shippingValue, 22 | }); 23 | }, 24 | }); 25 | 26 | server.bindAsync('0.0.0.0:3001', grpc.ServerCredentials.createInsecure(), () => { 27 | console.log('Shipping Service running at http://127.0.0.1:3001'); 28 | server.start(); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Applied Software Engineering Research Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/controller/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const shipping = require('./shipping'); 3 | const inventory = require('./inventory'); 4 | const cors = require('cors'); 5 | 6 | const app = express(); 7 | app.use(cors()); 8 | 9 | /** 10 | * Retorna a lista de produtos da loja via InventoryService 11 | */ 12 | app.get('/products', (req, res, next) => { 13 | inventory.SearchAllProducts(null, (err, data) => { 14 | if (err) { 15 | console.error(err); 16 | res.status(500).send({ error: 'something failed :(' }); 17 | } else { 18 | res.json(data.products); 19 | } 20 | }); 21 | }); 22 | 23 | /** 24 | * Consulta o frete de envio no ShippingService 25 | */ 26 | app.get('/shipping/:cep', (req, res, next) => { 27 | shipping.GetShippingRate( 28 | { 29 | cep: req.params.cep, 30 | }, 31 | (err, data) => { 32 | if (err) { 33 | console.error(err); 34 | res.status(500).send({ error: 'something failed :(' }); 35 | } else { 36 | res.json({ 37 | cep: req.params.cep, 38 | value: data.value, 39 | }); 40 | } 41 | } 42 | ); 43 | }); 44 | 45 | /** 46 | * Inicia o router 47 | */ 48 | app.listen(3000, () => { 49 | console.log('Controller Service running on http://127.0.0.1:3000'); 50 | }); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-livraria", 3 | "version": "1.0.0", 4 | "description": "Toy example of microservice", 5 | "main": "", 6 | "scripts": { 7 | "start": "run-p start-frontend start-controller start-shipping start-inventory", 8 | "start-controller": "nodemon services/controller/index.js", 9 | "start-shipping": "nodemon services/shipping/index.js", 10 | "start-inventory": "nodemon services/inventory/index.js", 11 | "start-frontend": "serve -p 5000 services/frontend", 12 | "exec": "run-p start-frontend exec-controller exec-shipping exec-inventory", 13 | "exec-controller": "node services/controller/index.js", 14 | "exec-shipping": "node services/shipping/index.js", 15 | "exec-inventory": "node services/inventory/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/aserg-ufmg/micro-livraria.git" 20 | }, 21 | "author": "Rodrigo", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/aserg-ufmg/micro-livraria/issues" 25 | }, 26 | "homepage": "https://github.com/aserg-ufmg/micro-livraria#readme", 27 | "dependencies": { 28 | "@grpc/grpc-js": "^1.5.2", 29 | "@grpc/proto-loader": "^0.6.9", 30 | "cors": "^2.8.5", 31 | "express": "^4.17.1", 32 | "google-protobuf": "^3.19.3" 33 | }, 34 | "devDependencies": { 35 | "serve": "^13.0.2", 36 | "nodemon": "^2.0.7", 37 | "npm-run-all": "^4.1.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /services/frontend/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background: #eff3f4; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', 5 | sans-serif; 6 | } 7 | 8 | .hero-body .container { 9 | max-width: 700px; 10 | } 11 | 12 | .hero-body .title { 13 | color: hsl(192, 17%, 99%) !important; 14 | } 15 | 16 | .hero-body .subtitle { 17 | color: hsl(192, 17%, 99%) !important; 18 | padding-top: 2rem; 19 | line-height: 1.5; 20 | } 21 | 22 | .features { 23 | padding: 5rem 0; 24 | } 25 | 26 | .box.cta { 27 | border-radius: 0; 28 | border-left: none; 29 | border-right: none; 30 | } 31 | 32 | .card-image > .fa { 33 | font-size: 8rem; 34 | padding-top: 2rem; 35 | padding-bottom: 2rem; 36 | color: #209cee; 37 | } 38 | 39 | .card-content .content { 40 | font-size: 14px; 41 | margin: 1rem 1rem; 42 | } 43 | 44 | .card-content .content h4 { 45 | font-size: 16px; 46 | font-weight: 700; 47 | } 48 | 49 | .card { 50 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.18); 51 | margin-bottom: 2rem; 52 | } 53 | 54 | .is-shady { 55 | animation: flyintoright 0.4s backwards; 56 | background: #fff; 57 | box-shadow: rgba(0, 0, 0, 0.1) 0 1px 0; 58 | border-radius: 4px; 59 | display: inline-block; 60 | margin: 10px; 61 | position: relative; 62 | transition: all 0.2s ease-in-out; 63 | width: 100%; 64 | } 65 | 66 | .is-shady:hover { 67 | box-shadow: 0 10px 16px rgba(0, 0, 0, 0.13), 0 6px 6px rgba(0, 0, 0, 0.19); 68 | } 69 | 70 | @media (min-width: 800px) { 71 | .book-meta { 72 | min-height: 250px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Micro Livraria 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

Micro Livraria

18 |
19 |
20 |
21 |
22 |

Uma simples livraria utilizando microservices

23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /services/frontend/index.js: -------------------------------------------------------------------------------- 1 | function newBook(book) { 2 | const div = document.createElement('div'); 3 | div.className = 'column is-4'; 4 | div.innerHTML = ` 5 |
6 |
7 |
8 | ${book.name} 13 |
14 |
15 |
16 |
17 |
18 |

R$${book.price.toFixed(2)}

19 |

Disponível em estoque: 5

20 |

${book.name}

21 |

${book.author}

22 |
23 |
24 |
25 | 26 |
27 | 30 |
31 | 32 |
33 |
34 |
`; 35 | return div; 36 | } 37 | 38 | function calculateShipping(id, cep) { 39 | fetch('http://localhost:3000/shipping/' + cep) 40 | .then((data) => { 41 | if (data.ok) { 42 | return data.json(); 43 | } 44 | throw data.statusText; 45 | }) 46 | .then((data) => { 47 | swal('Frete', `O frete é: R$${data.value.toFixed(2)}`, 'success'); 48 | }) 49 | .catch((err) => { 50 | swal('Erro', 'Erro ao consultar frete', 'error'); 51 | console.error(err); 52 | }); 53 | } 54 | 55 | document.addEventListener('DOMContentLoaded', function () { 56 | const books = document.querySelector('.books'); 57 | 58 | fetch('http://localhost:3000/products') 59 | .then((data) => { 60 | if (data.ok) { 61 | return data.json(); 62 | } 63 | throw data.statusText; 64 | }) 65 | .then((data) => { 66 | if (data) { 67 | data.forEach((book) => { 68 | books.appendChild(newBook(book)); 69 | }); 70 | 71 | document.querySelectorAll('.button-shipping').forEach((btn) => { 72 | btn.addEventListener('click', (e) => { 73 | const id = e.target.getAttribute('data-id'); 74 | const cep = document.querySelector(`.book[data-id="${id}"] input`).value; 75 | calculateShipping(id, cep); 76 | }); 77 | }); 78 | 79 | document.querySelectorAll('.button-buy').forEach((btn) => { 80 | btn.addEventListener('click', (e) => { 81 | swal('Compra de livro', 'Sua compra foi realizada com sucesso', 'success'); 82 | }); 83 | }); 84 | } 85 | }) 86 | .catch((err) => { 87 | swal('Erro', 'Erro ao listar os produtos', 'error'); 88 | console.error(err); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micro-Livraria: Exemplo Prático de Microsserviços 2 | 3 | Este repositório contem um exemplo simples de uma livraria virtual construída usando uma **arquitetura de microsserviços**. 4 | 5 | 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](https://engsoftmoderna.info/cap7.html) do livro [Engenharia de Software Moderna](https://engsoftmoderna.info). 6 | 7 | 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 **Node.js**, **REST**, **gRPC** e **Docker**. 8 | 9 | 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. 10 | 11 |

12 | 13 |

14 | 15 | No restante deste documento vamos: 16 | 17 | - Descrever o sistema, com foco na sua arquitetura. 18 | - Apresentar instruções para sua execução local, usando o código disponibilizado no repositório. 19 | - Descrever duas tarefas práticas para serem realizadas pelos alunos, as quais envolvem: 20 | - Tarefa Prática #1: Implementação de uma nova operação em um dos microsserviços 21 | - Tarefa Prática #2: Criação de containers Docker para facilitar a execução dos microsserviços. 22 | 23 | ## Arquitetura 24 | 25 | A micro-livraria possui quatro microsserviços: 26 | 27 | - Front-end: microsserviço responsável pela interface com usuário, conforme mostrado na figura anterior. 28 | - Controller: microsserviço responsável por intermediar a comunicação entre o front-end e o backend do sistema. 29 | - Shipping: microserviço para cálculo de frete. 30 | - Inventory: microserviço para controle do estoque da livraria. 31 | 32 | Os quatro microsserviços estão implementados em **JavaScript**, usando o Node.js para execução dos serviços no back-end. 33 | 34 | No entanto, **você conseguirá completar as tarefas práticas mesmo se nunca programou em JavaScript**. O motivo é que o nosso roteiro já inclui os trechos de código que devem ser copiados para o sistema. 35 | 36 | Para facilitar a execução e entendimento do sistema, também não usamos bancos de dados ou serviços externos. 37 | 38 | ## Protocolos de Comunicação 39 | 40 | Como ilustrado no diagrama a seguir, a comunicação entre o front-end e o backend usa uma **API REST**, como é comum no caso de sistemas Web. 41 | 42 | Já a comunicação entre o Controller e os microsserviços do back-end é baseada em [gRPC](https://grpc.io/). 43 | 44 |

45 | 46 |

47 | 48 | Optamos por usar gRPC no backend porque ele possui um desempenho melhor do que REST. Especificamente, gRPC é baseado no conceito de **Chamada Remota de Procedimentos (RPC)**. A ideia é simples: em aplicações distribuídas que usam gRPC, um cliente pode chamar funções implementadas em outros processos de forma transparente, isto é, como se tais funções fossem locais. Em outras palavras, chamadas gRPC tem a mesma sintaxe de chamadas normais de função. 49 | 50 | Para viabilizar essa transparência, gRPC usa dois conceitos centrais: 51 | 52 | - uma linguagem para definição de interfaces 53 | - um protocolo para troca de mensagens entre aplicações clientes e servidoras. 54 | 55 | Especificamente, no caso de gRPC, a implementação desses dois conceitos ganhou o nome de **Protocol Buffer**. Ou seja, podemos dizer que: 56 | 57 | > Protocol Buffer = linguagem para definição de interfaces + protocolo para definição das mensagens trocadas entre aplicações clientes e servidoras 58 | 59 | ### Exemplo de Arquivo .proto 60 | 61 | Quando trabalhamos com gRPC, cada microserviço possui um arquivo `.proto` que define a assinatura das operações que ele disponibiliza para os outros microsserviços. 62 | Neste mesmo arquivo, declaramos também os tipos dos parâmetros de entrada e saída dessas operações. 63 | 64 | O exemplo a seguir mostra o arquivo [.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/shipping.proto) do nosso microsserviço de frete. Nele, definimos que esse microsserviço disponibiliza uma função `GetShippingRate`. Para chamar essa função devemos passar como parâmetro de entrada um objeto contendo o CEP (`ShippingPayLoad`). Após sua execução, a função retorna como resultado um outro objeto (`ShippingResponse`) com o valor do frete. 65 | 66 |

67 | 68 |

69 | 70 | Em gRPC, as mensagens (exemplo: `Shippingload`) são formadas por um conjunto de campos, tal como em um `struct` da linguagem C, por exemplo. Todo campo possui um nome (exemplo: `cep`) e um tipo (exemplo: `string`). Além disso, todo campo tem um número inteiro que funciona como um identificador único para o mesmo na mensagem (exemplo: ` = 1`). Esse número é usado pela implementação de gRPC para identificar o campo no formato binário de dados usado por gRPC para comunicação distribuída. 71 | 72 | Arquivos .proto são usados para gerar **stubs**, que nada mais são do que proxies que encapsulam os detalhes de comunicação em rede, incluindo troca de mensagens, protocolos, etc. Mais detalhes sobre o padrão de projeto Proxy podem ser obtidos no [Capítulo 6](https://engsoftmoderna.info/cap6.html). 73 | 74 | Em linguagens estáticas, normalmente precisa-se chamar um compilador para gerar o código de tais stubs. No caso de JavaScript, no entanto, esse passo não é necessário, pois os stubs são gerados de forma transparente, em tempo de execução. 75 | 76 | ## Executando o Sistema 77 | 78 | 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. 79 | 80 | **IMPORTANTE:** Você deve seguir esses passos antes de implementar as tarefas práticas descritas nas próximas seções. 81 | 82 | 1. Faça um fork do repositório. Para isso, basta clicar no botão **Fork** no canto superior direito desta página. 83 | 84 | 2. 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) 85 | 86 | ``` 87 | git clone https://github.com//micro-livraria.git 88 | ``` 89 | 90 | 3. É também necessário ter o Node.js instalado na sua máquina. Se você não tem, siga as instruções para instalação contidas nessa [página](https://nodejs.org/en/download/). 91 | 92 | 4. 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: 93 | 94 | ``` 95 | cd micro-livraria 96 | npm install 97 | ``` 98 | 99 | 5. Inicie os microsserviços através do comando: 100 | 101 | ``` 102 | npm run start 103 | ``` 104 | 105 | 6. Para fins de teste, efetue uma requisição para o microsserviço responsável pela API do backend. 106 | 107 | - Se tiver o `curl` instalado na sua máquina, basta usar: 108 | 109 | ``` 110 | curl -i -X GET http://localhost:3000/products 111 | ``` 112 | 113 | - Caso contrário, você pode fazer uma requisição acessando, no seu navegador, a seguinte URL: `http://localhost:3000/products`. 114 | 115 | 7. 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. 116 | 117 | ## Tarefa Prática #1: Implementando uma Nova Operação 118 | 119 | Nesta primeira tarefa, você irá implementar uma nova operação no serviço `Inventory`. Essa operação, chamada `SearchProductByID` vai pesquisar por um produto, dado o seu ID. 120 | 121 | Como descrito anteriormente, as assinaturas das operações de cada microsserviço são definidas em um arquivo `.proto`, no caso [proto/inventory.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/inventory.proto). 122 | 123 | #### Passo 1 124 | 125 | Primeiro, você deve declarar a assinatura da nova operação. Para isso, inclua a definição dessa assinatura no referido arquivo `.proto` (na linha logo após a assinatura da função `SearchAllProducts`): 126 | 127 | ```proto 128 | service InventoryService { 129 | rpc SearchAllProducts(Empty) returns (ProductsResponse) {} 130 | rpc SearchProductByID(Payload) returns (ProductResponse) {} 131 | } 132 | ``` 133 | 134 | Em outras palavras, você está definindo que o microsserviço `Inventory` vai responder a uma nova requisição, chamada `SearchProductByID`, que tem como parâmetro de entrada um objeto do tipo `Payload` e como parâmetro de saída um objeto do tipo `ProductResponse`. 135 | 136 | #### Passo 2 137 | 138 | Inclua também no mesmo arquivo a declaração do tipo do objeto `Payload`, o qual apenas contém o ID do produto a ser pesquisado. 139 | 140 | ```proto 141 | message Payload { 142 | int32 id = 1; 143 | } 144 | ``` 145 | 146 | Veja que `ProductResponse` -- isto é, o tipo de retorno da operação -- já está declarado mais abaixo no arquivo `proto`: 147 | 148 | ```proto 149 | message ProductsResponse { 150 | repeated ProductResponse products = 1; 151 | } 152 | ``` 153 | 154 | Ou seja, a resposta da nossa requisição conterá um único campo, do tipo `ProductResponse`, que também já está implementando no mesmo arquivo: 155 | 156 | ```proto 157 | message ProductResponse { 158 | int32 id = 1; 159 | string name = 2; 160 | int32 quantity = 3; 161 | float price = 4; 162 | string photo = 5; 163 | string author = 6; 164 | } 165 | ``` 166 | 167 | #### Passo 3 168 | 169 | Agora você deve implementar a função `SearchProductByID` no arquivo [services/inventory/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/index.js). 170 | 171 | Reforçando, no passo anterior, apenas declaramos a assinatura dessa função. Então, agora, vamos prover uma implementação para ela. 172 | 173 | Para isso, você precisa implementar a função requerida pelo segundo parâmetro da função `server.addService`, localizada na linha 17 do arquivo [services/inventory/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/index.js). 174 | 175 | De forma semelhante à função `SearchAllProducts`, que já está implementada, você deve adicionar o corpo da função `SearchProductByID` com a lógica de pesquisa de produtos por ID. Este código deve ser adicionado logo após o `SearchAllProducts` na linha 23. 176 | 177 | ```js 178 | SearchProductByID: (payload, callback) => { 179 | callback( 180 | null, 181 | products.find((product) => product.id == payload.request.id) 182 | ); 183 | }, 184 | ``` 185 | 186 | A função acima usa o método `find` para pesquisar em `products` pelo ID de produto fornecido. Veja que: 187 | 188 | - `payload` é o parâmetro de entrada do nosso serviço, conforme definido antes no arquivo .proto (passo 2). Ele armazena o ID do produto que queremos pesquisar. Para acessar esse ID basta escrever `payload.request.id`. 189 | 190 | - `product` é uma unidade de produto a ser pesquisado pela função `find` (nativa de JavaScript). Essa pesquisa é feita em todos os items da lista de produtos até que um primeiro `product` atenda a condição de busca, isto é `product.id == payload.request.id`. 191 | 192 | - [products](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/products.json) é um arquivo JSON que contém a descrição dos livros à venda na livraria. 193 | 194 | - `callback` é uma função que deve ser invocada com dois parâmetros: 195 | - O primeiro parâmetro é um objeto de erro, caso ocorra. No nosso exemplo nenhum erro será retornado, portanto `null`. 196 | - O segundo parâmetro é o resultado da função, no nosso caso um `ProductResponse`, assim como definido no arquivo [proto/inventory.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/inventory.proto). 197 | 198 | #### Passo 4 199 | 200 | Para finalizar, temos que incluir a função `SearchProductByID` em nosso `Controller`. Para isso, você deve incluir uma nova rota `/product/{id}` que receberá o ID do produto como parâmetro. Na definição da rota, você deve também incluir a chamada para o método definido no Passo 3. 201 | 202 | Sendo mais específico, o seguinte trecho de código deve ser adicionado na linha 44 do arquivo [services/controller/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/controller/index.js), logo após a rota `/shipping/:cep`. 203 | 204 | ```js 205 | app.get('/product/:id', (req, res, next) => { 206 | // Chama método do microsserviço. 207 | inventory.SearchProductByID({ id: req.params.id }, (err, product) => { 208 | // Se ocorrer algum erro de comunicação 209 | // com o microsserviço, retorna para o navegador. 210 | if (err) { 211 | console.error(err); 212 | res.status(500).send({ error: 'something failed :(' }); 213 | } else { 214 | // Caso contrário, retorna resultado do 215 | // microsserviço (um arquivo JSON) com os dados 216 | // do produto pesquisado 217 | res.json(product); 218 | } 219 | }); 220 | }); 221 | ``` 222 | 223 | Finalize, efetuando uma chamada no novo endpoint da API: http://localhost:3000/product/1 224 | 225 | 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". 226 | 227 | **IMPORTANTE**: Se tudo funcionou corretamente, dê um **COMMIT & PUSH** (e certifique-se de que seu repositório no GitHub foi atualizado; isso é fundamental para seu trabalho ser devidamente corrigido). 228 | 229 | ```bash 230 | git add --all 231 | git commit -m "Tarefa prática #1 - Microservices" 232 | git push origin main 233 | ``` 234 | 235 | ## Tarefa Prática #2: Criando um Container Docker 236 | 237 | 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). 238 | 239 | Como nosso primeiro objetivo é didático, iremos criar apenas uma imagem Docker para exemplificar o uso de containers. 240 | 241 | Caso você não tenha o Docker instalado 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](https://docs.docker.com/get-docker/). 242 | 243 | #### Passo 1 244 | 245 | Crie um arquivo na raiz do projeto com o nome `shipping.Dockerfile`. Este arquivo armazenará as instruções para criação de uma imagem Docker para o serviço `Shipping`. 246 | 247 | 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. 248 | 249 |

250 | 251 |

252 | 253 | No Dockerfile, você precisa incluir cinco instruções 254 | 255 | - `FROM`: tecnologia que será a base de criação da imagem. 256 | - `WORKDIR`: diretório da imagem na qual os comandos serão executados. 257 | - `COPY`: comando para copiar o código fonte para a imagem. 258 | - `RUN`: comando para instalação de dependências. 259 | - `CMD`: comando para executar o seu código quando o container for criado. 260 | 261 | Ou seja, nosso Dockerfile terá as seguintes linhas: 262 | 263 | ```Dockerfile 264 | # Imagem base derivada do Node 265 | FROM node 266 | 267 | # Diretório de trabalho 268 | WORKDIR /app 269 | 270 | # Comando para copiar os arquivos para a pasta /app da imagem 271 | COPY . /app 272 | 273 | # Comando para instalar as dependências 274 | RUN npm install 275 | 276 | # Comando para inicializar (executar) a aplicação 277 | CMD ["node", "/app/services/shipping/index.js"] 278 | ``` 279 | 280 | #### Passo 2 281 | 282 | 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). 283 | 284 | ``` 285 | docker build -t micro-livraria/shipping -f shipping.Dockerfile ./ 286 | ``` 287 | 288 | onde: 289 | 290 | - `docker build`: comando de compilação do Docker. 291 | - `-t micro-livraria/shipping`: tag de identificação da imagem criada. 292 | - `-f shipping.Dockerfile`: dockerfile a ser compilado. 293 | 294 | O `./` no final indica que estamos executando os comandos do Dockerfile tendo como referência a raiz do projeto. 295 | 296 | #### Passo 3 297 | 298 | Antes de iniciar o serviço via container Docker, precisamos remover a inicialização do serviço de Shipping do comando `npm run start`. Para isso, basta remover o sub-comando `start-shipping` localizado na linha 7 do arquivo [package.json](https://github.com/aserg-ufmg/micro-livraria/blob/main/package.json), conforme mostrado no próximo diff (a linha com o símbolo "-" no início representa a linha original do arquivo; a linha com o símbolo "+" representa como essa linha deve ficar após a sua alteração): 299 | 300 | ```diff 301 | diff --git a/package.json b/package.json 302 | index 25ff65c..552a04e 100644 303 | --- a/package.json 304 | +++ b/package.json 305 | @@ -4,7 +4,7 @@ 306 | "description": "Toy example of microservice", 307 | "main": "", 308 | "scripts": { 309 | - "start": "run-p start-frontend start-controller start-shipping start-inventory", 310 | + "start": "run-p start-frontend start-controller start-inventory", 311 | "start-controller": "nodemon services/controller/index.js", 312 | "start-shipping": "nodemon services/shipping/index.js", 313 | "start-inventory": "nodemon services/inventory/index.js", 314 | 315 | ``` 316 | 317 | Em seguida, você precisa parar o comando antigo (basta usar um CTRL-C no terminal) e rodar o comando `npm run start` para efetuar as mudanças. 318 | 319 | Por fim, para executar a imagem criada no passo anterior (ou seja, colocar de novo o microsserviço de `Shipping` no ar), basta usar o comando: 320 | 321 | ``` 322 | docker run -ti --name shipping -p 3001:3001 micro-livraria/shipping 323 | ``` 324 | 325 | onde: 326 | 327 | - `docker run`: comando de execução de uma imagem docker. 328 | - `-ti`: habilita a interação com o container via terminal. 329 | - `--name shipping`: define o nome do container criado. 330 | - `-p 3001:3001`: redireciona a porta 3001 do container para sua máquina. 331 | - `micro-livraria/shipping`: especifica qual a imagem deve-se executar. 332 | 333 | Se tudo estiver correto, você irá receber a seguinte mensagem em seu terminal: 334 | 335 | ``` 336 | Shipping Service running 337 | ``` 338 | 339 | E o Controller pode acessar o serviço diretamente através do container Docker. 340 | 341 | **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. 342 | 343 | **IMPORTANTE**: Se tudo funcionou corretamente, dê um **COMMIT & PUSH** (e certifique-se de que seu repositório no GitHub foi atualizado; isso é fundamental para seu trabalho ser devidamente corrigido). 344 | 345 | ```bash 346 | git add --all 347 | git commit -m "Tarefa prática #2 - Docker" 348 | git push origin main 349 | ``` 350 | 351 | #### Passo 4 352 | 353 | Como tudo funcionou corretamente, já podemos encerrar o container e limpar nosso ambiente. Para isso, utilizaremos os seguintes comandos: 354 | 355 | ``` 356 | docker stop shipping 357 | ``` 358 | 359 | onde: 360 | 361 | - `docker stop`: comando para interromper a execução de um container. 362 | - `shipping`: nome do container que será interrompido. 363 | 364 | 365 | ``` 366 | docker rm shipping 367 | ``` 368 | 369 | onde: 370 | 371 | - `docker rm`: comando para remover um container. 372 | - `shipping`: nome do container que será removido. 373 | 374 | 375 | ``` 376 | docker rmi micro-livraria/shipping 377 | ``` 378 | 379 | onde: 380 | 381 | - `docker rmi`: comando para remover uma imagem. 382 | - `micro-livraria/shipping`: nome da imagem que será removida. 383 | 384 | ## Comentários Finais 385 | 386 | 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. 387 | 388 | No entanto, é importante ressaltar que em uma aplicação real existem outros componentes, como bancos de dados, balanceadores de carga e orquestradores. 389 | 390 | 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. 391 | 392 | 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](https://kubernetes.io/) é um dos orquestradores mais usados atualmente. 393 | 394 | Se quiser estudar um segundo sistema de demonstração de microsserviços, sugerimos este [repositório](https://github.com/GoogleCloudPlatform/microservices-demo), mantido pelo serviço de nuvem do Google. 395 | 396 | ## Créditos 397 | 398 | 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**. 399 | 400 | O código deste repositório possui uma licença MIT. O roteiro descrito acima possui uma licença CC-BY. 401 | --------------------------------------------------------------------------------