Reverse Proxy

La configurazione di Nginx come reverse proxy avviene semplicemente specificando una Location con la direttiva proxy_pass. Ad esempio:

location /mia/location/ {
    proxy_pass http://www.example.com/link/;
}

Quando i client richiederanno al server l'indirizzo /mia/location, Nginx effettuerà una richiesta al server www.example.com, richiedendo la risorsa /link e restituendo il risultato della richiesta al client.

L'intera location viene mappata sul server esterno. Ad esempio, se si richiede al server l'indirizzo /mia/location/sub/pagina.html il server Nginx richiederà al server esterno la risorsa /link/sub/pagina.html.

E' possibile specificare un numero di porta:

location /mia/location/ {
    proxy_pass http://www.example.com:3000/link/;
}

E' possibile inserire headers:

location /mia/location/ {
    proxy_set_header Accept-Encoding "";
    proxy_pass http://www.example.com/link/;
}

Si può usare un buffer per le richieste:

location /mia/location/ {
    proxy_buffers 16 4k;
    proxy_buffer_size 2k;
    proxy_pass http://www.example.com/link/;
}

Si può esplicitamente disabilitare il buffering:

location /mia/location/ {
    proxy_buffering off;
    proxy_pass http://www.example.com/link/;
}

Load Balancer

Nginx può essere configurato per svolgere il ruolo di load balancer in modo molto efficiente. In particolare, nginx supporta tre differenti modalità di load balancing:

  • round-robin - Le richieste vengono distribuite in sequenza ciclica, in base all'ordine di arrivo;
  • least-connected - Ogni richiesta è assegnata al server con meno connessioni attive in quel momento;
  • ip-hash - Una funzione di hash è utilizzata come funzione di mappatura tra l'indirizzo IP del client e l'identificativo del server che gestirà la richiesta.

La direttiva upstream viene utilizzata per indicare gli indirizzi degli application server utilizzati dal reverse proxy.

Ad esempio, supponendo di voler gestire le richieste in arrivo su www.miaapp.com attraverso i server srv1.miaapp.com, srv2.miaapp.com, srv3.miaapp.com, nginx andrebbe configurato come segue:

http {
    upstream myapp1 {
        server srv1.miaapp.com;
        server srv2.miaapp.com;
        server srv3.miaapp.com;
    }
    server {
        listen 80;
        location / {
            proxy_pass http://miaapp.com;
        }
    }
}

Quando la modalità di load balancing non è specificata, nginx utilizza round-robin per default.

Per attivare la modalità least-connected è sufficiente aggiungere la direttiva leat_conn nel gruppo upstream. Ad esempio:

...
    upstream myapp1 {
        least_conn;
        server srv1.miaapp.com;
        server srv2.miaapp.com;
        server srv3.miaapp.com;
    }
...

Persistenza delle Sessioni

Sia nella modalità round-robin che in quella least-connected non vi è garanzia della persistenza delle sessioni. Ricordiamo infatti che HTTP è per sua natura stateless, e pertanto la gestione delle sessioni è sempre a carico del server. In uno scenario in cui è presente un load balancer, è possibile che le richieste in arrivo da uno stesso client e che logicamente appartengono ad una stessa sessione siano distribuite su application server differenti e che pertanto non saranno in grado di riconoscere gli ID o i cookie di sessione generati dagli altri.

Occorre configurare il load balancer in modo tale da assicurarsi che le richieste in arrivo da uno stesso client vengano sempre servite dallo stesso application server. Tale schema è implementato in nginx con la modalità ip-hash.

In ip-hash, l'indirizzo IP del client è utilizzato come chiave di hashing per la determinazione dell'application server da selezionare. Per attivare questa modalità è sufficiente specificare la direttiva ip_hash nel gruppo upstream. Ad esempio:

...
    upstream myapp1 {
        ip_hash;
        server srv1.miaapp.com;
        server srv2.miaapp.com;
        server srv3.miaapp.com;
    }
...

Specifica dei Pesi

Nginx permette di attribuire un peso numerico a ciascun application server indicato nella direttiva upstream. Server con un peso maggiore verranno selezionati più frequentemente di quelli con un peso inferiore. Per default, in assenza di una esplicita indicazione, si assume un peso pari ad 1.

Questo permette di distribuire il carico in maniera più consona rispetto alle reali capacitè di elaborazione degli application server.

Si supponga che l'application server srv1.miaapp.com sia in grado di gestire più richieste rispetto ai server srv2 e srv3. In tal caso, si può assegnare ad srv1 un peso più alto per assicurarsi che esso venga selezionato più spesso dal load balancer. Ad esempio, specificando:

http {
    upstream myapp1 {
        server srv1.miaapp.com weight=3;
        server srv2.miaapp.com;
        server srv3.miaapp.com;
    }
    server {
        listen 80;
        location / {
            proxy_pass http://miaapp.com;
        }
    }
}

Resilienza

Nginx include anche un meccanismo di monitoraggio passivo degli application server. Ciò significa che se un application server non risponde correttamente alle richieste, esso verrà marcato come non funzionante ed nginx non lo selezionerà per le richieste successive.

È possibile controllare il funzionamento di questo schema. In particolare, è possibile specificare il numero di tentativi da effettuare prima di considerare l'application server non funzionante tramite la direttiva max_fails. Per default, questo parametro è impostato ad uno, e ciò comporta che un server venga immediatamente riconosciuto come non funzionante al verificarsi del primo errore.

È inoltre possibile configurare nginx affinchè monitori lo stato dei server non funzionanti, effettuando dei controlli periodici. Il tempo che intercorre tra un controllo ed il successivo viene impostato con la direttiva fail_timeout. Per default, l'intervallo di fail_timeout à impostato a 10 secondi.

Esercizio: Nginx Reverse Proxy e Load Balancer

Preparazione

Creare la directory per l'esercizio:

cd ~/ex
mkdir -p 04net1-ccli-net2-cproxy-2cweb
cd 04net1-ccli-net2-cproxy-2cweb

Preparare lo scaffolding:

mkdir ccli cproxynx cweb
touch ccli/dockerfile docker-compose.yml
tree

Il client ccli rimane quello del precedente esercizio:

vim ccli/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

RUN apk --update add --no-cache openssh tcpdump curl

CMD ["/bin/sleep","1000000"]

Il cproxy sarà un'immagine nginx:alpine. Il server web target rimane un'immagine httpd:2.4-alpine, come in precedenza.

Creaiamo un volume docker per la persistenza della configurazione:

docker volume create nginx-conf

Il docker-compose.yml è:

vim docker-compose.yml
version: '3.6'

services:
  one:
    build: ccli
    image: ccli
    container_name: one
    hostname: one
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.11
  two:
    image: nginx:alpine
    container_name: two
    hostname: two
    cap_add:
      - ALL
    volumes:
      - nginx-conf:/etc/nginx
    networks:
      net1:
        ipv4_address: 192.168.101.10
      net2:
        ipv4_address: 192.168.102.10
  three:
    image: httpd:2.4-alpine
    container_name: three
    hostname: three
    cap_add:
      - ALL
    volumes:
      - three-html:/usr/local/apache2/htdocs/
    networks:
      net2:
        ipv4_address: 192.168.102.12
  four:
    image: httpd:2.4-alpine
    container_name: four
    hostname: four
    cap_add:
      - ALL
    volumes:
      - four-html:/usr/local/apache2/htdocs/
    networks:
      net2:
        ipv4_address: 192.168.102.13
networks:
  net1:
    name: net1
    ipam:
      driver: default
      config:
        - subnet: 192.168.101.0/24
  net2:
    name: net2
    ipam:
      driver: default
      config:
        - subnet: 192.168.102.0/24
volumes:
  nginx-conf:
    external: true
  three-html:
    external: true
  four-html:
    external: true

Partenza del progetto:

docker compose up -d

Collegamento al server nginx:

docker exec -ti two sh

Nella directory di configurazione:

cd /etc/nginx

Disabilitiamo il file di configurazione del sito di default semplicemente cambiandogli nome:

mv conf.d/default.conf conf.d/default.conf.old

Modifichiamo il file principale di configurazione:

vi nginx.conf
worker_processes 5;
user nobody nobody;
error_log /dev/stdout;
events {
  worker_connections 1024;
}
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    upstream bongo {
        #ip_hash;
        server 192.168.102.12;
        server 192.168.102.13;
    }

    server {
        listen 8080;
        root /usr/share/nginx/html;
        access_log /dev/stdout;
        error_log /dev/stdout;
        location / {
                proxy_pass http://bongo/;
        }
    }
}

Testare la configurazione col comando:

nginx -t

Restartare il server col comando:

nginx -s reload

Uscire:

exit

Connettersi a one e provare se il proxy funziona:

docker exec -ti one sh

Ripetere più volte:

curl two:8080

Si vede l'effetto round robin.

Per provare la persistenza di connessione, tornare nella configurazione di two e scommentare il parametro:

        ip_hash;

Riprovando, si nota che il server upstream connesso è sempre lo stesso.