Wie man mehrere Webseiten mit Nginx und HAProxy mit LXD hosten kann
By zefanja
LXD und Linuxcontainer sind eine meiner Lieblingstechnologien in Ubuntu. In wenigen Sekunden kann man einen neue virtuelle Maschine in einem Container starten. Wir machen davon in [unserer Schule][1] [stark Gebrauch][2]. Fast alle Webanwendungen laufen in einem solchen Container. So haben wir die einzelnen Anwendungen besser getrennt. Weiterhin kann man sehr schnell einen Snapshot eines Containers machen und vieles mehr. Wenn man nun diese verschiedenen Webseiten öffentlich zugänglich machen möchte, gibt es ein Problem, denn i.d.R. hat man nur eine öffentliche IP zur Verfügung (fest bzw. dynamisch). Eine Lösung wäre, dass die Webanwendungen auf verschiedenen Ports laufen, aber das ist nicht unbedingt benutzerfreundlich. In diesem Beitrag möchte ich zeigen, wie man mehrere Webseiten mit Nginx und HAProxy mit LXD hosten kann.
Voraussetzungen
Wir brauchen:
- einen Server (ab Ubuntu 16.04) mit öffentlicher IP
- Zugang mit Rootrechten (root / sudo)
- eine Domain und Subdomain, die mit je einem DNS A Eintrag auf die öffentliche IP des Servers zeigen
Schritt 1 – Benutzer der lxd – Gruppe hinzufügen
Damit man mit einem Nicht-Root-Benutzer lxd verwalten kann, müssen wir zuerst den Benutzer der Gruppe hinzufügen:
sudo usermod --append --groups lxd username
``` Damit die Änderungen wirksam werden, müssen wir uns einmal _aus- und wieder einloggen_! ### Schritt 2 – lxd konfigurieren lxd ist in Ubuntu 16.04 vorinstalliert. Falls nicht, können wir es mit ``` sudo snap install lxd ``` nachholen. In LXD gibt es verschiedene Möglichkeiten, wie und wo die Container ihren Speicherplatz haben. Man kann verschiedene Dateisysteme auswählen und entscheiden, ob die Container in einer Datei oder extra Partition bzw. Festplatte gespeichert werden. In diesem Tutorial verwenden wir ZFS für Linux mit einen sogenannten „loop device“. Das installieren wir mitsudo apt-get install zfsutils-linux
``` Nun können wir mit der Einrichtung von lxd beginnen:sudo lxd init
``` Es werden verschiedene Fragen gestellt, die man wie folgt beantworten kann (Achtung: Die Netzwerkbrücke nur für IPv4 aktivieren! Das Subnet kann frei gewählt werden) ``` Name of the storage backend to use (dir or zfs) [default=zfs]: zfs Create a new ZFS pool (yes/no) [default=yes]? <span style="color: yes Name of the new ZFS pool [default=lxd]: lxd Would you like to use an existing block device (yes/no) [default=no]? no Size in GB of the new loop device (1GB minimum) [default=15]: 15 Would you like LXD to be available over the network (yes/no) [default=no]? no Do you want to configure the LXD bridge (yes/no) [default=yes]? yes LXD has been successfully configured. ``` Die Netzwerkbrücke ist notwendig, damit jeder Container seine eigene IP bekommt und Zugang zum Internet hat. So, jetzt ist alles eingerichtet und wir können unsere ersten Container erstellen! ### Schritt 3 – Container erstellen Um unsere Container zu verwalten, brauchen wir den `lxc` Befehl. Diese feine Programm ist sehr einfach zu verwenden und dabei sehr mächtig. Zuerst wollen wir uns mal alle Container anzeigen lassen. Das geht mit `lxc list`:$ lxc list +------+-------+------+------+------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +------+-------+------+------+------+-----------+
``` Es werden keine Container angezeigt, weil wir ja noch keine erstellt haben. Für unser Beispiel brauchen wir drei Container: einen für HAProxy und 2 Webserver mit Nginx (oder Apache, wenn das jemand bevorzugt). Wir werden den Befehl `lxc launch` verwenden, um einen Ubuntu 16.04 (`ubuntu:x`) Container namens `web1` zu erstellen und zu starten. Das `x` in `ubuntu:x` ist eine Abkürzung für den ersten Buchstaben von Xenial, der Codename von Ubuntu 16.04. `ubuntu:` ist der Bezeichner für das vorkonfigurierte Repository von LXD-Images. Unsere drei Container können wir also mit folgenden Befehlen erstellen: ``` $ lxc launch ubuntu:x web1 $ lxc launch ubuntu:x web2 $ lxc launch ubuntu:x haproxy ``` Wenn man den ersten Container erstellt, wird zuerst das Image heruntergeladen, dass etwas dauern kann. Die anderen beiden Container werden dann sehr schnell erstellt. Nun können wir mit lxc list schauen, ob unsere Container auch alle da sind: ``` $ lxc list +---------+---------+-----------------------+------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +---------+---------+-----------------------+------+------------+-----------+ | haproxy | RUNNING | 10.10.10.10 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ | web1 | RUNNING | 10.10.10.100 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ | web2 | RUNNING | 10.10.10.200 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ ``` In dieser Liste sehen wir zuerste den Namen des Containers und seinen Status (RUNNING, STOPPED). Dann die IP Adresse(n) (die hier exemplarisch ist) und der Typ. Zum Schlusso noch die Anzahl der Snapshots / Sicherungspunkte. Die IP-Adressen werden wir gleich brauchen (am besten notieren). ### Schritt 4 – Nginx-Container einrichten Um eine Verbindung zum Container herzustellen, verwenden wir den Befehl `lxc exec`, der den Namen des Containers und die auszuführenden Befehle enthält. ``` $ lxc exec web1 -- sudo --login --user ubuntu ``` Die Zeichenkette `--` gibt an, dass die Befehlsparameter für lxc dort zu Ende sind, und der Rest der Zeile wird als der auszuführende Befehl innerhalb des Containers übergeben. Der Befehl ist `sudo --login --user ubuntu`, der eine Login-Shell für das vorkonfigurierte Konto `ubuntu` im Container bereitstellt. > Hinweis: Wenn man eine Verbindung zu den Containern als _root_ herstellen müssen, kann man stattdessen den Befehl `lxc exec web1 -- /bin/bash` verwenden Nun sind wir in der Konsole des Containers (_ubuntu@web1_) und können nginx installieren: ``` $ sudo apt update $ sudo apt install nginx ``` Damit wir später besser erkennen auf welchem Webserver wir uns gerade befinden, ändern wir die Standard-Seite des Webservers: ``` $ sudo nano /var/www/html/index.nginx-debian.html ``` Für die Änderung bieten sich der _title_-Tag und die erste Überschrift (_h1_) an: ``` <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web1!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx on LXD container web1!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> ``` Nachdem wir die Datei an den beiden Stellen geändert haben, können wir mit **Strg+X** und **Y** die Datei speichern. Mit **Strg+D** oder `logout` verlassen wir wieder den Container. Zurück auf dem Server testen wir, ob der Webserver auch ordnungsgemäß funktioniert. Mit dem folgenden Befehl sollten wir die Willkommenseite sehen, die wir gerade eben angepasst haben: ``` $ curl http://10.10.10.100/ ``` Wenn alles geklappt hat, kann der zweite Container nach dem gleichen Muster eingerichtet werden. ### Schritt 5 – HAProxy konfigurieren Jetzt, da alle beiden Webserver-Container eingerichtet sind, fehlt nur noch der Reverse Proxy. Er ist dafür verantwortlich, dass die Anfragen von außen vom richtigen Container verarbeitet werden. [HAProxy][3] ist ein sehr leistungsstarker Proxy, der auch von Gihub oder Twitter verwendet wird. Die Möglichkeiten sind auch hier wieder sehr vielfältig. Wir werden deshalb nur ein sehr einfaches Setup machen. Zuerst loggen wir uns wieder in den Container ein und installieren HAProxy: ``` $ lxc exec haproxy -- sudo --login --user ubuntu $ sudo apt-get update $ sudo apt-get install haproxy ``` Nun konfigurieren wir HAProxy. Dazu öffnen wir die Datei _**/etc/haproxy/haproxy.conf**_ mit einem Texteditor:$ sudo nano /etc/haproxy/haproxy.cfg
``` Im Abschnitt `defaults` fügen wir die Optionen _forwardfor_ und _http-server-close_ hinzu.global ... defaults log global mode http option httplog option dontlognull <span class="highlight">option forwardfor</span> <span class="highlight">option http-server-close</span> timeout connect 5000 timeout client 50000 timeout server 50000 ...
``` #### Frontends Als nächstes konfigurieren wir das Frontend so, dass es auf unsere beiden Backend-Container zeigt. Wir fügen einen neuen Frontend-Abschnitt mit dem Namen _wwww_frontend_ hinzu, der so aussieht: ``` frontend www_frontend bind *:80 # Port 80 (www) an den Container binden # Es gibt eine Übereinstimmung sobald das HTTP Host: field einen der Hostnamen enthält (nach dem '-i'). acl host_web1 hdr(host) -i example.com www.example.com acl host_web2 hdr(host) -i web2.example.com # Leite die Verbindung zum richtigen Servercluster um, je nach Übereinstimmung. use_backend web1_cluster if host_web1 use_backend web2_cluster if host_web2 ``` Die _acl-Befehle_ stimmen mit den Hostnamen der Webserver überein und leiten die Anfragen an den entsprechenden Backend-Abschnitt weiter. #### Backends Dann definieren wir zwei neue Backend-Abschnitte, eine für jeden Webserver, und nennen sie _web1_cluster_ bzw. _web2_cluster_. Wir fügen den folgenden Code zur Datei hinzu, um die Backends zu definieren: ``` backend web1_cluster balance leastconn # Wir setzen den X-Client-IP HTTP-Header. Dies ist nützlich, wenn wir wollen, dass der Webserver die tatsächliche Client-IP kennt. http-request set-header X-Client-IP %[src] # Dieses Backend, hier "web1" genannt, verweist auf den Container "web1.lxd" (Hostname). server web1 web1.lxd:80 check backend web2_cluster balance leastconn http-request set-header X-Client-IP %[src] server web2 web2.lxd:80 check ``` Die _balance_-Option bezeichnet die Load-Balancing-Strategie. In diesem Fall entscheiden wir uns für die geringste Anzahl von Verbindungen. Die Option _http-request_ setzt einen HTTP-Header mit der realen Web-Client-IP. Wenn wir diesen Header nicht setzen würden, würde der Webserver die HAProxy-IP-Adresse als Quell-IP für alle Verbindungen aufzeichnen, was die Analyse der Herkunft des Datenverkehrs erschwert. Die Server-Option spezifiziert einen beliebigen Namen für den Server (web1), gefolgt von Hostname und Port des Servers. LXD stellt einen DNS-Server für die Container zur Verfügung, so dass web1.lxd sich auf die mit dem web1-Container verknüpfte IP auflöst. Die anderen Container haben eigene Hostnamen, wie z.B. web2.lxd und haproxy.lxd. Der c_heck_-Parameter weist HAPRoxy an, auf dem Webserver Überprüfungen durchzuführen, um sicherzustellen, dass er verfügbar ist. Um zu testen, ob die Konfiguration gültig ist, führen Sie den folgenden Befehl aus: ``` $ haproxy -f /etc/haproxy/haproxy.cfg -c Configuration file is valid ``` Nun müssen wir noch den HAProxy neu laden: ``` $ sudo systemctl reload haproxy ``` Damit sind wir im HAProxy-Container fertig und können uns wieder mit **Strg+D** bzw. **logout** abmelden. Wir haben HAProxy so konfiguriert, dass es als Reverse-Proxy fungiert, der alle Verbindungen, die es auf Port 80 empfängt, an den entsprechenden Webserver in den beiden anderen Containern weiterleitet. Testen wir, ob es Haproxy tatsächlich gelingt, die Anfragen an den richtigen Web-Container weiterzuleiten. Dazu führen wir den folgenden Befehl aus: ``` $ curl --verbose --header 'Host: web2.example.com' http://10.10.10.10 ``` Dies stellt eine Anfrage an HAProxy und setzt einen HTTP-Host-Header, den HAProxy verwenden soll, um die Verbindung zum entsprechenden Webserver umzuleiten. Als Ausgabe sollten wir nun die Standarseite des Nginx-Servers vom web2-Container erhalten! HAProxy hat die Anfrage richtig verstanden und an den web2-Container weitergeleitet. Dort zeigt der Webserver der Standard-Indexseite an, die wir zuvor bearbeitet haben, und zeigt den Text auf dem LXD-Container web2 an. ### Schritt 6 – Eingehende Verbindungen an den HAProxy weiterleiten Zum Schluss müssen wir noch dafür sorgen, dass alle externen Anfragen auf Port 80 an HAProxy weitergeleitet werden, damit die Welt auf unsere Websites zugreifen kann. HAProxy haben wir in einem Container installiert und ist standardmäßig aus dem Internet nicht erreichbar. Um dies zu lösen, erstellen wir eine iptables-Regel, um Verbindungen weiterzuleiten. Der Befehl iptables benötigt zwei IP-Adressen: die öffentliche IP-Adresse des Servers (server\_ip) und die private IP-Adresse des Haproxy-Containers (haproxy\_ip), die man mit dem Befehl `lxc list` erhält. Führen Sie diesen Befehl aus, um die Regel zu erstellen: ``` $ sudo iptables -t nat -I PREROUTING -i eth0 -p TCP -d server_ip/32 --dport 80 -j DNAT --to-destination haproxy_ip:80 ``` Um diesen iptables-Befehl zu speichern, damit er nach einem Neustart erneut angewendet wird, installieren wir das Paket _iptables-persistent_: ``` $ sudo apt-get install iptables-persistent ``` Bei der Installation des Pakets werden wir aufgefordert, die aktuellen iptables-Regeln zu speichern. Akzeptieren und speichern wir alle aktuellen iptables-Regeln. ### Schritt 7 – Zugriff von außen testen Wenn wir die beiden Domains (DNS A Eintrag) eingerichtet haben, sollten wir in der Lage sein, sich mit Ihrem Webbrowser mit jeder Website zu verbinden. Alternativ und nur zum lokalem Test können wir auch einen Eintrag in der **_/etc/hosts_** Datei machen: ``` server_IP example.com server_IP web2.example.com ``` Um zu testen, ob die beiden Webserver tatsächlich über das Internet erreichbar sind, greifen wir von einem lokalen Computer aus mit einem Browser auf unsere beiden Domains zu (example.com bzw. web2.example.com). In beiden Fällen sollten wir nun die entsprechende Standardseite des Webservers sehen! ### Fazit Wir haben zwei Websites eingerichtet, jede in einem eigenen Container, wobei HAProxy den Datenverkehr steuert. Diesen Prozess können wir wiederholen, um viele weitere Websites zu konfigurieren, die jeweils auf einen eigenen Container beschränkt sind. So könnten wir z.B. auch MySQL in einem neuen Container hinzufügen und dann ein CMS wie WordPress installieren, um eine Website zu starten. Weiterhin könnten wir diese Vorgehensweise auch verwenden, um ältere Softwareversionen zu unterstützen. Zum Beispiel, wenn eine Installation eines CMS eine ältere Version von Software wie PHP5 erfordert, dann können wir Ubuntu 14.04 in den Container installieren (`lxc launch ubuntu:t`), anstatt zu versuchen, die Paketmanager-Versionen, die auf Ubuntu 16.04 verfügbar sind, herabzusetzen. Schließlich bietet LXD die Möglichkeit, Snapshots des vollen Zustands von Containern zu machen, was die Erstellung von Backups und das Zurückrollen von Containern zu einem späteren Zeitpunkt vereinfacht. Darüber hinaus, wenn wir LXD auf zwei verschiedenen Servern installieren, dann ist es möglich, diese miteinander zu verbinden und Container zwischen Servern über das Internet zu migrieren. Doch darüber berichte ich vielleicht ein andern mal… **Update:** [Als nächsten Schritt sollte man das Setup noch mit SSL sicherer machen.][4]Setzt du LXD ein und welche Erfahrungen hast du bisher gemacht?
[1]: https://zefanjas.de/5-grossartige-open-source-programme-die-wir-in-unserer-schule-einsetzen/ [2]: https://zefanjas.de/open-source-in-der-schul-it-teil-2/ [3]: http://www.haproxy.org/ [4]: https://zefanjas.de/haproxy-nginx-lxd-und-lets-encrypt/