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.

Reverse Proxy Integration

Quelle https://acouvreur.github.io/sablier/

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.

Dynamic Strategy Shuffle Theme

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.

Dynamic Strategy

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.

Blocking Strategy

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.