Server

Wie man mehrere Webseiten mit Nginx und HAProxy mit LXD hosten kann

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 stark Gebrauch. 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 mit

sudo 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.

lxd init

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 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
    option   forwardfor
    option   http-server-close
    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 check-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.

Setzt du LXD ein und welche Erfahrungen hast du bisher gemacht?

6 Comments:

  1. Hallo zefanja,
    vielen Dank für den interessanten und informativen Artikel. Mir sind beim Lesen ein paar Fragen gekommen. Ich freue mich, wenn du diese beantworten kannst.

    1. Wie groß sind die Container-Images, die heruntergeladen werden?
    2. Entsprechen diese in der Größe in etwa der Ubuntu-Standard-Installation?

    LG
    Tronde

  2. zefanja

    Hallo Tronde,

    die LXD-Images sind nicht sehr groß, ich glaube so 100-150MB. Sie werden natürlich größer, wenn man in diesem Container Software installiert. Die Images sind deshalb so klein, da nur die wesentliche Software enthalten ist, alles ist sehr minimal. Das ist sehr angenehm, da man so kaum unnötige Pakete im Container hat.

  3. Pedro

    Hallo zefanja,
    ich stehe vor folgendes Problem ich habe alles so eingerichtet wie oben beschrieben.

    Ich nutze einem Dynamischen DNS. Und zwar kann ich dem Port 80 nicht forwarden, also habe ich auf dem port 120 gesetzt.

    Und dann die iptables wie folgt angepasst
    sudo iptables -t nat -I PREROUTING -i eth0 -p TCP -d server_ip/32 –dport 120 -j DNAT –to-destination haproxy_ip:80

    Ich gehe davon aus das der Eintrag schon bei dem DDNS Service eingetragen werden muss oder sehe ich das falsch?

    Hättest du eine Lösung parat?

    Pedro

  4. Pedro

    Sorry das ich nicht alles in einem Beitrag erläutere.

    Ich habe paar Sachen jetzt ausprobiert und zwar,
    ‚exemple.ddns.xx:120’ aufzurufen welches erfolglos ist.

    Und innerhalb des Netzwerkes versucht per IP darauf zu kommen, welches auch fehlschlägt. z.b. 192.168.x.x, 192.168.x.x:80, 192.168.x.x:120.

    Innerhalb des Rechners komme ich auf die Seiten per IP.

    MfG
    Pedro

Leave a Reply:

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert