DevSecOps

3 mai, 2019

Automatizando o ambiente de desenvolvimento e testes com Docker

Publicidade

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 10
  • WORKDIR /usr/app = Onde será descarregada a imagem da nossa app dentro da máquina
  • COPY package.json yarn.lock ./ = copiar os arquivos
  • RUN yarn = executa o yarn para instalar as dependências
  • COPY . .
  • EXPOSE 3000 = Vai colocar a porta 3000 exposta
  • CMD [“yarn”, “start”] = vai rodar o comando yarn 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.