Como criar um proxy reverso usando Nginx para responder um JSON quando chamar algum endpoint


Para ver este post em inglês, clique aqui.


O problema

No meu trabalho atual tive que criar uma página para verificar o status dos nossos ambientes, se estavam em funcionamento ou com problemas.

Semelhante ao que existe na AWS, na Apple, etc, como podem ver na imagem abaixo.

Trabalho finalizado, hora de escrever os testes para garantir a qualidade, prevenir que mudanças futuras no código quebrem o código atual, etc

Mas como eu poderia fazer isso? Não seria correto usar os endpoints reais nos testes (exceto se fossem testes de integração, o que não era o caso). Uma alternativa para esse problema seria utilizar um proxy reverso para enviar a resposta para o backend quando minha aplicação atiginsse o endpoint desejado. A imagem abaixo ilustra o funcionamento desse processo de forma simplificada.

Para conseguir esse funcionamento, decidi configurar um proxy reverso utilizando NGINX e subir ele com docker/docker-compose.


Proxy reverso

Um proxy reverso é um servidor que fica entre o cliente o servidor web, direcionando as requisições dos clientes para o servidor web apropriado e retornando a resposta para o cliente. NGINX é um servidor web open-sorce popular que também pode atuar como proxy reverso. Neste caso, ao invés de encaminharmos a requisição que chega no NGINX para outro web server, vamos devolve-la automaticamente para o cliente com a resposta desejada.

Em primeiro lugar, criei o Dockerfile com a imagem do proxy reverso, conforme visto abaixo, e copiei o arquivo nginx.conf para o container do proxy.

FROM nginx:alpine

COPY nginx.conf /etc/nginx/nginx.conf

O nginx.conf está abaixo, e deve estar no mesmo diretório que o Dockerfile:

worker_processes 1;

events { worker_connections 1024; }

http {

    sendfile off;

    upstream docker-nginx {
        server localhost:80;
    }

    server {
        listen 8080;

        location ~ /backend {
            default_type application/json;
            return 200 '{"api_version":"4.0.0", "status":"Ok"}\n';
        }

        location ~ /frontend {
            default_type application/json;
            return 200 '{"version":"2.0.0","status":"OK"}\n';
        }

        location ~ /* {
            default_type text/html;
            return 200 'NGINX reverse proxy up\n';
        }
    }
}

Definimos worker_process explicitamente como 1, mesmo sendo o valor padrão. É uma prática comum rodar 1 worker process por core. Para maiores informações, podem consultar o seguinte link: Thread Pools in NGINX Boost Performance 9x.

O worker_connections define o máximo de conexões simultâneas que podem ser abertas por um worker process. O valor padrão é 1024.

O sendfile é normalmente essencial para otimizar o desempenho dos web servers passando os ponteiros (sem copiar o objeto inteiro) direto para o socket. Como estamos utilizando o NGINX como proxy reverso para devolver as requests realizadas para o cliente podemos desabilitar essa funcionalidade.

A diretiva upstream define um grupo de servidores que podem escutar em diferentes portas. Neste caso, estamos definindo um servidor somente.

O server foi configurado para escutar a porta 8080. Quando fazemos uma requisição para localhost:8080/endpoint o endpoint deve bater com um dos definidos em locations, e retornar o que estiver configurado.

  • Se o endpoint for localhost:8080/backend, então a resposta vai ser o json definido em location ~ /backend com com status code definido lá

  • Se o endpoint for localhost:8080/frontend, então a resposta vai ser o json definido em location ~ /frontend com com status code definido lá

  • Se o endpoint for diferente de /backend e /frontend, a resposta será o html definido em location ~ /* com com status code definido lá

Nota: default_type deve ser adicionado, ou o browser vai baixar o json como um arquivo não reconhecido/identificado.


Docker compose

Com Compose, nós definimos uma aplicação multi-conteiner em um único arquivo, agilizando o processo de subir nossa aplicação com um único comando que faz todo o necessário para carregar o ambiente.

A principal aplicação do Docker Compose é para a criação de uma arquitetura de microserviços, carregando os containers e realizando a ligação entre eles.

Os comandos do docker-compose são similares aos comandos do Docker, mas afetam toda a arquitetura multi-container definida no arquivo de configuração docker-compose.yml, e não somente um container.

Para subir uma aplicação com Docker Compose, devem ser seguidos três passos:

  • Definir cada serviço em um Dockerfile

  • Definir os serviços e as relações entre eles no arquivo docker-compose.yml

  • Usar o comando docker-compose up para iniciar o sistema.

Abaixo podemos ver parte do arquivo de configuração docker-compose.yml

  reverse_proxy:
    build:
      context: ./reverse_proxy/
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    expose:
      - "8080"
    restart: always
    networks:
      local_network:
        aliases:
          - reverse_proxy

O context é ./reverse_proxy/ porque tanto o Dockerfile quanto o nginx.conf estão dentro do diretório reverse_proxy.


Testando

Após configurar o Docker Compose, utilizamos o comando docker-compose up para subir a aplicação e testar o proxy reverso.

user@machine:~$ docker-compose up
Creating reverse_proxy_1 ... done

user@machine:~$ curl http://localhost:8080/test
NGINX reverse proxy up

user@machine:~$ curl http://localhost:8080/backend
{"api_version":"4.0.0", "status":"Ok"}

user@machine:~$ curl http://localhost:8080/frontend
{"version":"2.0.0","status":"OK"}

Pronto. Agora podemos testar nossa aplicação sem maiores problemas ou implicações.


Notas

Este post foi escrito com a ajuda dos seguintes sites: