Nginx : entrée en matière

Aujourd'hui, je vous propose de travailler sur la mise en service d'un serveur HTTP. Oui, mais pas forcément (que) Apache. Nginx. Alors pourquoi Nginx ? Et bien cela remonte à a dernière session de BargentoEmile Heitor avait fait une entrée en matière très patriotique, vantant ses vertues de Reverse proxy et de légèreté de fonctionnement par rapport à Apache. Jetons donc un coup d'oeil sur cette bête. Comme je n'ai pas la prétention de tout connaître, je vais arquer le sujet sur plusieures parties : une première approche qui permettra d'aborder la bête de manière raisonnable, et sûrement plusieurs tips sur les configurations aux petits oignons. Mais comme il ne faut jamais tout faire à la fois, commençons d'abord par...

L'installation

Quoique disponible via les dépôts, en version 0.8.17, je vais suivre les bonnes habitudes et l'installer depuis ses sources. Afin d'avoir la possibilité d'utiliser SSL, je recommande d'activer le support OpenSSL.
$ wget http://nginx.org/download/nginx-0.8.53.tar.gz
$ tar zxf nginx-0.8.53.tar.gz && cd nginx-0.8.53
$ ./configure --with-openssl=/usr/lib/openssl
[...]
Configuration summary
+ using system PCRE library
+ using OpenSSL library: /usr/lib/openssl
+ md5: using system crypto library
+ sha1 library is not used
+ using system zlib library

nginx path prefix: "/usr/local/nginx"
nginx binary file: "/usr/local/nginx/sbin/nginx"
nginx configuration prefix: "/usr/local/nginx/conf"
nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
nginx pid file: "/usr/local/nginx/logs/nginx.pid"
nginx error log file: "/usr/local/nginx/logs/error.log"
nginx http access log file: "/usr/local/nginx/logs/access.log"
nginx http client request body temporary files: "client_body_temp"
nginx http proxy temporary files: "proxy_temp"
nginx http fastcgi temporary files: "fastcgi_temp"
nginx http uwsgi temporary files: "uwsgi_temp"
nginx http scgi temporary files: "scgi_temp"

$ make && make install
[...]

$ ll /usr/local/nginx/
total 24
drwxr-xr-x  6 root root 4096 2010-10-29 16:12 .
drwxr-xr-x 27 root root 4096 2010-10-29 16:12 ..
drwxr-xr-x  2 root root 4096 2010-10-29 16:12 conf
drwxr-xr-x  2 root root 4096 2010-10-29 16:12 html
drwxr-xr-x  2 root root 4096 2010-10-29 16:12 logs
drwxr-xr-x  2 root root 4096 2010-10-29 16:12 sbin
Fichiers de configuration, html, binaires et logs, tout est écrit dans /usr/local/nginx/, contrairement à une installation via les dépôts qui m'a semé des fichiers partout, à commencer par le /etc/nginx/.

Les jeux de configuration

Telle quelle, la configuration est viable et le lancement de nginx via /usr/local/sbin/nginx se fait de suite. On peut alors consulter le message de bienvenue sur http://127.0.0.1. Voici le fichier de configuration, tel qu'il est juste après installation.
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
 worker_connections  1024;
}

http {
 include       mime.types;
 default_type  application/octet-stream;

 #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
 #                  '$status $body_bytes_sent "$http_referer" '
 #                  '"$http_user_agent" "$http_x_forwarded_for"';

 #access_log  logs/access.log  main;

 sendfile        on;
 #tcp_nopush     on;

 #keepalive_timeout  0;
 keepalive_timeout  65;

 #gzip  on;

 server {
 listen       80;
 server_name  localhost;

 #charset koi8-r;

 #access_log  logs/host.access.log  main;

 location / {
 root   html;
 index  index.html index.htm;
 }

 #error_page  404              /404.html;

 # Error page redirection on static error pages /50x.html
 #
 error_page   500 502 503 504  /50x.html;
 location = /50x.html {
 root   html;
 }

 # Pass PHP scrips to Apache listening on 127.0.0.1:80
 #
 #location ~ \.php$ {
 #    proxy_pass   http://127.0.0.1;
 #}

 # Pass PHP scripts to FastCGI server listening on 127.0.0.1:9000
 #
 #location ~ \.php$ {
 #    root           html;
 #    fastcgi_pass   127.0.0.1:9000;
 #    fastcgi_index  index.php;
 #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
 #    include        fastcgi_params;
 #}

 # deny access to .htaccess files, if Apache's document root
 # concurs with nginx's one
 #
 #location ~ /\.ht {
 #    deny  all;
 #}
 }

 # another virtual host using mix of IP-, name-, and port-based configuration
 #
 #server {
 #    listen       8000;
 #    listen       somename:8080;
 #    server_name  somename  alias  another.alias;

 #    location / {
 #        root   html;
 #        index  index.html index.htm;
 #    }
 #}

 # HTTPS server
 #
 #server {
 #    listen       443;
 #    server_name  localhost;

 #    ssl                  on;
 #    ssl_certificate      cert.pem;
 #    ssl_certificate_key  cert.key;

 #    ssl_session_timeout  5m;

 #    ssl_protocols  SSLv2 SSLv3 TLSv1;
 #    ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
 #    ssl_prefer_server_ciphers   on;

 #    location / {
 #        root   html;
 #        index  index.html index.htm;
 #    }
 #}

}
On peut voir qu'il est composé de blocs permettant de définir des paramètres au sein d'un périmètre. 3 grands blocs se détachent à prime abord :
  • Le bloc général, contenant les paramètres propres au daemon nginx (utilisateur faisant tourner le daemon, logs, pid, ...)
    #user  nobody;
    worker_processes  1;
    
    #error_log  logs/error.log;
    #error_log  logs/error.log  notice;
    #error_log  logs/error.log  info;
    
    #pid        logs/nginx.pid;
  • Un bloc events, qui gère les paramètres concernant les connexions, et dont on ne s'occupera pas du tout pour le moment
    events {
    worker_connections  1024;
    }
  • Un bloc regroupant les paramètres propres aux flux http. C'est sur ce bloc que vous allez majoritairement travailler, puisqu'il contient notamment des sous blocs servers.
    http {
    include       mime.types;
    default_type  application/octet-stream;
    
    sendfile        on;
    keepalive_timeout  65;
    
    server {
    listen       80;
    server_name  localhost;
    
    location / {
    root   html;
    index  index.html index.htm;
    }
    
    #error_page  404              /404.html;
    
    # redirection des pages d'erreur sur les pages statiques /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   html;
    }
    }
    }
C'est aussi pour cela que nous avons construit depuis les sources : le fichier de configuration est commenté et clair, et propose des exemple de configurations alternatives qui permettent de rapidement prendre en main et mettre en place les services voulus.

Le fichier de configuration est donc établi comme une hiérarchie de directives, qui seront parcourues séquentiellement.

Maintenant qu'on sait à quoi s'en tenir, jouons un peu !

Un exemple de serveur HTTP

La spécification des servers NGinx est très semblable aux VirtualHost d'Apache. Nginx peut parfois donner une impression de plus fine granularité, notamment grâce aux jeux des locations, et la possibilité de créer un log par serveur de manière intuitive. Nous allons nous familiariser avec une configuration classique, qui est donnée par le wiki. Pour plus de lisibilité, je ne reprendrais pas le bloc général, qui ne change pas, mais m'attacherais plus aux sous-blocs server. Par contre, de votre côté, et pour que cela fonctionne, il faut bien évidemment que le fichier soit complet !
server {
listen          80;
server_name     *.domain.com;
rewrite ^       http://www.domain.com$request_uri permanent;
}

server {
listen          80;
server_name     www.domain.com;

index           index.html;
root            /home/domain.com
}
Ici, le premier bloc intercepte toutes les requêtes interrogeant n'importe quel domaine de domain.com sur le port 80. Les requêtes interceptées seront réécrites telles qu'elles interrogeront désormais www.domain.com, toujours sur le même port.

De l'utilité des server_name

Cette configuration est intuitive pour nous, mais elle souligne un point important : pourquoi www.domain.com n'est-il pas compris dans *.domain.com ? Car * signifie en gros tout caractère ou chaîne de caractère. Et bien parce que NGinx est intelligent et validera l'expression la plus spécifique. En réalité, Nginx va s'appuyer sur le Host Header spécifié par le client. Lorsque celui n'est pas renseigné (IP directement saisie, établissement d'une session SSL, ...), il est possible d'orienter tout de même la requête en spécifiant une valeur spéciale en tant que server_name : _

En fait, NGinx évalue et dirige les requête dans un ordre bien particulier :

  • De manière prioritaire, les noms statiques, complets
  • En second, les noms commençant par une * comme notre *.domain.com
  • Ensuite, ceux finissant par une * -logiquement, www.domain.*
  • Enfin, les noms modulés par des expressions régulières.
En toute logique donc, votre serveur sera plus réactif si vous spécifiez correctement les server_name que si vous vous contentez d'un "catch all". Un autre point bon à savoir : si le premier bloc server ne possède pas de directive listen et qu'il n'y a pas de serveur par défaut, alors le serveur par défaut sera automatiquement désigné comme le prochain bloc server ayant une directive listen de spécifiée. Cela peut expliquer pas mal de chose...

rewrite

Rewrite fait partie des instructions NGinx. Comme son nom l'indique, elle réécrit les URL des requêtes, et donc les redirige vers le bloc server adéquat. Elle prend en paramètre une expression régulière, le remplacement voulu et un flag. Attention cependant, la regex ne travaille que sur le chemin relatif, donc notre URI. Elle ne prend donc pas en compte le serveur spécifié dans la requête à réécrire. Le flag, quant à lui, permet de contrôler la fin des processus de réécriture.
  • last : qui effectue la réécriture, après quoi il recherche l'URI et la location correspondante
  • break : qui ne fait que la réécriture
  • redirect : renvoie une redirection temporaire (code 302)
  • permanent : renvoie une redirection permanente (code 301)
La page wiki est ici !

$request_uri

Vous avez sûrement remarqué le très compréhensible $request_uri. Mais mais mais cette variable n'apparaît nulle part ailleurs dans la configuration ! Comment NGinx peut savoir à quoi cela correspond ? NGinx est comme ça. Il met à disposition des variables qui, pour la plupart ne sont pas modifiables, afin de pouvoir être facilement explicite avec ce que l'on veut faire sans pour autant partir from scratch. Pour faire simple, $request_uri correspond à la seconde partie de l'URL originalement requêtée, celle qui commence juste après l'adresse du serveur et qui inclut les arguments. S'il faut un exemple : pour l'URL http://www.domain.com/support/docs/index.php, $request_uri vaudrait /support/docs/index.php. Il est d'ailleurs spécifié dans le wiki que $request_uri ne concerne pas les URI qui ont fait l'objet de réécritures. Ces URI-là seront disponibles via la variable $uri, qui contient l'URI actuellement utilisée. Pour tout savoir, par ici.

Location, substitution et regex sous NGinx

Une regex -une expression régulière si vous préférez-, est un pattern de recherche qui identifiera ou non un ou plusieurs éléments. La regex est tout un art, mais je suppose que beaucoup d'entre vous le connaissent et le manient déjà. Sous NGinx aussi, on a la possibilité d'évaluer des patterns assez complets et, en fonction de leur identification, leur associer des traitements. Les regex peuvent être évaluées partout, y compris sur les location blocs. Lorsque c'est le cas, Nginx adoptera le même comportement que lors de l'évaluation des Host Header et ira chercher la correspondance la plus exacte entre l'URI et les location blocs. Bien entendu, il peut y avoir plusieurs location blocs par server bloc. Par exemple, ici, nous avons un location bloc exprès pour le traitement des fichiers .php.
server {
listen          80 default;
server_name     _;

index           index.html;
root            /var/www/default

# Toute URI commençant par /forum matchera le bloc et passera par un rewrite
location /forum {
# Capture de l'URI et redirection de celle-ci sur le sous-domaine correspondant.
rewrite forum(.*) http://forum.domain.com$1 permanent;
}

location ~* \.php$ {

# exception IF  : if doit toujours être évité, notamment parce que NGinx n'évalue pas en terme de fichier mais en terme de localisation.
# L'exception est là pour combler un trou de sécurité :
# Par exemple, empêcher l'execution de /forum/avatars/user2.jpg sur une requête d'URI /forum/avatars/user2.jpg/index.php
# Une autre solution consiste de manière plus propre à mettre cgi.fix_pathinfo=0 dans le php.ini
# Référence : http://wiki.nginx.org/Pitfalls#Passing_Every_.7E_.5C.php.24_request_to_to_PHP

if (!-f $request_filename) {
return 404;
}

fastcgi_pass 127.0.0.1:9000;
}

}

server {
server_name     forum.domain.com;
listen          80;
index           index.php;
root            /home/domain.com/forum;
}

Cet exemple est également intéressant car il nous montre comment réutiliser un pattern détecté : il suffit d'entourer le pattern de parenthèse et d'y faire référence avec un $1 ! Cela permet d'effectuer des réécritures au niveau des URI demandées, et parfois même de les envoyer sur, au hasard, une adresse en https...

En parlant de HTTPS...

Puisque nous avons activé le support SSL, autant s'en servir ! Avant toute chose, mieux vaut avoir son set certificat SSL / clé prêt. Si vous avez besoin d'un petit coup de pouce, jetez un coup d'oeil par ici ! Voici une petite configuration assez simple et qui est facilement identifiable en tant que configuration HTTPS.
 server {
      access_log  /var/log/nginx/access.log main;
      add_header  Cache-Control public;
      error_log   /var/log/nginx/error.log info;
      expires     90d;
      index       index.html;
      listen      127.0.0.1:443;
      root        /var/www/htdocs;
      server_name  domain.com www.domain.com;

     ## SSL Certs
      ssl on;
      ssl_certificate /ssl_keys/domain.com.crt;
      ssl_certificate_key /ssl_keys/domain.com.key;
      ssl_ciphers HIGH:!ADH:!MD5;
      ssl_prefer_server_ciphers on;
      ssl_protocols TLSv1;
      ssl_session_cache shared:SSL:1m;
      ssl_session_timeout 5m;

     ## Forcer HTTPS
      add_header Strict-Transport-Security "max-age=2592000; includeSubdomains";

     ## Toute autre erreur renvoie une page d'erreur générique
      error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
                 500 501 502 503 504 505 /example_error_page.html;
      location  /example_error_page.html {
          internal;
      }
  }
}
Ce qui change, par rapport à notre serveur HTTP, c'est le port sur lequel écoute le serveur et cette section SSL. Dans cette section, le SSL est explicitement activé et tous les éléments relatifs aux certificats et à l'encryptage sont stipulés. Et si l'on veut ne passer que par ce serveur, il ne reste plus qu'à réécrire les requêtes adressées à notre serveur HTTP simple et botter en touche sur la sollicitation d'autres server_name.
# HTTP redirigé sur HTTPS
server {
      add_header  Cache-Control public;
      access_log  /var/log/nginx/access.log main;
      error_log   /var/log/nginx/error.log info;
      expires     90d;
      listen      127.0.0.1:80;
      root        /var/empty;
      server_name domain.com www.domain.com;

      location / {
        if ($host ~* ^(domain\.com|www\.domain\.com)$ ) {
          rewrite  ^/(.*)$  https://domain.com/$1  permanent;
        }
        return 444;
      }
  }

# default server
server {
server_name  _;  #default
return 444;
}

Reverse proxy ?

C'est bien beau tout ça, mais avant d'être un serveur HTTP, NGinx est avant tout un Reverse Proxy. Un reverse proxy est, comme son nom l'indique, un serveur proxy, mais qui fonctionne en inverse d'un proxy habituel. En effet, un server proxy permet aux utilisateurs interne à un réseau d'accéder à des services externes à ce réseau. Et bien un reverse proxy, c'est un serveur qui permet à des entités externes au réseau d'accéder aux services de ce réseau. Cela implique que les reverse proxy doivent être mis devant les serveurs ciblés, typiquement une ferme de serveurs Web, afin de pouvoir router correctement les flux. Cela permet également de gérer un minimum de sécurité et de dispatcher la charge sur différents serveurs en cas de besoin. Un petit exemple de reverse proxying :
http {
[...]
## Proxy options
  proxy_buffering           on;
  proxy_cache_min_uses       3;
  proxy_cache_path          /usr/local/nginx/proxy_temp/ levels=1:2 keys_zone=cache:10m inactive=10m max_size=1000M;
  proxy_cache_valid         any 10m;
  proxy_ignore_client_abort off;
  proxy_intercept_errors    on;
  proxy_next_upstream       error timeout invalid_header;
  proxy_redirect            off;
  proxy_set_header          X-Forwarded-For $remote_addr;
  proxy_connect_timeout     60;
  proxy_send_timeout        60;
  proxy_read_timeout        60;

 ## Backend serveurs (webserver1 est le serveur primaire et webserver2 son backup dans le cas où webserver1 est down)
    upstream webbackend  {
      server webserver1.domain.lan weight=10 max_fails=3 fail_timeout=30s;
      server webserver2.domain.lan weight=1 backup;
    }

  server {
      add_header  Cache-Control public;
      access_log  /var/log/nginx/access.log main;
      error_log   /var/log/nginx/error.log;
      index       index.html;
      limit_conn  gulag 50;
      listen      127.0.0.1:80 default;
      root        /usr/local/nginx/html;
      server_name _;

     ## On ne traite que les requêtes s'adressant à domain.com
     ## les autres sont bottées en 444
      if ($host !~ ^(domain.com|www.domain.com)$ ) {
         return 444;
      }

     ## Seuls les chemin complets depuis la racine sont traités.
     ## Si on veut juste référencer les noms de fichiers, utiliser $request_filename au lieu de $request_uri
      location / {
        if ($request_uri ~* (^\/|\.html|\.jpg|\.pl|\.png|\.css|\.ico|robots\.txt)$ ) {
          break;
        }
        return 444;
      }

     ## PROXY - Forum
      location /forum/ {
        proxy_pass http://forum.domain.lan/forum/;
      }

     ## PROXY - Data
      location /files/ {
        proxy_pass http://data.domain.lan/;
      }

     ## PROXY - Web
      location / {
        proxy_pass  http://webbackend;
        proxy_cache            cache;
        proxy_cache_valid      200 24h;
        proxy_cache_use_stale  error timeout invalid_header updating http_500 http_502 http_503 http_504;
        proxy_ignore_headers   Expires Cache-Control;
      }

     ## Toute autre erreur renvoie la page d'erreur générique
      error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
                 500 501 502 503 504 505 506 507 /error_page.html;
      location  /error_page.html {
          internal;
      }
  }
}
On voit bien dans cet exemple que le reverse proxying s'arque sur 2 points : les paramétrages généraux, qui s'appliqueront à toutes les requêtes, et les proxy_pass, qui aiguilleront les URI matchant une location en particulier sur des serveurs en backend. Pour les curieux, jetez un coup d'oeil ici pour avoir la liste des options et leurs rôles.

Conclusion

Finalement, pour une première approche, ce n'est pas très douloureux, on a vu pire ! Et oui, le but de NGinx n'est pas de vous compliquer la tâche, mais plutôt de vous rendre la vie plus simple. D'ailleurs, NGinx ne se limite pas à ces 4 lignes, loin de là ! Mais la suite sera pour plus tard :) Si cela vous a donné envie de titiller un peu la bête, n'hésitez pas à faire partager vos retours d'expérience ! Enjoy !
Vus : 2786
Publié par K-Tux : 59