NGINX aus der Sicht eines Softwareentwicklers

  • von Autor

Michael Wellner, Softwareentwickler bei sidion

Einleitung

Der NGINX ist ein Open-Source-Webserver mit einem ersten Release im Jahr 2004. Seither wurde er ständig weiter entwickelt und erfreut sich sehr hoher Beliebtheit. Viele größere Tech-Unternehmen wie z.B. Google, Facebook, IBM, WordPress, ... setzen auf den NGINX. Wenn man gewissen Statistiken (Wikipedia, Google Trends, ...) glauben mag, ist er - was die Verwendung angeht - knapp hinter dem Apache HTTP Server. Durch seine Flexibilität kann er auch als Reverse-Proxy oder Load-Balancer und vor allem als HTTPS Einstiegspunkt ins System eingesetzt werden. Es gibt seit 2011 auch eine kostenpflichtige Premiumversion unter dem Namen "NGINX Plus", auf diese möchte ich in diesem Artikel jedoch nicht eingehen. Das Alleinstellungsmerkmal für den NGINX ist eine geringe Speichernutzung durch seinen asynchronen eventgesteuerten Ansatz. Jede Serveranfrage bekommt einen eigenen Thread. Der NGINX ist vor allem für Webseiten mit vielen gleichzeitigen Besuchern (und eventuell kurzer Verweildauer) optimal geeignet. Hier kommt der eventgesteuerte Ansatz zur vollen Entfaltung. Der Apache macht für jede Anfrage einen eigenen Kindprozess auf, während beim NGINX viele Anfragen in einem Kindprozess laufen können. Durch die hohe Parallelität kommt er auch mit geringerer Speichernutzung aus. Möchte man den NXING mit anderen Modulen erweitern, stößt man auf einen Nachteil gegenüber dem Apache, nämlich kann man diese Module nicht dynamisch im laufenden Betrieb hinzufügen, sondern diese müssen schon beim initialen Kompilieren des NGINX vermerkt werden. Selbes gilt übrigens auch für die Änderung von globalen Konfigurationen - auch hier muss der NGINX komplett neu gestartet werden.

Reverse-Proxy / Load-Balancer / Rate-Limiter

Die Verwendung von NGINX kann sehr vielseitig sein. Angefangen vom Ausliefern einer einfachen HTML Seite, bis hin zur Single-Page-Application mit einem modernen Fronten-Framework. Vor allem aber dient er als guter Einstiegspunkt für einen Server, auf dem ein oder mehrere Docker Container laufen. Schwerpunktmäßig wird er dann als Reverse-Proxy oder als Load-Balancer eingesetzt. Außerdem kann er auch einfach als Rate-Limiter konfiguriert werden. Auf alle Ansätze möchte ich hier etwas detaillierter eingehen und anhand von simplen Code-Beispielen erläutern.

Reverse-Proxy

Die Verwendung eines Reverse-Proxy kann mehrere Gründe haben. Zum einen kann er eben ein zentraler Einstiegspunkt für einen Server mit mehreren Apps sein. Die Anfragen werden dann über eine Adresse mittels Pfad weitergeleitet (auch Prinzip eines API Gateways). Beispiel:

  • http(s)://www.meinedomain.de/cms (Weiterleitung an ein Content Management System)
  • http(s)://www.meinedomain.de/blog (Weiterleitung an einen Blog)
  • http(s)://www.meinedomain.de/git (Weiterleitung an eine Versionsverwaltung)

Außerdem können genauso öffentliche IP-Adressen hinter einer einzigen Domain versteckt werden - das ist dann ein Forward-Proxy. Beim Reverse-Proxy kann man auch ohne Upstreams arbeiten (nicht so wie beim Load-Balancer). Details bitte einfach meinem Code-Repository entnehmen.

Load-Balancer

Für das Load-Balancing braucht man für einen Upstream mehrere Instanzen (z.B. mehrere Docker Container einer Anwendung). Load-Balancing selbst bedeutet, dass man die Last einer Anwendung auf eben mehrere Instanzen verteilt. Diese sind intern üblicherweise über verschiedene Ports erreichbar. Der NGINX bietet hier vier Methoden für die Lastverteilung an:

  • Standardmäßig wird "Round-Robin" verwendet. Dies bedeutet, dass die Anfragen gleichmäßig auf alle Instanzen verteilt werden.
  • Gewichtung über mathematische Berechnung wie z.B. 7 & 3, was so viel bedeutet wie: 70% der Anfragen zur ersten Instanz und die anderen 30% der Anfragen zur anderen Instanz.
  • Es wird an die Instanz weiter geleitet, die aktuell am wenigsten aktive Verbindungen hat ("least connected").
  • Außerdem kann man sich für eine IP-Adresse die Instanz merken, dann bekommt diese IP-Adresse für jeden Request dieselbe Instanz zugewiesen. Hier mal ein Code-Beispiel eines Upstreams mit Gewichtung:
upstream with-weights-upstream {
    # use ip adress of the defined docker network gateway
    server 172.99.0.1:8081;
    server 172.99.0.1:8082 weight=7;
    server 172.99.0.1:8083 weight=3;
}

Außerdem können auch noch Health Checks für das Load-Balancing verwendet werden - sprich: Wenn eine Instanz über einen Zeitraum nicht antwortet, bekommt sie erst mal keine Requests mehr weitergeleitet.

Rate-Limiter

Beim Rate-Limiter hat man prinzipiell zwei Möglichkeiten: Entweder man verwendet den $server_name, dann bedeutet das, dass es ein Limit auf dem Server gibt, egal von welcher IP-Adresse die Anfrage kommt. Oder man entscheidet sich für das Gegenteil $binary_remote_addr, was zur Folge hat, dass es pro anfragender IP-Adresse ein Limit gibt. Ein großer Vorteil: Es können mehrere Limits (über so genannte "Zonen") erstellt werden. Damit kann man z.B. speziell auf einem Login ein Limit setzen um die Zahl der eingeloggten User zu begrenzen und gleichzeitig noch ein Limit für z.B. den Download einer Datei setzen. Standardmäßig gibt man NGINX 10MB zum Speichern der binären IP-Adressen für das Rate Limiting. Laut NGINX selbst entspricht das ca. 160.000 IP-Adressen. Wenn das angegebene Limit für einen Zeitraum erreicht wird, werden alle weiteren Anfragen vorerst mit einem 503 Statuscode beantwortet. Beispiel eines Limits: limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s; Zusätzlich kann man dann in einer Location mittels burst noch eine Art Queue für Anfragen aufbauen. Dort können dann Anfragen die das Limit überschreiten entweder gespeichert werden (wartet dann ab, bis wieder ein Request erlaubt wäre) oder ohne Speichern - mittels Angabe von nodelay direkt noch abgearbeitet werden (danach tritt dann aber das Limit in Kraft). Das detaillierte Verhalten kann dann am besten mit einem Request-Tool, welches eine gewisse Anzahl an Request pro Zeitraum abschickt, gut testen.

HTTPS

Ein weiterer Faktor für die Nutzung eines NGINX ist der HTTPS Einstiegspunkt in einen Server. Hat man den Server mal via HTTPS erreicht, kann nachfolgend einfach via HTTP auf die "echten" Applikationen geroutet werden. Es besteht aber auch die Möglichkeit nach innen weiterhin über HTTPS zu routen, dann muss aber auch die Applikation, welche den Request entgegennimmt, mit HTTPS umgehen können. NGINX HTTP Anfragen können ganz einfach auf HTTPS umgeleitet werden:

listen 80;
listen [::]:80;

listen 443 ssl http2;
server_name <<DOMAIN_NAME>>;

# Redirect non-https traffic to https
if ($scheme != "https") {
    return 301 https://$host$request_uri;
}

Ausführliche Code-Beispiele und weitere Properties gibt es genug im Internet. Die Einrichtung von HTTPS erfolgt über ein selfsigned Zertifikat, ein gekauftes Zertifikat oder über Let's encrypt und certbot. Letzteres wird empfohlen. Den certbot braucht man dabei zur automatischen Aktualisierung des Let's encrypt Zertifikats. Hier ist eines zu beachten: Falls man den certbot mit der HTTP-Challenge ausführt (certbot certonly --standalone --preferred-challenges http -d <<DOMAIN_NAME>>), darf beim Erstellen des Zertifikats nichts auf Port 80 laufen. Der certbot wird hier von außerhalb über die angegebene Domain angesprochen. Man kann das aber einfach umgehen, indem man die /.well-known/acme-challenge Anfragen auf einen Upstream weiterleitet. Dort kann man dann einfach den certbot auf einem anderen Port (z.B. 8080) laufen lassen - beim certbot erreicht man das über folgenden Parameter --http-01-port 8000. Den Start des certbots macht man am besten mit einen wiederkehrenden Task (z.B. jede Woche).

Weitere wissenswerte Tipps

Dockerisierung

Bei der Dockerisierung gibt es ein paar Dinge, die man sich zunutze machen kann. Normalerweise arbeitet der NGINX nicht mit Umgebungsvariablen. Wenn man jedoch Templates erstellt, welche man über den Pfad /etc/nginx/templates mounted, dann kann man Umgebungsvariablen verwenden. Ein Beispiel dafür wäre z.B. den nginx Port in einem docker-compose File dynamisch zu konfigurieren. Dies ist auch gut in meinem Beispiel-Repo ersichtlich, da ich jeden NGINX mit einem eigenen Port starten lasse. Man muss aber beachten, dass in diesen Templates kein http {} Block verwendet wird, sondern dass die Zeilen ohne Einrückung bereits diese sind, die in einen http {} kommen.

Docker Upstream Ausfall

Was auch noch zur Dockerisierung zu sagen ist: Nach dem Ausfall eines Upstreams muss normalerweise der Reverse-Proxy oder Load-Balander neu gestartet werden. Will man das verhindern, sollte man explizit ein globales Docker Netzwerk mit Gateway anlegen, alle Container diesem Netzwerk hinzufügen und jeweils einen Resolver konfigurieren. Dann ist ein Upstream nach einen Ausfall wieder erreichbar ohne den Neustart des Reverse-Proxy oder Load-Balancer. Das Vorgehen ist ebenfalls in meinem Beispiel-Repo im README beschrieben.

Streaming

Falls man eine Datenbank hinter dem NGINX hat, kann man diese ebenfalls über den NGINX erreichbar machen. Dafür wird dann aber nicht der http {} Block verwendet, sondern man benötigt einen stream {} Block:

stream {

    upstream mysql-db {
        server localhost:3306;
    }

    server {
        listen <<DOMAIN_NAME>>:<<PORT>> so_keepalive=on;
        proxy_pass mysql-db;
    }

}

Außerdem kann man so auch ssh Verbindungen über den NGINX routen, so wie Video Dateien abspielen.

Datenschutz - IP Logging

Bezüglich Datenschutz kann man das Logging der IP-Adressen anonymisieren. Dazu kann ein eigenes log_format angeben werden, mit welchem man die letzte Zahl der IP-Adresse auf "0" setzt.

map $remote_addr $remote_addr_anonymous {
    ~(?P<ip>\d+\.\d+\.\d+)\.    $ip.0;
    ~(?P<ip>[^:]+:[^:]+):       $ip::;
    default                     0.0.0.0;
}

log_format anonymized '[$time_local] $remote_addr_anonymous "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';

Mock-Server

Außerdem kann man mit dem NGINX auch seinen eigenen Mock-Server basteln, welcher dann JSON Datenstrukturen ausliefert. In folgendem Beispiel wird beim Aufruf von /api/profiles/thomas.maier einfach das JSON <<PFAD>>/thomas.maier/profile.json ausgegeben:

location ~* /api/profiles/(.*)$ {
    default_type application/json; # needed because we directly return html text
    rewrite ^/api/profiles(.*)$ $1/profile.json break;
}

Möchte man eventuell einen Fehler simulieren, lässt sich das JSON auch direkt ausgeben:

location /error {
   default_type application/json; # needed because we directly return html text
   return 404 '{"error":{"code":404, "message":"Not Found"}}';
}

Konfiguration formatieren

Mit der Installation des npm Packages https://www.npmjs.com/package/nginxbeautifier kann man die Konfiguration formatieren lassen.

NGINX Version verstecken

Man kann bei jeder beliebigen Seite über das Response Header Atttribut server einfach einsehen, ob es sich um einen NGINX handelt. Hier wird empfohlen, die Versionsnummer zu deaktivieren, damit potenzielle Hacker keine Informationen über das Alter der laufenden NGINX Instanz erfahren. Dies macht man über server_tokens off; im http {} Block.

Persönliches Fazit

Mich als Softwareentwickler hat vor allem mal interessiert, wie ich einen soliden Weg finde, einen oder mehrere Docker Container auf einem Server mit HTTPS abzusichern. Zusätzlich, wie eingangs beschrieben, bin ich bei einem Projekt mit meinen Azubis mehrmals über eine NGINX Config gestoßen. Ich finde den NGINX super, weil er sehr einfach zu konfigurieren ist. Und egal ob man ihn auf einem Server installiert oder als Alpine Docker Image laufen lässt, er ist sehr leichtgewichtig. Dadurch, dass er auf direktem Wege sowohl plain HTML & JSON ausliefern kann, macht es ihn für die Verwendung eines eigenen Mock-Servers oder zum Testen z.B. eines Gateways nützlich. Die einfache Konfiguration als Reverse-Proxy oder Load-Balancer rundet das Gesamte ab. Ich hab den NGINX auf jeden Fall fest in meinen Werkzeugkasten aufgenommen.

Zurück