Migration meines vServer zu Traefik 2

Es ist nun schon einige Zeit her, dass eine neue Major-Version von Traefik erschienen ist. Mehr dazu in diesen Blog-Post von contanious. Anfangs war ich skeptisch, was die Breaking-Changes und die Verabschiedung von alten Konzepten, betrifft, die bis dato gut funktioniert haben. Zum Thema "gut funktionieren" später mehr. Die Migration an sich gestaltete sich gesamt doch recht einfach.

Für alle die bei Traefik nur Bahnhof verstehen ...

Für wen Traefik komplett fremd ist, dem rate ich sich mal die groben Konzepte näher anzuschauen und dann evtl. wiederzukommen, da hier auch einige Beispiele auftauchen werden, die so in der offiziellen Doku nicht unbedingt so leicht zu finden sind.

Kontext & Vorbereitungen

Der Kontext der Migration war mein eigener kleiner vServer, auf dem ich diverse Docker-Container für mich und diverse Projekte betreibe. Nachdem ich hierbei keine zweite Kiste für die Vermeidung von Downtimes habe, musste die Migration am offenen Herzen erfolgen. Nach einigen lokalen Tests stellte sich das als relativ problemlos heraus. Die entsprechende Konfiguration kann durch die neue Web-UI recht gut validiert werden, und so ohne wirkliches Umschalten sichtbar. Die Tags sind hierbei so stark modifiziert worden, dass sie parallel an die Container gehängt werden können.

Neue Weboberfläche - Neues Glück

Von der neuen Web-UI, die von den Traefik-Machern auch entsprechend angepriesen wurde, bin ich begeistet. Sie bietet eine sehr bequeme und einfache Oberfläche, die deutlich mehr Informationen bereitstellt als der Vorgänger.

Um die entsprechenden Konfigurationen und Services wieder zu finden, gibt es hier einen neuen Menüpunkt unter HTTP. TCP ist für die Migration erst einmal zweitrangig gewesen, da hier kein Bedarf für mich besteht.

Die Routers-Page im Überblick

Wie oberhalb zu sehen findet sich eine Übersicht mit den Entrypoints, sowie den entsprechenden Rules. Das liefert schnell eine Übersicht ob die Konfiguration valide und somit auch grundsätzlich funktional ist. Mehr zu den Infos später.

Durch einen Klick auf die entsprechende Detail-Ansicht kommt man in die Details zum Routing. Beispielsweise mit meinem Blog, der eine Middleware zur Umgehung des HTTP(S)-Redirect konfiguriert hat:

Blog in der Detail-Ansicht

Man erkennt recht schnell die eigentliche Konfiguration und kann auf einen Blick den "Weg" eines Routing nachvolziehen und auch noch einmal verifizieren, dass die gewünschte Middleware auch passend konfiguriert ist.

Alles in allem war die Weboberfläche doch ein sehr großer Schritt in die richtige Richtung. Einziges Manko: die entsprechende integrierte Visualisierung für die Metriken ist verschwunden, mehr dazu später.

Von Frontends- und Backends zu Router, Middleware und Service

Der relativ primitive Ansatz mit Frontends und Backends fand ich zu Beginn sehr angenehm. Rückblickend muss ich allerdings sagen, dass das neue Konzept mit der Trennung in drei Layer deutlich verständlicher und einfacher ist. Zudem sorgt das ganze subjektiv für einen Tick bessere Performance.

Diese Trennung macht das Ganze auch deutlich leichter les- und konfigurierbar. So hat der Blog folgende docker-compose-Konfiguration mit Traefik 1.x:

labels:
  - "traefik.port=2368"
  - "traefik.backend=blog"
  - "traefik.enable=true"
  - "traefik.frontend.entryPoints=http"
  - "traefik.frontend.rule=Host:blog.timo-reymann.de"
docker-compose.yml Traefik 1.7 für den Blog

Mit Traefik 2 sieht das Ganze dann so aus:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.blog.rule=Host(`blog.timo-reymann.de`)"
  - "traefik.http.services.blog.loadbalancer.server.port=2368"
  - "traefik.http.middlewares.blog-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
  - "traefik.http.routers.blog.middlewares=blog-headers"
  - "traefik.http.routers.blog.entrypoints=web"
docker-compose.yml Traefik 2 für den Blog

Die Angabe des Backend entfällt dabei nicht, sondern wandert mehr in die Routers-Sektion, das macht das ganze zwar zu mehr Text aber dafür deklarativer. Die Angabes des X-Forwarded-Header wird mit Traefik 2 zwingend notwendig, da hier sonst auf Port 80 bzw. http die URLs erzeugt werden, was dann zu unschönend Mixed-Content-Warnings im Browser führt. Das betrifft hierbei grundsätzlich nur Installationen mit SSL-Termination. In meinem Falle ist der Load-Balancer via HTTP erreichbar und wird von CloudFlare entsprechend mit SSL versorgt.

HTTP-Port und Services

Wie beim Blog oben gezeigt, musste ich bei einigen Anwendungen, die nicht standardmäßig ihre HTTP-Daten via Port 80 schicken, im Service umsetzen, das geht relativ schmerzfrei mit folgenden Label:

traefik.http.services.<serviceName>.loadbalancer.server.port=<httpPort>
Label für Non-Standard-Ports in der docker-compose

Bei allen Containern, die Port 80 standardmäßig freigeben, ist die Konfiguration des Ports dabei komplett überflüssig und kann mit wenigen Zeilen erfolgen:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.timo_reymann_de.rule=Host(`timo-reymann.de`) || Host(`www.timo-reymann.de`)"
  - "traefik.http.routers.timo_reymann_de.entrypoints=web"
docker-compose Labels am Beispiel meiner Homepage

Der Entrypoint ist hierbei der Port, den Traefik verwendet um Traffic engegenzunehmen und ist deshalb nicht vom Container und dessen Ports abhängig. Das hat mich zu Beginn etwas verwirrt, war aber nach dem dritten Container, den ich umgestellt habe, dann sehr intuitiv ;)

Mailcow

Für meinen eigenen Mail-Server setze ich schon vor einiger Zeit auf mailcow. Ein Projekt, mit dem Ziel ein fertiges Mail-System mit Docker zu betreiben. Hierbei ist auch SoGo und eine Weboberfläche zur Adminsitrierung inkludiert. An dieser Stelle nutze ich auch den eingebauten Exchange-Part von SoGo, hierbei müssen einige (Sub)domains geroutet werden, was für verschiedene Domains gerne sehr langgezogen wird.

Mit Traefik 1.7 gibt es an dieser Stelle bereits ein HostRegexp, das aber nur mit der Subdomain funktioniert hatte. Mit dem neuen Release gibt es jetzt auch Support die ganze Domain via Wildcard verfügbar zu machen, so wird aus einer Liste von Subdomains, folgendes:

version: '2.1'

services:
    nginx-mailcow:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.mailcow.rule=HostRegexp(`{host:(autodiscover|autoconfig|webmail|mail|email).+}`) || Host(`beta.timo-reymann.de`) || Host(`mailcow.timo-reymann.de`)"
        - "traefik.http.routers.mailcow.entrypoints=web"
        - "traefik.http.services.mailcow.loadbalancer.server.port=1080"
      networks:
        - gateway

networks:
  gateway:
    external:
      name: gateway
docker-compose.override.yml für Mailcow

Jetzt müssen für neue Mail-Domains nur noch die entsprechenden DNS-Einträge gesetzt werden und schon kann es los gehen. Ein sehr hoher Komfort der auch die Wartung erheblich vereinfacht.

Migration von Keycloak

Achtung an der Stelle, wenn Keycloak hinter Traefik betrieben wird: Mit Traefik 2 gibt es hier einige Header die gesetzt werden müssen:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.keycloak.rule=Host(`keycloak.timo-reymann.de`)"
  - "traefik.http.routers.keycloak.entrypoints=web"
  - "traefik.http.services.keycloak.loadbalancer.server.port=8080"
  - "traefik.http.middlewares.keycloak-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
  - "traefik.http.middlewares.keycloak-headers.headers.customrequestheaders.X-Forwarded-For=https"
  - "traefik.http.middlewares.keycloak-headers.headers.customrequestheaders.X-Forwarded-Port=443"
  - "traefik.http.routers.keycloak.middlewares=keycloak-headers"
docker-compose Label für CatchAll

Diese Vielzahl an Headern ist nach meiner Erfahrung leider notwendig. Keycloak ist hierbei anscheinend aufgrund diverser Faktoren darauf angewiesen. Mit Traefik 1.7 war das Ganze nicht notwendig.

ACHTUNG an dieser Stelle, das neue Traefik arbeitet bei der Priorität nach Länge der Regel, also wird entsprechend die Regel mit den meisten Zeichen höher priorisiert. Um eine evtl. Kollision zu vermeiden ist es zu empfehlen die Priorität relativ niedrig, wie hier auf 1 zu setzen, das vermeidet Collisions.

CatchAll

Um fehlende Mappings oder vorrübergehende Downtime durch das Update eines Containers überbrücken zu können, eigenet sich ein "CatchAll". Unter Traefik 1.7 war das mit einem entsprechenden Label möglich. Bei Traefik 2 gibt es so etwas zwar offiziell nicht mehr, man kann hier jedoch mit Regex genau dasselbe erreichen:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.not_found.rule=HostRegexp(`{host:.+}`)"
  - "traefik.http.routers.not_found.entrypoints=web"
  - "traefik.http.routers.not_found.priority=1"
Labels für Keycloak in der docker-compose

Migration von zwei Containern mit minimaler Downtime

Das Update an sich fand wie bereits erwähnt am offenen Herzen statt. Am besten funktioniert das Ganze mit einem zweiten Container mit der neuen Traefik-Version, der entsprechend auf einem anderen Port erreichbar ist. Dann kann Container für Container mit den neuen Labeln versorgt werden. Eine kurze Downtime pro Container ist dabei nicht zu vermeiden, ist jedoch für dieses Setup die beste Lösung.

Wenn dann alle Container mit den neuen Labeln versorgt sind, kann der Port auf den neuen Container migriert werden. Fix mit einem Script alle Endpoints gecheckt und schon kann es an den Rückbau der alten Container gehen.

Hierbei können die "alten" Label einfach entfernt werden, ein Restart via Cron nachts erledigt dann den Rest, die alten Label verschwinden von den Containern und je nach Uhrzeit kriegt davon im Idealfall niemand etwas mit.

Beim neuen Container wird jetzt neben CLI-Arguments auch eine YAML-File für die Konfiguration aktzeptiert. Ich persönlich bevorzuge YAML über das alte Toml-Format und dadurch konnte ich auch nochmal von Scratch starten, was eventuelle Copy/Paste-Fehler schon mal ausschliessen sollte. Letztendlich war das Ergebnis folgendes:

providers:
  docker:
    exposedByDefault: false
    endpoint: "unix:///var/run/docker.sock"
    network: gateway

metrics:
  prometheus: {}

log:
  level: INFO
Meine traefik.yml

An der Stelle fällt dem konzentrierten Leser auf, dass exposedByDefault auf false steht, heißt das wir Traefik pro Container via Label traefik.enable=true mitteilen das wir diesen Container loadbalancen wollen. Zudem geben wir ein Standardnetzwerk an, in dem Traefik nach Containern suchen soll. Das ist soweit identisch mit meiner vorherigen Konfiguration, genauso wie die Angabe des Default-Network.

Metriken

Wie bereits erwähnt gibt es keine integrierten Diagramme mehr für Traefik. Hierbei gab es allerdings schon länger eine Prometheus-Integration, diese lässt sich in der traefik.yml relativ einfach aktivieren:

metrics:
  prometheus: {}
Metrics-Block in der traefik.yml

Das wars schon! Traefik einmal neu starten und schon stehen auf http://traefikIp:8080/metrics die entsprechenden Metriken zur Verfügung. Diese lassen sich dann via Prometheus abgreifen und mit Grafana visualisieren. Entsprechende Tutorials hierzu finden sich im Internet, falls man hier noch keine Erfahrungen hat.

Das Ganze ist in meinem Fall durch einige Vorkentnisse und Erfahrungen mit diesem Metric-Stack in einigen Stunden erfolgt. Ein entsprechendes fertiges Dashboard für Traefik 2 gibt es hier noch nicht, an der Stelle muss selbst Hand angelegt werden. Bei Interesse kann ich auch gerne meine Dashboard-Konfiguration teilen.

Das Ganze kann dann z. B. so aussehen:

Mein Traefik Dashboard

Durch die gezwungene Umstellung auf Prometheus konnte ich insgesamt noch einmal mit Prometheus auch im privaten Umfeld arbeiten. Zudem macht so ein Dashboard auch für das Auge einiges mehr her als die integrierte Visualisierung in Traefik 1.7

The End

Zum Ende dieses Posts möchte ich das Ganze nochmal Revue passieren lassen. Die Migration stellte sich insgesamt als recht simpel heraus. Durch das Migrieren in zwei Schritten war schnell ein Ergebnis pro Container sichtbar und eine gewisse damit verbundene Sicherheit.

Dank dieser Dynamik ging das Update relativ zügig und fehlerfrei, bei kurzer bis kaum bemerkbarer Downtime. An dieser Stelle muss allerdings angemerkt werden, dass der entsprechende Traffic auf den von Traefik gerouteten Domains als eher gering einzustufen ist.

Durch das Upgrade ist auch an der einen oder anderen Stelle etwas Neues aufgetaucht, wie z. B. das Ghost (für den Blog) oder Keycloak jetzt mehr X-Header benötigen, das war dann aber auch relativ schnell behoben und war nicht weiter tragisch.

An dieser Stelle kann ich auch nur noch einmal eine Empfehlung aussprechen. Für alle, die eine einfache, schnelle und intuitive Lösung suchen um ihre Docker-Container zu loadbalancen und hinter Domains zu packen. Gerade in Kombination mit CloudFlare wird es so ziemlich einfach den heimischen vServer schnell mit neuen Anwendungen zu bestücken.

Daher bleibt mir nur noch zu sagen ...