Docker Scale-to-Zero mit Traefik und Sablier
Wenn man Web-Anwendungen oder -Services betreibt, die nur sporadisch genutzt werden, kann es sinnvoll sein, diese nur bei einem konkreten Bedarf zu starten. Im Umfeld von Docker-Containern und Kubernetes existiert für solche Anwendungsfälle das Konzept des Scale-to-Zero, also das Herunterskalieren von Workloads auf Null.
Für HTTP-Services in Verbindung mit einem Reverse-Proxy bietet Sablier eine einfache Möglichkeit, Scale-to-Zero in der eigenen Infrastruktur umzusetzen.
Scale-to-Zero
Scale-to-Zero bezeichnet das vollständige Herunterskalieren von Workloads auf Null, so dass keine Ressourcen mehr verbraucht werden. Im Umfeld von Kubernetes existieren Lösungen wie Keda, die aber eher auf Event-basierte Anwendungsfälle abzielen und nicht primär für HTTP-Services geeignet sind. Das HTTP-Addon für Keda befindet sich aktuell im Beta-Stadium.
Das vollständige Herunterfahren von Anwendungen kann einen Vorteil bringen, wenn man sehr viele Services mit jeweils wenigen Aufrufen betreibt. Damit sinkt die Anzahl der zeitgleich laufenden Container und der Ressourcenverbrauch. Setzt man hier auf einen Cloud-Anbieter und zahlt pro Ressource oder Zeiteinheit, lassen sich so Kosten sparen.
Der Beispiel-Code für diesen Post findet sich auf Github: https://github.com/davull/demo-docker-traefik-sablier
Sablier und Traefik
Sablier ist eine leichtgewichtige Lösung, um HTTP-Services nach Bedarf zu starten und zu stoppen. Es unterstützt verschiedene Container-Provider, aktuell Docker, Docker Swarm und Kubernetes. Um auf Basis von eingehenden Anfragen die entsprechenden Container hoch- und runterzufahren, existieren Plugins für die Reverse-Proxies Traefik, Nginx und Caddy. Da Sablier als eine API definiert ist, kann es prinzipiell auch in andere Systeme integriert werden.
Nachfolgend wird der Einsatz von Sablier mit Traefik und Docker beschrieben. Für andere Reverse-Proxies und Container-Provider erfolgt die Konfiguration aber analog.
Ausgangspunkt ist eine Traefik-Instanz und ein Workload, der über Sablier gesteuert werden soll. Die Definition der Services erfolgt mittels Docker Compose.
Das Konfigurationsfile docker-compose-traefik.yaml
für Traefik enthält eine einzige Service-Beschreibung und ein Netzwerk:
version: "3"
services:
traefik:
container_name: traefik
image: traefik:v2.10
ports:
- 80:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik/traefik.yml:/etc/traefik/traefik.yml
- ./traefik/dynamic_config/:/etc/traefik/dynamic_config/
- ./traefik/log/:/etc/traefik/log/
networks:
- traefik
...
networks:
traefik:
name: traefik
Der Workload besteht aus drei Services (hier alle durch traefik/whoami-Images abgebildet), welche eine verteilte Anwendung simulieren sollen. Die Datei docker-compose-whoami.yaml
hat folgenden Inhalt:
version: "3"
services:
whoami:
container_name: whoami
image: traefik/whoami
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami-router.entrypoints=web"
whoami-nginx:
container_name: nginx
image: traefik/whoami
networks:
- traefik
whoami-mariadb:
container_name: mariadb
image: traefik/whoami
networks:
- traefik
networks:
traefik:
name: traefik
external: true
Sablier einrichten
Sablier selber kann als Docker-Container oder als Binary ausgeführt werden. Wir verwenden hier den Container und erweitern die Datei docker-compose-traefik.yaml
um eine weitere Service-Beschreibung:
sablier:
container_name: traefik-sablier
image: acouvreur/sablier:1.3.0
command:
- start
- --provider.name=docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- traefik
Sablier benötigt Zugriff auf den Docker-Host-Service, daher mounten wir den Pfad /var/run/docker.sock
an die gleiche Stelle im Container. Über den Parameter --provider.name
geben wir den verwendeten Container-Provider docker
an.
Traefik-Plugin hinzufügen
Damit Traefik von Sablier erfährt und mit der Sablier-API kommunizieren kann, wird das Traefik-Plugin für Sablier konfiguriert. Dazu wird die Konfigurationsdatei traefik.yml
um folgende Zeilen erweitert:
experimental:
plugins:
sablier:
moduleName: "github.com/acouvreur/sablier"
version: "v1.3.0"
Konfiguration vorbereiten
Damit Sablier nun die Container unseres Workloads starten und stoppen kann, müssen zwei Anpassungen vorgenommen werden.
Da Traefik keinen Zugriff mehr auf Container-Labels hat, wenn ein Container nicht mehr läuft (auf Null skaliert wurde), müssen wir zunächst die Konfiguration des Workload-Services von Container-Labels auf einen Konfigurations-Datei umstellen. Dies ist natürlich nur notwendig, wenn für die Traefik-Konfiguration auch Container-Labels genutzt wurden.
Die Labels aus dem Workload-Service werden in eine Datei dynamic_config/whoami.yaml
übertragen.
Aus
# docker-compose-whoami.yaml
...
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami-router.entrypoints=web"
wird
# dynamic_config/whoami.yaml
http:
services:
whoami-service:
loadBalancer:
servers:
- url: http://whoami:80
routers:
whoami-router:
rule: "Host(`whoami.example.com`)"
service: whoami-service
entryPoints:
- web
Anschliessend können wir mit dem Einbringen von Sablier fortfahren.
Sablier konfigurieren
Sablier kennt zwei Arten (sogenannte Stategien), auf eine eingehende HTTP-Anfrage zu reagieren, wenn der entsprechende Container nicht läuft:
- Dynamic Strategy: Sablier liefert eine Status-Webseite aus, die dem Benutzer anzeigt, dass sein angefragter Service gerade gestartet wird und leitet ihn nach dem Start automatisch weiter bzw. lädt die Seite neu.
- Blocking Strategy: Sablier blockiert die Anfrage solange, bis der dahinterliegende Container gestartet ist und liefert dann die Antwort aus.
Die Blocking Strategy eignet sich vor allem für APIs, so dass der aufrufende Client beim ersten Zugriff auf einen heruntergefahrenen Service lediglich eine längere Antwortzeit erhält, nicht aber mit Retries und Redirects umgehen muss.
Dynamic Strategy
Um unseren Workload mit der Dynamic Strategy zu konfigurieren, legen wir zunächst eine Traefik-Middelware für Sablier an. Diese wird in der Datei dynamic_config/sablier.yaml
definiert:
http:
middlewares:
sablier-dynamic:
plugin:
sablier:
sablierUrl: http://sablier:10000
sessionDuration: 1m
names: whoami,nginx,mariadb
dynamic:
displayName: whoami
refreshFrequency: 1s
showDetails: true
theme: shuffle
sablierUrl
zeigt auf unseren Sablier-Container, welchen wir in der Datei docker-compose-traefik.yaml
definiert haben, Port 10.000 ist der Standardport.
Der Parameter sessionDuration
gibt an, wie lange ein Container gestartet bleiben soll, nachdem der letzte Zugriff auf ihn erfolgt ist. Danach wird der Container wieder heruntergefahren.
Mit names
geben wir die Namen der Container an, die Sablier steuern soll. Diese Namen müssen mit den Namen der Container in unserem docker-compose-whoami.yaml
-File übereinstimmen (Parameter container_name
).
Unterhalb des Keys dynamic
wird das Verhalten der Dynamic Strategy konfiguriert. Wir können einen Anzeigenamen (displayName
) für die Statusseite angeben, die Aktualisierungsrate (refreshFrequency
) der Statusseite, ob Details (showDetails
) angezeigt werden sollen und welches Theme (theme
) verwendet werden soll. Themes stehen mehrere zur Auswahl, es können aber auch eigene Themes definiert werden.
Nun konfigurieren wir unseren Workload so, dass er die Sablier-Middleware auch nutzt. In der Datei dynamic_config/whoami.yaml
ändern wir die Definition des whoami-Routers ab und fügen die Middleware hinzu:
http:
...
routers:
whoami-router:
rule: "Host(`whoami.example.com`)"
service: whoami-service
entryPoints:
- web
middlewares:
- sablier-dynamic@file
Damit ist Sablier bereit, unseren Request entgegenzunehmen und den dahinterliegenden Container für uns zu starten und nach einer Minute Leerlauf wieder herunterzufahren.
Ein Hinweis zur Dynamic Strategy: Der Safari Browser auf iOS-Devices zeigt beim ersten Zugriff leider nicht die Status-Page an sondern liefert einen
This side can't be reached
-Fehler.
Blocking Strategy
Die Blocking Strategy wird analog zur Dynamic Strategy konfiguriert. Wir legen zunächst eine weitere Traefik-Middelware für Sablier an. Diese wird in ebenfalls der Datei dynamic_config/sablier.yaml
definiert:
http:
middlewares:
sablier-dynamic:
...
sablier-blocking:
plugin:
sablier:
sablierUrl: http://sablier:10000
sessionDuration: 1m
names: whoami
blocking:
defaultTimeout: 10s
defaultTimeout
gibt an, wie lange auf das Starten des Ziel-Containers gewartet werden soll, bevor die Anfrage abgebrochen wird.
In der Datei dynamic_config/whoami.yaml
nutzen wir die neue Middleware:
http:
...
routers:
whoami-router:
...
middlewares:
- sablier-blocking@file
Wenn wir unseren Workload nun mit der Blocking Strategy aufrufen, erhalten wir beim ersten Zugriff eine längere Antwortzeit, da Sablier den dahinterliegenden Container erst starten muss.
Fazit
Sablier bietet eine einfache Möglichkeit, Scale-to-Zero für HTTP-Services umzusetzen. Wenn man sich einmal die notwendigen Informationen aus den verschiedenen Dokumentationen für Sablier, Traefik und dem Traefik-Plugin zusammengesucht hat, ist die Konfiguration nicht komplex. Allerdings ist die Dokumentation mitunter recht dünn und man trifft immer mal wieder auf leere Seiten.
Der Code der Beispiele mit Commits zu den einzelnen Konfigurations-Schritten findet sich auf Github https://github.com/davull/demo-docker-traefik-sablier.