Uma introdução ao NGINX
Tags: nginx, cache, proxy, loadbalancing
Published: 2022-05-21 12:25:27

---
title: Uma introdução ao NGINX
published: true
description: Entendendo o básico de NGINX usando um exemplo prático
tags: nginx, cache, proxy, loadbalancing
//cover_image: https://direct_url_to_image.jpg
---

Volta e meia leio de algumas pessoas que **NGINX** (engine X) é complicado. Eu não vou mentir: não é algo simples de se entender e usar de primeira. Primeiramente, não é fácil entender qual a finalidade dele. É um servidor, mas o que ele faz? Como é usado?

NGINX é um servidor que pode ser usado de diversas maneiras, tais como: load balancer (balancear requests para os servidores de aplicação), proxy reverso (receber um request da internet e repassar para outro servidor ou serviço) e uma das capacidades mais fantásticas do NGINX, na minha opinião, é sua finalidade como cache.

Uma generosa parte da internet roda sobre NGINX e alguns segmentos bem importantes usam ele como principal servidor, como plataformas de streaming, aplicações de larga escala tais como lojas, redes sociais e, também, é usado como base de muitas CDNs.

## Porque usar NGINX?

Talvez você já tenha se perguntado: porque eu deveria usar NGINX? Não é mais fácil só apontar o DNS para vários IPs?
Isso é possível, mas você não vai ter a robustez de apontar seu DNS para um load balancer que pode balancear seus requests de uma forma mais inteligente, com o apoio de healthcheck caso seus servidores de aplicação não estejam respondendo e de algoritmos de balanceamento, implementando o modelo correto de distribuição de requests.

Se você já compreende o básico e apenas quer olhar uma configuração escrita com containers rodando uma solução, o [repositório de exemplo](https://github.com/mauricioabreu/nginx-intro). Ele está dividido em tags mostrando a evolução das partes do texto.

Vamos criar nossa primeira configuração, bem básica, com apenas uma rota que sempre retorna 200:

```nginx
worker_processes auto;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;

    location / {
      return 200;
    }
  }
}
```

Vamos usar *Docker* com a ajuda do *Docker Compose*:

```yaml
services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "8080:80"
```

Testando nossa rota:

```console
curl -v http://localhost:8080

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 0
< Connection: keep-alive
```

Nossa rota funcionou. Ela retorna 200 como esperado.

Vamos entender o que essa configuração faz, focando apenas no bloco *http*. O bloco http é chamado de contexto, assim como o *events*, e ambos está em um contexto chamado *main*

* **server** - é um bloco designado a escrever as configurações de um servidor dentro da sua configuração. Você pode ter vários deles, cada um atendendo em uma porta diferente. Você pode expor um servidor para o mundo e ter outro interno, sem cache, por exemplo, ou até driblando a autenticação, por exemplo.
* **listen** - aqui você define em qual porta seu servidor vai aceitar as conexões. Note que o *docker-compose.yml* exporta a 80 como 8080 para o host, por isso o request é feito na 8080 (porta 80 do container).
* **location** - é a diretiva usada para definir as rotas. Elas são bem poderosas. Aceitam expressões regulares, é possível capturar as variáveis e usá-las na configuração. O sistema de location, também, conta com diferentes tipos de match.

  * Sem modificador, o match é feito pelo começo da URI
  * **=** é um match exato
  * **~** é um match com expressão regular

Esses são os modificadores que mais usei até hoje.

Nossa location faz match com qualquer URI que comece com **/**, ou seja, qualquer uma como */compras*, */produtos/lista*, etc.

Podemos adicionar uma nova location com match exato.

```nginx
location = /teste {
  return 200 "teste route";
}

location / {
  return 200;
}
```

Agora podemos notar que a rota */teste* tem um body sendo respondido.

```console
curl -v http://localhost:8080/teste

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 11
< Connection: keep-alive
teste route
```

Vimos um pouco do básico da configuração, agora é hora de fazermos algo mais avançado.

## Usando como proxy reverso

Uma definição simplista de proxy reverso é um request que você faz em algum endereço interno, como por exemplo http://meusite.com.br/minhaloja/produtos/1 e não sabendo pra onde exatamente esse request vai ser processado. E, após processado, o servidor de proxy reverso te manda o conteúdo da resposta do servidor de aplicação.

![uma imagem descrevendo um request na internet, passando por nginx como proxy reverso e indo pra aplicação final](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bv0938z74oy6xwkjdh6.png)

Vamos criar uma aplicação de teste que responde na porta 8081, mas que vamos acessar através do nosso NGINX.

A aplicação vai ser em Python usando Flask.

```python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "<p>Index page!</p>"
```

Modificamos nossa configuração do servidor para conter apenas a rota da aplicação. Agora com a adição da diretiva **proxy_pass**, que é usada para fazer do NGINX um servidor de proxy reverso.

```nginx
server {
  listen 80;

  location / {
    proxy_pass http://app:8081;
  }
}
```

Observe que usamos o nome do serviço descrito (*app*) no docker-compose para referenciar o nome que a aplicação atende.

```console
curl -v http://localhost:8080
< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/html; charset=utf-8
< Content-Length: 18
< Connection: keep-alive
<p>Index page!</p>
```

Podemos ver que a resposta possui um header adicional (*Content-Type*), agora, além da resposta que veio da aplicação.

## Como fazer proxy reverso pra múltiplos servidores?

A realidade de uma aplicação em produção, na grande maioria das vezes, é ter mais de um servidor pra servir ela. E isso se deve a inúmeros motivos. Servidores têm recursos finitos (placa de rede, disco, CPU) para atender muitas requisições. E não é apenas sobre isso. E se o servidor sofrer uma falha de hardware? Ou até alguma falha de rede? Inúmeros motivos podem fazer sua aplicação ficar sem um fallback pra esses casos.

É aí que entramos com a diretiva **upstream** do NGINX, usada para definir um conjunto de servidores usados para balancear sua aplicação.

Primeiramente, modificaremos nosso docker-compose para criar duas instâncias da aplicação. Para fins de exemplo, não usaremos a diretiva `scale` porque ela possui um balanceamento round robin, e queremos mostrar como usar a diretiva de upstream com múltiplos servidores de aplicação.

```yaml
app_1:
  build: app
  ports:
    - "8081:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0
app_2:
  build: app
  ports:
    - "8082:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0
```

Vamos modificar nossa configuração para criar um upstream e colocar os dois hostnames da nossa aplicação:

```nginx
upstream app {
  server app_1:8081;
  server app_2:8081;
  keepalive 100;
}

server {
  listen 80;

  location / {
    proxy_pass http://app;
  }
}
```

Definimos um upstream chamado app, e é esse nome que usamos na diretiva proxy_pass. Usar proxy_pass tem algumas vantagens em vez de usar o proxy_pass diretamente com um IP ou hostname:

* Definir diferentes estratégias de balanceamento dos servidores de aplicação (algoritmos de balanceamento diferente com pesos diferentes para cada servidor);
* Usar keepalive, um número inteiro do total de conexões TCP mantidas do NGINX com o servidor, evitando criar conexões TCP a todo request;
* Resolução de DNS é feita quando o servidor sobe. Sem definir um upstream, um nome como google.com no proxy_pass vai gerar uma consulta de DNS por request. Em larga escala isso pode ser um gargalo (até para o DNS interno da sua empresa, caso exista).

A configuração que fizemos vai balancear os request de forma igual, 50% pra cada servidor. Olhando nos logs, podemos ver isso acontecendo na prática:

```
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:48 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:48] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:50 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:50] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:53 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:53] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:55 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:55] "GET / HTTP/1.0" 200 -
```

Agora, vamos dar uma olhada em como funciona o sistema de cache do NGINX.

## Utilizando o NGINX como um servidor de cache

Muita da informação que consumimos não precisa, necessariamente, estar 100% atualizada a todo o instante. Se temos um arquivo estático como um script, uma foto ou um vídeo em nosso site, não precisamos ir buscar ou gerar esse conteúdo a todo momento. Podemos nos utilizar do NGINX como um servidor de cache em alguns passos, fazendo cache de diversos requests da sua aplicação.

Primeiro, vamos adicionar uma rota que trabalha com um parâmetro de query:

```python
@app.route("/hello")
def hello():
    name = request.args.get("name", "nobody")
    return f"<p>Hello, {name}</p>"
```

Agora, vamos modificar o NGINX para ele fazer cache das respostas.

```nginx
worker_processes auto;

events {
  worker_connections 1024;
}

http {
  proxy_cache_path /tmp/cache levels=1:2 keys_zone=app_cache:5m max_size=10m inactive=20m use_temp_path=off;

  upstream app {
    server app_1:8081;
    server app_2:8081;
    keepalive 100;
  }

  server {
    listen 80;

    location / {
      add_header X-Cache-Status $upstream_cache_status;
      proxy_cache app_cache;
      proxy_cache_valid 200 30s;
      proxy_cache_valid 404 1m;
      proxy_cache_key $host$uri$is_args$args;
      proxy_cache_lock on;
      proxy_pass http://app;
    }
  }
}
```

O que significa cada diretiva de proxy adicionada?
* **proxy_cache_path** - descreve onde vamos guardar o cache. Os arquivos de cache são guardados em arquivos.
* **add_header** - adiciona um header *X-Cache-Status* na resposta para que possamos depurar quando aconteceu *MISS*, *HIT*, etc.
  * **HIT** - conteúdo veio diretamente do cache por estar válido, ainda.
  * **MISS** - conteúdo não foi achado no cache e precisou ser buscado na origem.
* **proxy_cache** - zona do cache. A mesma zona pode ser usada em outros lugares da configuração.
* **proxy_cache_valid** - define um tempo de validade do cache para um código HTTP específico. Nossa aplicação não define um header *Cache-Control*, então, configuramos ele direto no NGINX.
* **proxy_cache_key** - parâmetros usados para montar a chave de cache. Essa montagem de valores é extremamente importante para construir um cache seguro e eficiente.
* **proxy_cache_lock** - impede que mais de um request vá na origem para buscar o mesmo conteúdo. Uma diretiva importante para proteger a origem do conteúdo. As conexões esperam até que o cache seja populado.

Conferindo o resultado, podemos fazer uma requisição pra rota nova:

```console
curl -v "http://localhost:8080/hello?name=bento"
```

```
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:51 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.176.4 - - [20/May/2022 22:04:51] "GET /hello?name=bento HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:55 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:57 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:58 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:59 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:06:05 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.176.4 - - [20/May/2022 22:06:05] "GET /hello?name=bento HTTP/1.0" 200 -
```

Observe como o primeiro request bate no NGINX e na app. Os requests em sequência não vão até a app porque eles estão usando o cache que orientamos o servidor a fazer.

Nos últimos dois requests é possível ver que o request foi pra outra instância da app porque passou os 30 segundos de validade que configuramos, garantindo que o cache precisa ser revalidado e que o balanceamento continua funcionando.

## Próximos passos

Ao mesmo tempo que isso é o básico de NGINX é, também, o suficiente para você desbravar mais utilidades pra ele. É possível adicionar muita lógica de aplicação no NGINX que pode ajudar você a construir aplicações mais focadas na regra de negócio em vez de focar em balanceamento, cache, autorização, etc.

NGINX possui suporte a embutir código dinâmicamente, sendo as principais [Lua](https://github.com/openresty/lua-nginx-module) e [Javascript](https://www.nginx.com/products/nginx/modules/nginx-javascript/).