Back-End

11 out, 2018

Trabalhando com SAPUI5 localmente – Parte 03: adicionando mais serviços ao Docker

Publicidade

No artigo anterior, movemos um projeto para o Docker. A ideia era mover exatamente a mesma funcionalidade (mesmo sem tocar em nada dentro do código-fonte).

Agora vamos adicionar mais serviços. Sim, eu sei, parece overenginering (é exatamente isso, na verdade), mas eu quero construir algo com diferentes serviços trabalhando juntos. Vamos começar.

Mudaremos um pouco nosso projeto original. Agora nosso front-end terá apenas um botão. Esse botão aumentará o número de cliques, mas persistiremos essas informações em um banco de dados do PostgreSQL. Além disso, ao invés de incrementar o contador no back-end, nosso back-end emitirá um evento para um broker de mensagem RabbitMQ.

Teremos um serviço do worker para ouvir esse evento, e esse worker persistirá as informações. A comunicação entre o worker e o front-end (para mostrar o valor incrementado) será feita via websockets.

Com essas premissas, vamos precisar de:

  • Frontend: aplicação UI5
  • Backend: aplicativo PHP/lumen
  • Worker: aplicativo nodejs que está ouvindo um evento RabbitMQ e atendendo ao servidor websocket (usando socket.io)
  • Servidro Nginx
  • Banco de dados PosgreSQL
  • Broker de mensagens RabbitMQ

Como nos exemplos anteriores, nosso back-end PHP será servidor via Nginx e PHP-FPM.

Aqui podemos ver o arquivo docker-compose para configurar todos os serviços:

version: '3.4'
 
services:
  nginx:
    image: gonzalo123.nginx
    restart: always
    ports:
    - "8080:80"
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-nginx
    volumes:
    - ./src/backend:/code/src
    - ./src/.docker/web/site.conf:/etc/nginx/conf.d/default.conf
    networks:
    - app-network
  api:
    image: gonzalo123.api
    restart: always
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-lumen-dev
    environment:
      XDEBUG_CONFIG: remote_host=${MY_IP}
    volumes:
    - ./src/backend:/code/src
    networks:
    - app-network
  ui5:
    image: gonzalo123.ui5
    ports:
    - "8000:8000"
    restart: always
    volumes:
    - ./src/frontend:/code/src
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-ui5
    networks:
    - app-network
  io:
    image: gonzalo123.io
    ports:
    - "9999:9999"
    restart: always
    volumes:
    - ./src/io:/code/src
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-io
    networks:
    - app-network
  pg:
    image: gonzalo123.pg
    restart: always
    ports:
    - "5432:5432"
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-pg
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}
      PGDATA: /var/lib/postgresql/data/pgdata
    networks:
    - app-network
  rabbit:
    image: rabbitmq:3-management
    container_name: gonzalo123.rabbit
    restart: always
    ports:
    - "15672:15672"
    - "5672:5672"
    environment:
      RABBITMQ_ERLANG_COOKIE:
      RABBITMQ_DEFAULT_VHOST: /
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
    networks:
    - app-network
networks:
  app-network:
    driver: bridge

Vamos usar os mesmos arquivos do Docker utilizados no artigo anterior, mas também precisamos de novos para o worker, o servidor de banco de dados e a fila de mensagens:

Worker:

FROM node:alpine
 
EXPOSE 8000
 
WORKDIR /code/src
COPY ./io .
RUN npm install
ENTRYPOINT ["npm", "run", "serve"]

O script do worker é um script simples, que serve ao servidor socket.io e emite um websocket dentro de cada mensagem para a fila do RabbitMQ.

var amqp = require('amqp'),
  httpServer = require('http').createServer(),
  io = require('socket.io')(httpServer, {
    origins: '*:*',
  }),
  pg = require('pg')
;
 
require('dotenv').config();
var pgClient = new pg.Client(process.env.DB_DSN);
 
rabbitMq = amqp.createConnection({
  host: process.env.RABBIT_HOST,
  port: process.env.RABBIT_PORT,
  login: process.env.RABBIT_USER,
  password: process.env.RABBIT_PASS,
});
 
var sql = 'SELECT clickCount FROM docker.clicks';
 
// Please don't do this. Use lazy connections
// I'm 'lazy' to do it in this POC <img draggable="false" class="emoji" alt="🙂" src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/svg/1f642.svg">
pgClient.connect(function(err) {
  io.on('connection', function() {
    pgClient.query(sql, function(err, result) {
      var count = result.rows[0]['clickcount'];
      io.emit('click', {count: count});
    });
 
  });
 
  rabbitMq.on('ready', function() {
    var queue = rabbitMq.queue('ui5');
    queue.bind('#');
 
    queue.subscribe(function(message) {
      pgClient.query(sql, function(err, result) {
        var count = parseInt(result.rows[0]['clickcount']);
        count = count + parseInt(message.data.toString('utf8'));
        pgClient.query('UPDATE docker.clicks SET clickCount = $1', [count],
          function(err) {
            io.emit('click', {count: count});
          });
      });
    });
  });
});
 
httpServer.listen(process.env.IO_PORT);

Servidor de banco de dados:

FROM postgres:9.6-alpine
COPY pg/init.sql /docker-entrypoint-initdb.d/

Como podemos ver, vamos gerar a estrutura do banco de dados na primeira compilação.

CREATE SCHEMA docker;
 
CREATE TABLE docker.clicks (
clickCount numeric(8) NOT NULL
);
 
ALTER TABLE docker.clicks
OWNER TO username;
 
INSERT INTO docker.clicks(clickCount) values (0);

Com o servidor RabbitMQ, vamos usar a imagem docker oficial, por isso não precisamos criar um Dockerfile. Nós também mudamos um pouco nossa configuração Nginx. Queremos usar o Nginx para servir back-end e também o servidor socket.io. Isso porque não queremos expor portas diferentes à internet.

server {
    listen 80;
    index index.php index.html;
    server_name localhost;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /code/src/www;
 
    location /socket.io/ {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass "http://io:9999";
    }
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass api:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Para evitar problemas de CORS, também podemos usar o destino SCP (o proxy localneo neste exemplo), para servir socket.io também. Então precisamos:

  • Mudar nosso arquivo neo-app.json:
"routes": [
    ...
    {
      "path": "/socket.io",
      "target": {
        "type": "destination",
        "name": "SOCKETIO"
      },
      "description": "SOCKETIO"
    }
  ],

E é basicamente isso. Aqui também podemos usar um arquivo docker-compose de “produção” sem expor todas as portas e mapear o sistema de arquivos para a nossa máquina local (útil quando estamos desenvolvendo).

version: '3.4'
 
services:
  nginx:
    image: gonzalo123.nginx
    restart: always
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-nginx
    networks:
    - app-network
  api:
    image: gonzalo123.api
    restart: always
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-lumen
    networks:
    - app-network
  ui5:
    image: gonzalo123.ui5
    ports:
    - "80:8000"
    restart: always
    volumes:
    - ./src/frontend:/code/src
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-ui5
    networks:
    - app-network
  io:
    image: gonzalo123.io
    restart: always
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-io
    networks:
    - app-network
  pg:
    image: gonzalo123.pg
    restart: always
    build:
      context: ./src
      dockerfile: .docker/Dockerfile-pg
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}
      PGDATA: /var/lib/postgresql/data/pgdata
    networks:
    - app-network
  rabbit:
    image: rabbitmq:3-management
    restart: always
    environment:
      RABBITMQ_ERLANG_COOKIE:
      RABBITMQ_DEFAULT_VHOST: /
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
    networks:
    - app-network
networks:
  app-network:
    driver: bridge

E isso é tudo! O projeto completo está disponível na minha conta do GitHub.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://gonzalo123.com/2018/10/08/working-with-sapui5-locally-part-3-adding-more-services-in-docker/