Node.js, Docker e docker-compose

E é assim que funciona. A aplicação fica em um ambiente seguro e pode ser executada em qualquer lugar

Eu escrevi um post explicando o que é e como funciona o Docker. A grosso modo, "é tipo uma máquina virtual" (normalmente entendem melhor dessa forma). Você coloca um projeto ou serviço dentro de um container Docker e pode executar ele em qualquer lugar, sem precisar de configurações especiais.

Pra você ter uma ideia, nós vamos fazer uma API usando Node.js e nem vamos precisar ter o Node.js instalado na máquina. Nada de dores de cabeça com configurações e instalações (exceto as instalações do Docker e do docker-compose, claro).

Você pode ter na sua máquina várias versões do Java, Python, Apache, Kafka, Redis. Tudo isso sem precisar configurar nada.

O que é o projeto

Nós vamos escrever uma API RESTful, usando Express, com dois endpoints que respondem ao verbo GET e retornam strings retiradas do package.json do próprio projeto. O que vai ser executado, de fato, vão ser os testes desses endpoints. Os testes serão feitos usando Mocha, Chai e Chai-HTTP (uma extensão do Chai que permite fazer requests). Tudo isso rodando dentro de um container.

O repositório do projeto está no GitHub.

Mas que diabos é "docker-compose"?

Explicação rápida: é uma ferramenta para para executar aplicações que usam múltiplos containers. Seu serviço tem uma API, um banco de dados, um serviço de cache e mais um servidor storage? Ao invés de configurar um por um na sua máquina (e muito possivelmente perder um tempo precioso com configuração e frustração), coloque tudo em containers e execute tudo de uma vez com um só comando. Essencialmente, configure uma vez, execute quantas vezes for.

Instalação

Apenas dois programas serão necessários para esse projeto (e, sério, você não precisa instalar o Node.js).

Construindo a API

O começo desse projeto é bem standard. Você cria um diretório, depois adiciona o package.json .

O que é cada pacote

  • express - framework que iremos usar para criar a API RESTful
  • mocha - framework de testes que iremos usar para testar o servidor web
  • chai - biblioteca para fazer as asserções dos testes
  • chai-http - extensão do Chai com métodos HTTP integrados (ele vai fazer os requests durante os testes)

O servidor Express

Na raiz do projeto, o arquivo server.js deve ser criado com o seguinte conteúdo:

Por mais que o código do servidor seja bem direto, existem algumas coisas que devem ser resaltadas. Primeiro que estamos usando o conteúdo do package.json para servir de conteúdo para o retorno dos endpoints (as dependências em "/deps" e a versão em "/versao"). A segunda coisa é a ultima linha do arquivo. "module.exports = server". O servidor é exportado para que possa ser testado pelo mocha (isso já vai ficar claro).

Os testes do servidor web

Primeiro é preciso criarmos o diretório "test" na raiz do projeto. Depois, criar um novo arquivo para hospedar os testes. O nome do arquivo pode ser qualquer um. Aqui vou chamar de "express.test.js".

Então adicionamos o conteúdo do arquivo. Os testes vão verificar se a versão e as dependências, ambos retornados pelos endpoints, estão de acordo com o esperado.

As três coisas (mais) importantes aqui são as seguintes:

  • require('../server')
  • chai.use(chaiHttp)
  • chai.request(server).get()

Em "require('../server')" nós importamos o servidor express que exportamos na última linha de "server.js". Ele vai ser usado pelo "chai-http" para fazer os requests aos endpoints.

"chai.use" é um método para adicionar extensões à instância do chai que criamos. Ao adicionar "chaiHttp" como argumento, estamos incrementando as funcionalidades do chai (nesse caso, com métodos HTTP).

Dentro dos dois testes presentes no arquivo, são feitos requests GET para o nosso servidor. Esses requests acontecem com a chamada do método "chai.request(server).get(rota)", onde "server" é o nosso servidor importado, e "rota" é a rota desejada.

Um último ajuste

É importante editar a propriedade "test" dentro de "scripts", no "package.json". O Mocha deve ser chamado quando "npm test" for executado.

Esse "--recursive" diz ao mocha para olhar dentro do diretório "test" (obrigatóriamente com esse nome) e executar todos os arquivos de teste que estiverem lá dentro.

O Dockerfile

O Docker vai compilar o Dockerfile e criar o container que iremos usar. Esse container vai ser um Linux Alpine, com o Node.js 8 e nossa API e suas dependências.

O arquivo deve ter o nome "Dockerfile" e deve ser criado na raiz do projeto e deve ter o seguinte conteúdo:

Explicando as seis linhas do arquivo:

  • baixe um Linux Alpine com o Node.js 8
  • pegue o package.json da aplicação coloque no diretório temporário (é mais do que isso, mas vamos deixar assim por enquanto)
  • crie o diretório "/src", depois entre no diretório temporário e instale as dependêndias do projeto
  • mova o "node_modules" criado em "/tmp" para o diretório "/src" que criamos anteriormente
  • pegue todos os arquivos do projeto (o código fonte) e adicione-os, juntamente ao "node_modules", em "/src"
  • marque "/src" como o "diretório principal" do container

Nós podemos usar esse container como está para testar a nossa aplicação. Entretanto, essa não seria a melhor maneira. Nós vamos fazer o docker-compose cumprir essa tarefa.

O docker-compose

O docker-compose vai automatizar as nossas tarefas. Nesse primeiro caso, onde temos apenas uma API (e sem banco de dados), ele vai fazer pouco. No próximo post vou tratar como trabalhar com uma API (usando Node.js) e um banco de dados (MongoDB) dentro do docker-compose.

O arquivo "docker-compose.yml", como o "Dockerfile", deve ficar na raiz do projeto. Seu conteúdo é o seguinte:

A primeira linha do arquivo indica a versão da API do docker-compose que vai ser usada. Ela não é muito importante para o nosso contexto, mas tem que estar lá.

O que o arquivo faz, linha por linha:

  • lista os serviços que vão ser utilizados (no nosso caso, somente um)
  • define o primeiro serviço com o nome "api" (podemos usar qualquer nome. Coloquei "api" porque, afinal, se trata de uma API)
  • cria uma imagem de container baseado no "Dockerfile" presente no diretório atual (no caso, ".")
  • nomeia a imagem de "nddc"
  • executa o comando "npm test" assim que o container estiver online

No Dockerfile nós definimos como "WORKDIR" o diretório onde estava o projeto dentro do container. Quando o docker-compose criar o container, ele vai entrar nesse diretório. Então vai executar o comando que está definido em "command".

Feito isso, vamos executar nossos testes:

Esse comando vai criar uma imagem Docker (nddc), copiar o código fonte da API, instalar as dependências e, finalmente, executar os testes. Terminando os testes, o docker-compose termina a execução do container retornando o exit status dos testes (zero ou não zero). Tudo isso em um ambiente seguro e isolado. Não vão haver problemas de "na minha máquina funcionou" ou coisas do tipo. Vai funcionar na sua máquina da mesma forma como vai funcionar no servidor de produção.

O resultado da execução do comando deve ser esse:

Esse foi um post introdutório ao docker-compose. No próximo post vamos adicionar o MongoDB e fazer algumas operações entre dois containeres, sem precisar se incomodar com a instalação e configuração de banco de dados na sua máquina (ou em qualquer máquina, for that matter).

:wq