Neste artigo, faremos uma aplicação prática, utilizando os recursos do Docker para acelerar o nosso desenvolvimento e integraremos tecnologias como MongoDB, Redis e jobs em nossa aplicação Node.js, fazendo uso de containers. Para finalizar, iremos gerar uma imagem dos recursos utilizados e publicaremos no Docker Hub.
Para isso, criaremos uma aplicação simples, com o uso do ExpressJS para receber via api uma mensagem para envio de e-mail (não vamos enviar o e-mail.
Será apenas uma simulação para entender o fluxo da aplicação e aplicabilidade dos recursos). A ideia é que, após o recebimento desta mensagem, enviaremos ela ao redis, utilizando um sistema de filas.
E com o uso node-scheduler, faremos a persistência desta no MongoDB. O detalhe é que o shceduler/worker/jobs também rodará em outro container, isolando a aplicação. Ao todo, para este exemplo iremos gerar quatro containers que trocarão informações entre si.
A vantagem de utilizar Docker no desenvolvimento e teste é a automatização dos recuros. Você não precisará necessariamente de um Mongo ou Redis instalado em sua máquina. Assim, conseguimos após o desenvolvimento entregar a mesma estrutura para os testes, garantindo uma melhor qualidade do projeto.
Mas antes de mais nada, você precisa de uma máquina com Docker. Para este processo usarei o Mac. Não aconselho o Windows para isso, pois não tem um funcionamento legal. Você consegue o instalador do Docker e exemplos no site dele:
Eu estarei utilizando o yarn para criação do projeto, então execute o comando yarn init -y
no diretório do projeto e adicione as bibliotecas yarn add body-parser express mongoose redis node-schedule
. De forma global, yarn global add nodemon
.
Instaladas as dependências do nosso projeto, criaremos a seguinte estrutura de pastas:
Na pasta conf criaremos dois arquivos para conexão do banco e redis. Não se preocupe, se não tiver esses recursos ou com a configuração do host destes, apenas configure igual ao exemplo.
Explicarei mais pra frente como faremos a ligação disso via Docker. A configuração do db.js ficará da seguinte maneira:
const mongoose = require("mongoose");
const host = "mongo";
const port = "27017";
mongoose.connect(`mongodb://${host}:${port}/email`).then(connect => {
console.log("==> conexão com o MONGO OK!");
}).catch(err => {
console.log("==> falha na conexão com o MongoDB!");
});
module.exports = mongoose;
E a do Redis (redis.js) assim:
const redis = require('redis');
const host = 'redis';
const port = '6379';
const client = redis.createClient(port, host);
client.on('connect', function(){
console.log('==> Conexão com o REDIS OK!')
})
client.on('error', function (err) {
console.log('==> Falha na conexão com o REDIS: ' + err);
});
module.exports = client;
Dentro da pasta Models criaremos um arquivo chamado “emailModel.js”, onde vamos criar o schema para armazenamento da mensagem:
const mongoose = require('../conf/db');
const Schema = mongoose.Schema;
const emailSchema = new Schema({
remetente:{
type:String
},
destinatario:{
type:String
},
assunto:{
type:String
},
texto:{
type:String
}
});
const emailModel = mongoose.model('email', emailSchema);
module.exports = emailModel;
No arquivo de inicialização da nossa api, será o app.js. Repare abaixo que utilizaremos a propriedade SADD do Redis para criar uma chave com o nome “sendEmail”, que conterá várias requisições. Ou seja, uma chave com vários itens dentro.
const express = require('express');
const bodyParser = require("body-parser");
const redis = require('./conf/redis');
const mongo = require('./conf/db');
const port = 3000;
const app = express();
app.use(bodyParser.json());
app.post('/', async (req, res) => {
console.log('body', req.body)
const value = JSON.stringify(req.body);
let retorno = { mensagem: 'Email enviado para a fila de processamento' }
await redis.SADD("sendEmail", value, function (err, success) {
if (!err)
retorno = { mensagem: `Falha ao enviar email para a fila: ${err}` }
});
res.send(retorno)
});
app.listen(port);
Agora iremos configurar o nosso arquivo de job. Criamos dentro da pasta jobs um arquivo chamado persistencia.js. Este, por sua vez, vai ler a fila no redis e enviar ao MongoDB.
Para isso, utilizaremos o “SMEMBER” do redis para ler todos os valores que estão dentro da chave “sendEmail”, e se caso der certo a leitura, o valor será armazenado no MongoDB. Por fim, a chave será removida do redis com o comando “SREM”. O scheduler irá executar a cada cinco segundos:
const redis = require("../conf/redis");
const emailModel = require("../Models/emailModel");
const schedule = require("node-schedule");
let exec = 0;
persistir = async () => {
await redis.smembers("sendEmail", function(err, values) {
if (!err)
for (i in values) {
let value = values[i];
let email = JSON.parse(value);
emailModel.create(email);
redis.SREM("sendEmail", value);
}
});
};
const job = schedule.scheduleJob("0-59/5 * * * * *", async date => {
exec += 1;
await persistir();
console.log(`execução número:${exec}, hora:${date}`);
});
Com isso, finalizamos a nossa aplicação! Agora criaremos um arquivo chamado .dockerignore na raiz da nossa aplicação. O funcionamento deste cara é similar ao .gitignore, e iremos restringir a utilização da pasta node_modules:
Agora, para fazer nossa aplicação executar dentro de um container, criaremos um arquivo na raiz da nossa aplicação, chamado Dockerfile. Este arquivo será a configuração, ou os passos que iremos executar para que nossa aplicação execute:
Entendo os comandos:
FROM node:10-alpine
= versão de uma máquina com o básico do node versão 10WORKDIR /usr/app
= Onde será descarregada a imagem da nossa app dentro da máquinaCOPY package.json yarn.lock ./
= copiar os arquivosRUN yarn
= executa o yarn para instalar as dependênciasCOPY . .
EXPOSE 3000
= Vai colocar a porta 3000 expostaCMD [“yarn”, “start”]
= vai rodar o comandoyarn
buscando o valor do start, configurado no package.json
Configurando o package.json:
Nesta etapa precisamos alterar algumas configurações no nosso package.json para rodar a nossa aplicação e a persistência dos e-mails.
No script padrão de start executaremos sobre o nodemon. E também criaremos um novo comando chamado “persistência”, que executará nosso job:
Agora, com toda a nossa aplicação configurada, faremos as integrações dos containers para rodar. Para isso, vamos utilizar o docker-compose, que tem o poder de relacionar os containers da nossa aplicação com os demais serviços que vamos utilizar. O arquivo docker-compose.yaml deverá ser criado na raíz da nossa aplicação e sua estrutura será a seguinte:
version: "3"
services:
app:
container_name: app
build: .
ports:
- "3000:3000"
command: yarn start
volumes:
- .:/usr/app
links:
- mongo
- redis
jobs:
container_name: workers
build: .
command: yarn persistencia
volumes:
- .:/usr/app
links:
- redis
mongo:
container_name: mongoDB_dev
image: mongo
volumes:
- ./data:/data/db
ports:
- "27017:27017"
redis:
container_name: redis_dev
image: redis
volumes:
- ./data:/redis/db
ports:
- "6379:6379"
Explicando o compose:
- app: nome da nosso serviço da aplicação
- container_name: nome que terá o nosso container
- build: diretório do build. No caso, o mesmo diretório
- ports: porta que será utilizada e para onde ela será redirecionada no nosso container
- command: qual será o script de execução da nossa aplicação. Este, por sua vez, é o que configuramos no package.json
- volumes: este parâmetro é muito importante ter configurado. É onde os dados dos nossos containers serão persistidos. Ou seja, quando desligar um container/atualizar, os dados irão permanecer. Caso não configure este parâmetro, os dados sempre serão apagados quando o container der baixa
- links: este é o parâmetro que habilitará o uso da nossa aplicação com os demais serviços. Então, o ideal é sempre informá-los. Repare que é o nome dos serviços de Mongo e Redis
Quanto ao services Redis e Mongo, se você reparar, os hosts do db.js e redis.js são os mesmos, não será utilizado ip para comunicação. O Docker automaticamente configurará essa string de conexão, por isso é importante dar o mesmo nome.
Agora terminamos nossas configurações do docker e estamos prontos para executar nossa aplicação integrada com os demais serviços que também rodarão em container. Ufa!
Para executar tudo, basta executar no terminal docker-compose up
. Este processo pode demorar um pouco na primeira vez, pois o Docker fará o download das imagens do Mongo e Redis, caso não existir.
Se tudo rodou e inicializou sem problemas excelente! Mas ainda temos um detalhe a tratar antes de executar um post para nossa app.
Criar o database. Então, no seu gerenciado Mongo, conecte com o host:localhost e porta:27017 que foi a porta que alocamos no docker-compose. O nome da base a ser criada, conforme a string de conexão, se você seguiu este artigo, é “email”.
Agora, sim! Bora testar!
Nos logs que deixei na aplicação:
Se abrirmos nossa base de dados, veremos que o registro foi persistido! Para testar e ver como fica a estrutura da chave no redis é só não executar o job, e é claro, ter instalado um gerenciador do redis como o Redis Desktop Manager ou via terminal mesmo.
Para parar o serviço, Ctrl + C. Perceba que, se você alterar qualquer coisa no código, automaticamente o serviço se reinicia por causa do nodemon. Para ver como tudo está rodando e acompanhar o processamento, em um novo terminal execute docker stats -a
.
Gerando uma imagem:
Vamos imaginar que você queira levar essa estrutura para os teste. Você não vai copiar o seu projeto para uma outra máquina e executar o compose – nós vamos gerar uma imagem com a estrutura criada!
Para isso, o ideal é você tenha uma conta criada no Docker Hub.
Para gerar a imagem, não é necessário ter a conta no Docker Hub, mas se você quer compartilhar sua imagem e fazer uso dela em outro ambiente, seria ideal. Para criar a imagem, basta rodar docker build -t seu_usuario_dockerhub/nome_image
.
Observação: precisa do “ .” para que seja especificado que é o diretório atual.
Para ver se deu certo, basta rodar no seu terminal docker images
. Sua imagem deve ser listada!
Enviando a imagem ao Docker Hub:
Execute no seu terminal o comando Docker login e informe seu usuário e senha do Docker-Hub. Depois disso, basta executar o comando:
docker push nome_da_imagem
Após é só abrir o seu repositório no Docker-Hub e conferir se imagem está disponível. Você pode tornar a imagem pública. Dessa forma, outras pessoas da sua equipe podem fazer uso dela.
Se você quer fazer uso da imagem que geramos neste exemplo, basta fazer o pull dela do meu hub: docker pull wagnerww/nodejs-docker-mongo-redis.
Finalizando
Neste artigo vimos como automatizar o uso do Docker em sua aplicação Node.js, gerando uma imagem do seu ambiente e publicando-a no Docker-Hub.
É muito bacana utilizar estes recursos disponíveis do Docker, pois facilita e automatiza o seu processo de gerar um build da sua aplicação para enviar para os testes ou até produção, dependendo do caso.
Claro, você não necessariamente precisa configurar o Redis ou o Mongo direto no compose – você pode ter contêineres separados e integrar via host normal, sem problemas nenhum.
Existem vários outros recursos que podem ser aplicados também, como o PM2, NGINX e por aí vai, mas isso é questão para outro artigo.
Espero que tenha gostado. Não deixe de compartilhar e deixar seu like neste artigo.
Até a próxima!
Para entender melhor sobre o Dockerfile e o compose, aconselho esta leitura:
Repositório do código do exemplo.