Traefik als Proxy für Redmine

Seit einiger Zeit nutze ich den “Cloud Native Application Proxy” Traefik als Einstiegspunkt oder neudeutsch Edge Router auf allen Servern. Auf allen Servern? Nein, auf einer kleinen VM innerhalb des heimischen Netzes läuft unbeugsam die Kombination aus Nginx-Proxy-Container und Nginx-Proxy-Companion und hört nicht auf, Widerstand zu leisten… Zumindest war dies bis vor kurzem der Fall.

Upgrades und deren Folgen

Ein Upgrade des Hosts auf die aktuelle Ubuntu Server Version führte jedoch dazu, dass es in einem der bislang problemlos laufenden Containern zu Fehlern kam. Zwar liefen sowohl Nginx-Proxy, als auch Nginx-Proxy-Companion nach wie vor, doch ein damals speziell gebautes Nginx-Image, das auf mehreren Ports lauschte und dabei die SSL-Zertifikate nutzte, die zuvor vom Nginx-Proxy-Companion erzeugt worden waren, stellte sich letztlich quer. Da letztlich die Nutzung mehrerer Ports aufgrund steter Änderung der Infrastruktur gar nicht mehr notwendig war, und das verwendete Docker-Image auch endlich aktualisiert hätte werden müssen, stellte sich die Frage, ob der weitere Einsatz der bisherigen Docker-Container überhaupt sinnvoll gewesen wäre. Somit erschien es mir nicht nur einfacher, sondern auch konsistenter, auch auf jener kleinen VM direkt auf Traefik zu setzen, selbstverständlich unter Mithilfe von Docker.

Zentraler Einstiegspunkt für Web-Applikationen

Die Aufgaben von Traefik bzw. der zuvor eingesetzten Nginx-Lösung sind dabei klar umrissen. Auf dem Server läuft außer dem Proxy kein weiterer Dienst, vielmehr dient die Maschine als zentraler Einstiegs- und Verteil-Punkt für Web-Anwendungen, die auf anderen VMs innerhalb des heimischen Netzes betrieben werden. Dabei werden die Ports 80 und 443 von der Fritz!Box per Port-Weiterleitung (mit einem kleinen Umweg, der hier jedoch irrelevant ist) an den Server weitergeleitet. Damit ist es möglich, z.B. namensbasierte virtuelle Hosts zu betreiben, ebenso erfolgt die Verwaltung und Bereitstellung von SSL-Zertifikaten an zentraler Stelle, bei Änderung einer internen IP-Adresse muss die Konfiguration ebenfalls nur an einer Stelle geändert werden, unerwünschte Requests können zentral geblockt werden usw., kurzum – die bisher eingesetzte Struktur erschien mir nach wie vor sinnvoll, nur sollten Nginx & Co. durch Traefik ersetzt werden.

Der erste Schritt bestand darin, gründlich aufzuräumen, somit wurden alle alten Docker-Container zunächst gestoppt und gelöscht. Das Löschen umfasste auch die Docker-Images, damit nicht möglicherweise versehentlich zu einem späteren Zeitpunkt ein Image einer “latest”-Version verwendet werden kann, das jedoch bereits mehrere Monate alt ist. Ebenso wurden die Pakete der Docker-Engine selbst aktualisiert, denn in der Zwischenzeit hatte es Docker ja wieder einmal geschafft, die empfohlene Installationsmethode zu ändern, weshalb ich an dieser Stelle einmal mehr auf die entsprechende Dokumentation (hier für die Ubuntu-Distribution) verweise.

Docker-Compose-File für Traefik

Im nächsten Schritt habe ich das Docker-Compose-File für Traefik erstellt. Dessen Aufbau ist analog zu den Docker-Compose-Dateien, die bereits auf anderen Servern im Einsatz sind, weshalb es hier nicht weiter ausführlich erläutert werden soll. An dieser Stelle daher nur die Datei traefik_compose.yml als Ganzes, selbstverständlich ohne Domainnamen, Passwörter o.ä.:

version: '3.3'

services:
  traefik:
    # Use the latest Traefik image
    image: traefik:v2.8
    ports:
      # Listen on port 80, default for HTTP, necessary to redirect to HTTPS
      - "80:80"
      # Listen on port 443, default for HTTPS
      - "443:443"
    restart: always
    dns:
      - 8.8.8.8
      - 8.8.4.4

    labels:
      # Enable Traefik for this service, to make it available in the public network
      - "traefik.enable=true"
      # Use the traefik-public network (declared below)
      - "traefik.docker.network=traefik-public"
      
      # admin-auth middleware with HTTP Basic auth
      # Using the environment variables USERNAME and HASHED_PASSWORD
      - "traefik.http.middlewares.admin-auth.basicauth.users=admin:<PASSWORD>"

      # Enable gzip compression
      - "traefik.http.middlewares.def-compress.compress=true"
      # https-redirect middleware to redirect HTTP to HTTPS
      # It can be re-used by other stacks in other Docker Compose files
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
      # traefik-http set up only to use the middleware to redirect to https
      # Uses the environment variable DOMAIN
      - "traefik.http.routers.traefik-public-http.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-public-http.entrypoints=http"
      - "traefik.http.routers.traefik-public-http.middlewares=https-redirect"
      # traefik-https the actual router using HTTPS
      # Uses the environment variable DOMAIN
      - "traefik.http.routers.traefik-public-https.middlewares=secHeaders@file"
      - "traefik.http.routers.traefik-public-https.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-public-https.entrypoints=https"
      - "traefik.http.routers.traefik-public-https.tls=true"


      # Use the special Traefik service api@internal with the web UI/Dashboard
      - "traefik.http.routers.traefik-public-https.service=api@internal"
      # Use the "le" (Let's Encrypt) resolver created below
      - "traefik.http.routers.traefik-public-https.tls.certresolver=le-tls"
      # Enable HTTP Basic auth, using the middleware created above
      - "traefik.http.routers.traefik-public-https.middlewares=secHeaders@file,admin-auth,def-compress"
      # Define the port inside of the Docker service to use
      - "traefik.http.services.traefik-public.loadbalancer.server.port=8080"

      # custom path for prometheus metrics
      - "traefik.http.middlewares.admin-auth-metrics.basicauth.users=adminmetrics:<METRICS PASSWORD>"

      - "traefik.http.routers.traefik-public-metrics-http.rule=Host(`traefik.example.com`) && PathPrefix(`/traefik-metrics`)"
      - "traefik.http.routers.traefik-public-metrics-http.entrypoints=http"
      - "traefik.http.routers.traefik-public-metrics-http.middlewares=https-redirect"

      - "traefik.http.routers.traefik-public-metrics-https.middlewares=secHeaders@file"
      - "traefik.http.routers.traefik-public-metrics-https.rule=Host(`traefik.example.com`) && PathPrefix(`/traefik-metrics`)"
      - "traefik.http.routers.traefik-public-metrics-https.entrypoints=https"
      - "traefik.http.routers.traefik-public-metrics-https.tls=true"

      - "traefik.http.routers.traefik-public-metrics-https.service=prometheus@internal"
      - "traefik.http.routers.traefik-public-metrics-https.tls.certresolver=le-tls"
      - "traefik.http.routers.traefik-public-metrics-https.middlewares=secHeaders@file,admin-auth-metrics,def-compress"

    volumes:
      # Add Docker as a mounted volume, so that Traefik can read the labels of other services
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # Mount the volumes to store the certificates and files for dynamic configuration
      - ./data/certificates:/certstore
      - ./conf:/conf
      - ./certificates:/certs
    command:
      # Enable Docker in Traefik, so that it reads labels from Docker services
      - "--providers.docker"
      - "--global.sendAnonymousUsage"
      - "--metrics.prometheus=true"
      - "--metrics.prometheus.manualrouting=true"

      # Do not expose all Docker services, only the ones explicitly exposed
      - "--providers.docker.exposedbydefault=false"
      - "--providers.file.directory=/conf"
      - "--providers.file.watch=true"
      # Create an entrypoint "http" listening on address 80
      - "--entrypoints.http.address=:80"
      # Create an entrypoint "https" listening on address 80
      - "--entrypoints.https.address=:443"

      # first certresolver dns
      # Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
      - "--certificatesresolvers.le-ns1.acme.email=ich@example.com"
      # Store the Let's Encrypt certificates in the mounted volume
      - "--certificatesresolvers.le-ns1.acme.storage=/certstore/acme.json"
      - "--certificatesResolvers.le-ns1.acme.dnschallenge.provider=pdns"
      - "--certificatesResolvers.le-ns1.acme.dnschallenge.delayBeforeCheck=20"
      - "--certificatesResolvers.le-ns1.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"

      # second certresolver tls
      # Use the TLS Challenge for Let's Encrypt
      - "--certificatesresolvers.le-tls.acme.tlschallenge=true"
      - "--certificatesresolvers.le-tls.acme.storage=/certstore/acme-tls.json"
      - "--certificatesresolvers.le-tls.acme.email=ich@example.com"


      # Enable the access log, with HTTP requests
      - "--accesslog"
      # Enable the Traefik log, for configurations and errors
      - "--log"
      - "--log.level=DEBUG"
      # Enable the Dashboard and API
      - "--api=true"
      - "--api.dashboard=true"

    environment:
      PDNS_API_KEY: "<POWERDNS_API_KEY>"
      PDNS_API_URL: "https://nameserver.example.com"
      
    networks:
      # Use the public network created to be shared between Traefik and
      # any other service that needs to be publicly available with HTTPS
      - "traefik-public"

networks:
  # Use the previously created public network "traefik-public", shared with other
  # services that need to be publicly available via this Traefik
  traefik-public:
    external: true

Die eingebauten Prometheus-Metriken wurden ebenfalls wieder aktiviert, das Traefik-Dashboard sowie die Metriken sind mit Basic-Authentication geschützt, wobei eine permanente Weiterleitung von http auf https stattfindet, schließlich sollen Passwörter nicht etwa im Klartext übermittelt werden. Um SSL-Zertifikate von Let’s Encrypt zu erstellen, stehen sowohl die TLS-Challenge als auch der Weg per DNS zur Verfügung.

Das Netzwerk namens “traefik-public” wird mittels Docker-Kommando “docker network create traefik-public” angelegt, weitere Parameter sind hierbei nicht nötig, da Docker auf der Maschine eigenständig und nicht etwa in einem Docker-Swarm-Verbund läuft.

In der Datei conf/dynamic.yml werden noch die Middleware “secHeaders” sowie TLS-Optionen definiert, zu deren Inhalt siehe im bereits erwähnten Artikel.

Gestartet wird Traefik nun mittels:

geschke@seli:~/services/traefik$ docker compose -f traefik_compose.yml up -d

Im Unterschied zum in der Vergangenheit eigenständigen Binary “docker-compose” ist dessen Funktionalität nun als Plugin innerhalb der Docker-CLI verfügbar. An den hier genutzten Parametern hat sich hingegen nichts geändert, insofern entfällt letztlich nur der Bindestrich zwischen “docker” und “compose“. Nach dem nur wenige Sekunden dauernden Start von Traefik kann das Erzeugen der Let’s-Encrypt-Zertifikate auch ein wenig länger benötigen, aber sobald dies geschehen ist, sollte das Traefik-Dashboard unter der konfigurierten URL verfügbar sein.

Das Dashboard von Traefik

Traefik als Proxy für Redmine

Im nächsten Schritt wurde der eigentliche Proxy für die web-basierte Projekt-Management Redmine eingerichtet. Ich nutze Redmine einerseits als Ticket-System, andererseits das integrierte Wiki als Notizbuch nicht nur für IT-, sondern für alle möglichen Themen. Über die Jahre hat es sich als stabile Lösung bewährt, auch wenn die Installation Rails-typisch etwas umständlich erscheinen mag. Tatsächlich laufen Redmine und dessen Datenbank als einer der wenigen Dienste noch nicht in einem Docker-Container, sondern auf einer dedizierten virtuellen Maschine. Innerhalb des heimischen Netzes wäre die Redmine-Installation zwar standardmäßig verfügbar, doch möchte ich von jedem Ort darauf zugreifen können, weshalb nun Traefik ins Spiel kommt.

Dabei ist die Konfiguration auf sehr einfache Weise möglich. Schließlich laufen Redmine mitsamt Thin und Nginx bereits auf ihrer Maschine, insofern müssen Requests nur weitergeleitet werden und somit eben genau das erledigen, wofür ein Proxy nun einmal vorhanden ist.

Traefik lässt sich vielfältig einrichten, bisher wurden für die Bereitstellung von Web-Anwendungen dazu die Labels der Docker-Compose-Files – und somit der “Docker-Provider” – verwendet, doch da kein weiterer Docker-Container bzw. -Service notwendig ist, entfällt auch diese Möglichkeit. Von der Syntax her fast noch einfacher ist die Nutzung einer Yaml-Datei. Denn hier kommt das Verzeichnis “conf” ins Spiel, das zur “dynamischen” Konfiguration von Traefik dient bzw. dementsprechend eingerichtet wurde.  Mit Hilfe des so genannten File-Providers werden alle Konfigurationsdateien, die in diesem Verzeichnis liegen, von Traefik automatisch ausgewertet.

Eine “dynamische” Konfigurationsdatei

Zur Definition des Redmine-Proxy sind dabei nur wenige Zeilen in einer z.B. “redmine-example.yml” genannten und innerhalb des conf-Verzeichnisses platzierten Datei nötig:

http:
  routers:
    router-redmine-example:
      entryPoints:
        - http
      rule: "Host(`redmine.example.com`)"
      middlewares:
        - https-redirect@docker
      service: "service-redmine-example"
    router-redmine-example-secured:
      entryPoints:
        - https
      rule: "Host(`redmine.example.com`)"
      middlewares:
        - secHeaders
        - def-compress@docker
      service: "service-redmine-example"
      tls:
        certResolver: le-tls
  services:
    service-redmine-example:
      loadBalancer:
        servers:
        - url: "http://192.168.1.234:80/"

Die jeweiligen Komponenten sollten bekannt sein – im Unterschied zur Einrichtung mittels Docker-Compose-File-Labels unterscheidet sich hier nur die Syntax.

Traefik unterscheidet zwischen “statischer” und “dynamischer” Konfiguration. Statische Optionen lassen sich nur durch einen Neustart ändern, während die dynamische Konfiguration mittels der so genannten “Provider” bereit gestellt und zur Laufzeit ausgewertet wird. Obwohl also eine Datei an sich im Allgemeinen als statisch gilt, werden für die Definition des Redmine-Proxys ausschließlich Komponenten verwendet, die Traefik als dynamisch betrachtet.

Definiert werden zwei Router, der erste namens “router-redmine-example” dient nur zur Weiterleitung auf die verschlüsselte https-URL. Dazu wird die Middleware “https-redirect” verwendet, die im Rahmen des Traefik-Docker-Compose-Files erstellt wurde. Die Angabe “https-redirect@docker” ist hierbei zwingend notwendig, um Traefik zu erkennen zu geben, dass die Middleware im Docker-Provider gefunden wird. Falls die Angabe “@docker” fehlt, geht Traefik davon aus, dass sich die benannte Middleware im Namensraum des File-Providers befindet – und quittiert dies mit einem Fehler, der netterweise sehr eindeutig ist und auch in der Traefik-UI angezeigt wird.

Dasselbe gilt für die im Router “router-redmine-example-secured” eingesetzte Middleware “def-compress@docker“, die zur gzip-Komprimierung dient. Da diese im Docker-Provider definiert wurde, muss sie im File-Provider mit dem Suffix “@docker” angesprochen werden.

Dieser zweite Router ist bzgl. der Host-Regel identisch zum ersten, als Entrypoint wird nun der wiederum im Traefik-Docker-Compose-File definierte “https” genutzt, und neben der Middleware zur Komprimierung der zu übertragenden Daten findet sich die Middleware “secHeaders“, die sich zwar in einer anderen Datei, und zwar der oben erwähnten “dynamic.yml“, aber innerhalb desselben Providers, d.h. dem File-Provider, befindet. Zur Erzeugung des SSL-Zertifikate soll der unter dem Namen “le-tls” definierte Resolver Verwendung finden. Abgeschlossen werden beide Router durch die Nennung eines Services, von dem die Requests bearbeitet werden sollen. Eine Übersicht der Traefik-Komponenten findet sich in der Dokumentation.

Einzig erwähnenswert erscheint mir, dass auch der erste Router, der nur für den Redirect auf https zuständig ist, einen Service benennen muss. Auch wenn der Service letztlich gar nicht angesprochen wird, darf die Angabe nicht fehlen, ansonsten vermeldet Traefik einen Fehler.

Der Service namens “service-redmine-example” besteht dabei aus einem Loadbalancer, der auf die interne URL bzw. IP-Adresse inkl. Port des Redmine-Servers zeigt. Dabei handelt es sich um eine der einfachsten Service-Definitionen, bei dem letztlich nur alle Requests an einen einzigen Server weitergeleitet werden. Traefik bietet dazu auch weit umfassendere Möglichkeiten, dazu an dieser Stelle wieder einmal ein Verweis auf die Dokumentation.

Traefiks Provider, oder auch: Namensräume

Zu den Definitionen bzw. der Nomenklatur der Middlewares noch ein Hinweis: In einem ersten Versuch hatte ich das notwendige Suffix “@docker” noch nicht eingesetzt. Da der Versuch mit einem Fehler endete, hatte ich die jeweiligen Middlewares mit ihrem eigenen Namen in derselben Datei definiert, und zwar wie folgt:

http:
  #[...weiterer Inhalt wie oben...]
  middlewares:
    mw-redmine-example-https-redirect:
      redirectScheme:
        scheme: https
        permanent: true
    mw-redmine-example-compress:
      compress: {}

Die Middlewares “mw-redmine-example-https-redirect” für den permanenten Redirect auf https und “mw-redmine-example-compress” für die gzip-Komprimierung konnten somit direkt innerhalb der Datei bzw. des File-Providers ohne weitere Zusätze verwendet werden, z.B.:

middlewares:
  - secHeaders
  - mw-devkuerbisorg-compress

Mit Hilfe der Angabe des Docker-Providers lässt sich eine solche in diesem Fall doppelte Middleware-Definition jedoch vermeiden. Hingegen würde ich Middleware-Definitionen, die ausschließlich bei denjenigen Routern verwendet werden, die in der Konfigurationsdatei definiert werden, auch innerhalb derselben Datei platzieren.

Traefik, praktisch, gut

Dank der “dynamischen” Konfiguration genügt es tatsächlich, die Yaml-Datei im Verzeichnis “conf” zu speichern. Angesichts des Parameters “--providers.file.watch=true” überwacht Traefik Änderungen im File-Provider und reagiert darauf, ohne dass ein Reload notwendig wäre. In der Traefik-UI werden die Änderungen bzw. neuen Router, Services und ggf. Middleware auch umgehend dargestellt. 

Traefik hat sich als eine gute und einfache Lösung herausgestellt. Zum Beispiel genügt nun ein Traefik-Container anstatt der zuvor eingesetzten Kombination aus Nginx-Proxy- und Nginx-Proxy-Companion-Container sowie eines zusätzlichen Nginx für die zusätzlichen Ports. Zwar würde ich inzwischen keine unterschiedlichen Ports mehr benutzen, sondern vermutlich stattdessen andere Hostnamen verwenden, aber weitere Ports ließen sich mit der entsprechenden EntryPoint-Definition innerhalb derselben Traefik-Instanz zum Einsatz bringen.

 

2 Gedanken zu „Traefik als Proxy für Redmine“

Schreibe einen Kommentar

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

Tags: