DevSecOps

25 mai, 2018

Arquitetura de micro serviços em Node.js + MongoDB – Parte 02

Publicidade

No primeiro artigo desta série, eu fiz um resumão do porquê de escolher uma arquitetura de micro serviços para seus sistemas valer a pena, quais as vantagens do modelo e indiquei Node e Mongo como uma dupla de tecnologias a serem consideradas para este tipo de abordagem. Finalizei o artigo passado explicando a arquitetura de um case de exemplo envolvendo um sistema para uma rede de cinemas.

Neste artigo continuaremos a série, mas desta vez, colocando a mão na massa: organizaremos a estrutura padrão que será usada em nossos microservices, construiremos o primeiro deles e modelaremos o seu banco de dados.

Então, vamos lá!

Organizando a arquitetura

Relembrando rapidamente o primeiro cenário de uso da nossa arquitetura de microservices:

Consulta de Lançamentos do Cinema

Neste cenário, iniciaremos nosso desenvolvimento com o microservice MOVIES e sua respectiva database. Cabe a esse serviço fornecer informações referentes ao catálogo de filmes cujos direitos de exibição foram comprados pela rede. Além do CRUD básico, espera-se deste serviço que seja possível saber quais filmes são os lançamentos da rede, basicamente os que entraram nos últimos 30 dias, que é mais ou menos a duração do status de lançamento de um filme.

Para estruturar este projeto como um todo, crie uma pasta central chamada cinema-microservice. Dentro dela colocaremos todos os microservices e dados dos mesmos, divididos em subpastas, por uma questão de organização, como mostra a hierarquia de pastas abaixo.

  • cinema-microservice
    • movies-service
    • data
    • src
  • cinema-catalog-service
    • data
    • src

Obviamente, quando fizermos o deploy dos mesmos, eles serão feitos de maneira independente, mas por uma questão de organização do projeto e do repositório, se você vier a versionar este projeto, faz sentido agrupá-los desta forma. Apenas lembre-se de não versionar as pastas de dados e a node_modules de cada microservice, adicionando os respectivos caminhos no seu .gitignore.

Dentro da subpasta movies-service, que é a que vamos focar neste artigo, temos as pastas data e src. Na pasta data armazenaremos os dados do nosso banco MongoDB (basta apontar o dbpath para cá na inicialização do banco) deste microservice. Já na pasta src, armazenaremos os códigos-fonte do mesmo.

Dentro da pasta src teremos a seguinte estrutura de pastas e arquivos, em todos os nossos microservices a partir deste aqui:

  • movies-service
  • src
  • api
  • config
  • repository
  • server
  • index.js
  • packages.json

Os arquivos index.js e packages.json são auto-explicativos no cenário de uma webapi em Node.js. Na pasta api teremos os módulos das rotas deste microservice. Na pasta config, os módulos de configuração e de acesso básico a dados (MongoDB cru). Na pasta repository nós teremos módulos seguindo o pattern Repository, uma versão mais “NoSQL” do pattern DAO (Data Access Object, focado em SQL).

E basicamente esta é a estrutura. Agora vamos aos dados!

Organizando os dados

Como estamos focando no microservice MOVIES, nossa base de dados será bem tranquila, pois teremos apenas uma coleção de documentos com todos os filmes dentro. Obviamente, se você não está acostumado com modelagem de dados em MongoDB (se é que modelagem é o termo correto aqui), sugiro a série de artigos MongoDB para iniciantes em NoSQL e até mesmo o meu livro de MongoDB.

Nossos filmes possuem a seguinte informação:

  • Identificador único
  • Título
  • Duração (em minutos)
  • Imagem (capa promocional)
  • Sinopse
  • Data de lançamento
  • Categorias (ação, romance, etc)

Obviamente você deve imaginar que poderíamos ter muitas outras informações aqui, como faixa etária, trailer, formato de tela, idioma, etc. Vou ficar só com essas por uma questão de simplicidade.

Em um banco relacional tradicional, como isso seria modelado? Algumas colunas da suposta tabela “Filmes” são bem óbvias, como: ID, Titulo, Duracao, Sinopse e DataLancamento. Mas e o campo imagem? Apesar dos bancos SQL suportarem BLOBs, nunca foi uma boa opção por pesar demais nos SELECTs e no crescimento do banco como um todo. No entanto, o mesmo não pode ser dito do MongoDB, onde podemos ter campos binários facilmente sem abrir mão da performance. Ainda assim entenderei se você decidir por armazenar apenas a URL da imagem em uma URL pública (AWS S3?).

Mas o que eu queria falar mesmo era das categorias. Esta é uma relação que pelas formas normais e levando muito a sério a não-repetição dos dados deveria ser N-N com 3 tabelas: uma “Filmes”, outra” Categorias” e a terceira “CategoriaFilmes” apenas com chaves-estrangeiras para as duas primeiras. No entanto, esta não é a abordagem sugerida para o MongoDB. Aqui até podemos ter uma coleção de documentos “Categorias”, se necessária, mas a abordagem mais comum é usar um campo multivalorado no documento de filme contendo as categorias do mesmo; simples assim.

Obviamente você deve se preocupar em garantir que as categorias sejam escritas sempre da mesma forma, à nível de aplicação. Caso contrário, será terrível filtrar por elas mais tarde. Enfim, nossa coleção de Filmes possuirá documentos com a seguinte estrutura:

{
   _id: ObjectId("sacbaskbcksabckscstds67ds"),
   titulo: "Vingadores: Guerra Infinita",
   sinopse: "Os heróis mais poderosos da Marvel enfrentando o Thanos",
   duracao: 120,
   dataLancamento: ISODate("2018-05-01T00:00:00Z"),
   imagem: "http://www.luiztools.com.br/vingadores-gi.jpg",
   categorias: ["Aventura", "Ação"]
}

Para subir o banco de dados do nosso microservice, apenas use uma instância do mongod apontando o dbpath para a pasta data dentro de cinema-microservice/movies-service/data. Obviamente, em produção você terá uma abordagem diferente, mas ainda de um banco para cada microservice.

Conectando o banco

Agora que temos o modelo do nosso banco pronto e a estrutura de pastas organizada, vamos começar a programar nosso primeiro microservice.

Vamos começar acessando a pasta do nosso movie-service/src via terminal, criando um arquivo index.js na raiz desta pasta e usando o comando ‘npm init’ nela, que é para criar o package.json do microservice. Depois, rode o comando abaixo pra garantir que teremos as nossas dependências mínimas garantidas.

npm install express mongodb dotenv-safe tape tap-spec helmet morgan

Tem várias coisas que devemos fazer e não há necessariamente uma ordem certa para que elas funcionem. O primeiro microservice será um pouco chato e demorado de fazer, mas conforme a gente for avançando pelos demais, você irá pegando o jeito. Sendo assim, vou começar por algo que acho que é mais fácil de todo mundo entender, o acesso a dados.

Dentro da pasta movie-service/src/config vamos criar um arquivo mongodb.js, com o seguinte conteúdo dentro:

const MongoClient = require("mongodb").MongoClient;
var connection = null;
var db = null;

function connect(callback){
    if(connection) return callback(null, db);

    MongoClient.connect(process.env.MONGO_CONNECTION, (err, conn) => {
        if(err) 
            return callback(err, null);
        else {
            connection = conn;
            db = conn.db(process.env.DATABASE_NAME);
            return callback(null, db);
        }
    })
}

function disconnect(){
    if(!connection) return true;
    connection.close();
    connection = null;
    return true;
}

module.exports = { connect, disconnect }

Note que esse arquivo mongodb.js espera que existam duas variáveis de ambiente com a string de conexão ao banco. Essas variáveis de ambiente devem ser definidas em um arquivo sem nome com a extensão ‘.env’ na raiz do movie-service/src/, sendo que o pacote dotenv-safe que instalamos anteriormente exige a existência de um ‘.env.example’ com a definição das variáveis de ambiente existentes.

#.env, don't commit to repo
MONGO_CONNECTION=mongodb://localhost:27017
DATABASE_NAME=movie-service
PORT=3000
#.env.example, commit to repo
MONGO_CONNECTION=
DATABASE_NAME=
PORT=

Para nos certificarmos que este módulo está funcionando, vamos escrever um teste unitário para ele. Se você nunca ouviu falar em testes unitários antes, recomendo ler este artigo sobre TDD.

Na mesma pasta movie-service/src/config, crie um arquivo mongodb.test.js, e dentro escreva o seguinte código, que nada mais faz do que usar a biblioteca tape (que foi instalada anteriormente no nosso npm install) pra testar a conexão:

const test = require('tape');
const mongodb = require('./mongodb');

function runTests(){
    test('MongoDB Connection', (t) => {
        mongodb.connect((err, conn) => {
            t.assert(conn, "Connection established");
            t.end();
        });
    })

    test('MongoDB Disconnection', (t) => {
        t.assert(mongodb.disconnect(), "Disconnected");
        t.end();
    })
}

module.exports = { runTests }

Como teremos muitos arquivos de teste diferentes em nossa aplicação, cada um em sua pasta, vamos criar na raiz de movie-service/src um index.test.js que vai indexar todos nossos testes, a começar por esse primeiro, como abaixo:

require("dotenv-safe").load();
require("./config/mongodb.test").runTests();

Note que também carreguei o módulo do dotenv-safe pois precisamos que as variáveis de ambiente estejam carregadas para que nossos testes funcionem.

Falando em funcionar, antes de rodar este teste abra o seu packages.json que fica na raiz de movie-service/src e edite-o para que os scripts de start e de test fiquem iguais ao código abaixo:

{
  "name": "movie-service",
  "version": "1.0.0",
  "description": "Service to provide detailed movies info.",
  "main": "index.js",
  "scripts": {
    "start": "node index",
    "test": "node index.test | tap-spec"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.3",
    "mongodb": "^3.0.7",
    "tap-spec": "^4.1.1",
    "tape": "^4.9.0"
  }
}

Se você rodar agora sua aplicação com o comando abaixo, o seu unit test deve ser executado. Caso seu banco esteja online, obviamente.

/movie-service/src> npm test

Consultando o banco

Agora que sabemos que nossa conexão com o banco funciona, vamos criar nosso módulo de repositório para que possamos fornecer os dados do MongoDB da maneira que as chamadas ao nosso serviço esperam.

Não vou fazer um CRUD completo aqui, pois já abordei CRUDs de Node com Mongo em outras oportunidades. Dentro do nosso case de exemplo, levarei em conta que precisamos implementar apenas o R (Read) para fornecer dados de filmes específicos (por id) e dos filmes que são lançamentos nos cinemas (lançados nos últimos 30 dias).

Para criar nosso módulo de repositório – que por sua vez usará o módulo mongodb.js -, entre na pasta movie-service/src/repository e crie dois arquivos, o repository.js e o repository.test.js, sendo que o primeiro deve ter o conteúdo abaixo:

const mongodb = require("../config/mongodb");

function getAllMovies(callback){
    mongodb.connect((err, db) => {
        db.collection("movies").find().toArray(callback);
    })
}

function getMovieById(id, callback){
    mongodb.connect((err, db) => {
        db.collection("movies").findOne({_id: require("mongodb").ObjectId(id)}, callback);
    });
}

function getMoviePremiers(callback){

    var monthAgo = new Date();
    monthAgo.setMonth(monthAgo.getMonth() - 1);
    monthAgo.setHours(0, 0, 0);
    monthAgo.setMilliseconds(0);

    mongodb.connect((err, db) => {
        db.collection("movies").find({ dataLancamento: { $gte: monthAgo } }).toArray(callback);
    });
}

function disconnect(){
    return mongodb.disconnect();
}

module.exports = { getAllMovies, getMovieById, getMoviePremiers, disconnect }

Aqui temos uma função para cada um dos três métodos elementares que precisamos ter na APi, e uma última para desconectar o repositório do banco de dados, função esta que será usada em certas ocasiões como em testes unitários.

E no segundo arquivo, repository.test.js, colocamos os testes do primeiro, de maneira análoga ao que fizemos com o módulo mongodb.test.js:

const test = require('tape');
const repository = require('./repository');

function runTests(){

    var id = null;

    test('Repository GetAllMovies', (t) => {
        repository.getAllMovies((err, movies) => {
            if(movies && movies.length > 0) id = movies[0]._id;
            
            t.assert(!err && movies && movies.length > 0, "All Movies Returned");
            t.end();
        });
    })
    
    test('Repository GetMovieById', (t) => {
        if(!id) {
            t.assert(false, "Movie by Id Returned");
            t.end();
            return;
        }

        repository.getMovieById(id, (err, movie) => {
            t.assert(!err && movie, "Movie by Id Returned");
            t.end();
        });
    })

    test('Repository GetMoviePremiers', (t) => {
        repository.getMoviePremiers((err, movies) => {
            t.assert(!err && movies && movies.length > 0, "Movie Premiers Returned");
            t.end();
        });
    })

    test('Repository Disconnect', (t) => {
        t.assert(repository.disconnect(), "Disconnect Ok");
        t.end();
    })
}

module.exports = { runTests }

E por fim, adicione mais uma linha em nosso movie-service/src/index.test.js para que rode também este novo módulo de teste:

require("dotenv-safe").load();
require("./config/mongodb.test").runTests();
require("./repository/repository.test").runTests();

Obviamente que estes últimos testes não passarão se você rodar um ‘npm test’ no terminal, mas isso porque nosso banco de dados não possui qualquer informação de filme, o que você pode resolver abrindo uma instância do utilitário ‘mongo’ no terminal (executando um use no banco ‘movie-service’) e inserindo o comando abaixo para adicionar uma carga de filmes:

db.movies.insert([{
   titulo: "Os Vingadores: Guerra Infinita",
   sinopse: "Os heróis mais poderosos da Marvel enfrentando o Thanos",
   duracao: 120,
   dataLancamento: ISODate("2018-05-01T00:00:00Z"),
   imagem: "http://www.luiztools.com.br/vingadores-gi.jpg",
   categorias: ["Aventura", "Ação"]
},
{
   titulo: "Os Vingadores: Era de Ultron",
   sinopse: "Os heróis mais poderosos da Marvel enfrentando o Ultron",
   duracao: 110,
   dataLancamento: ISODate("2016-05-01T00:00:00Z"),
   imagem: "http://www.luiztools.com.br/vingadores-eu.jpg",
   categorias: ["Aventura", "Ação"]
},
{
   titulo: "Os Vingadores",
   sinopse: "Os heróis mais poderosos da Marvel enfrentando o Loki",
   duracao: 100,
   dataLancamento: ISODate("2014-05-01T00:00:00Z"),
   imagem: "http://www.luiztools.com.br/vingadores.jpg",
   categorias: ["Aventura", "Ação"]
}])

Agora, sim; ao rodar o ‘npm test’, seus testes unitários devem passar com sucesso:

Repository Tests – OK

Com estes testes todos passando, temos a certeza de que a parte do banco de dados da nossa futura API estará 100% operacional, cabendo agora programarmos a API em si, que irá trabalhar com estes dados que viemos “brincando” até então.

No entanto, a programação da API movie-service ficou para a terceira parte desta série de artigos!