Neuigkeiten von trion.
Immer gut informiert.

Unterbrechungsfreies Deployment mit Docker Compose

Docker Compose

Ab und zu kommen Wünsche, die sich extrem einfach mit Kubernetes umsetzen lassen würde, aber …​
Kubernetes ist viel zu komplex!
Wir haben kein Kubernetes!
Es ist doch nur ein Server (aber Ausfallsicherheit brauchen wir schon) …​

Wie könnte also ein unterbrechungsfreies Deployment aussehen, wenn lediglich Docker und Docker Compose zur Wahl stehen, und was sind die Tradeoffs, die damit einher gehen?
Schauen wir es uns an einem praktischen Beispiel an.

Container eignen sich tatsächlich sehr gut, um damit unabhängig von der konkreten Softwareplattform Anwendungen handhabbar zu betreiben. Und in jedem Fall ist die Verwendung eine Voraussetzung, wenn es irgendwann einmal Richtung Kubernetes gehen sollte. Docker Compose eignet sich damit als Brückenlösung und schafft gleichzeitig Erfahrung im Umgang mit Containern.

Ein einfaches Docker Compose File zum Betrieb eines Anwendungscontainers, der beispielsweise aus einer GitLab Container Registry stammen könnte, kann so aussehen:

Docker Compose für Anwendungscontainer
services:
  app-demo:
    image: localhost:5005/root/app-demo/ci:latest
    restart: on-failure
    ports:
      - "80:80"

Hier kann man sich die Frage stellen, wer für einen Restart des Containers verantwortlich ist, sollte dieser crashen. Das kann entweder ein systemd-Unit pro Container sein, oder es wird eine Restart-Policy im Docker Compose konfiguriert, dann ist der Docker Daemon für den Container verantwortlich.

Nun soll es möglich sein, den Container durch eine andere Version zu ersetzen, ohne, dass es zu einem Ausfall kommt. Dazu benötigt man eine Reserve-Instanz und einen Weg, um geordnet eine Instanz herunterzufahren, ohne, dass bei ggf. noch laufende Requests kaputt gehen.
Docker compose bietet einen Deployment-Mode an, um Repliken direkt im Compose File anzugeben. Damit wäre dann bei zwei Repliken für eine Reserve-Instanz gesorgt, wobei dabei natürlich der doppelte Speicherbedarf als Tradeoff bedacht werden muss.
Mit zwei Containerinstanzen ist es nun nicht mehr möglich, ein simples Portmapping zu nutzen, denn es kann jeweils nur eine Instanz an einen Port binden.

Konfiguration von Repliken für einen Anwendungscontainer
services:
  app-demo:
    image: localhost:5005/root/app-demo/ci:latest
    restart: on-failure
    deploy:
      mode: replicated
      replicas: 2

Als Lösung für den Zugriff kommt ein Reverse-Proxy, in diesem Fall traefik, zum Einsatz. Das ist ein besonders robuster Container, der als einziger nach außen verfügbar ist, und eingehende Requests an alle Anwendungsinstanzen weiterverteilt.

Traefik als Reverse Proxy mit Docker Compose
  traefik:
    image: traefik:v3
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--log.level=INFO"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

Durch Labels kann die Routingkonfiguration an traefik kommuniziert werden.

traefik Konfiguration durch Labels am Anwendungscontainer
services:
  app-demo:
    image: localhost:5005/root/app-demo/ci:latest
    restart: on-failure
    deploy:
      mode: replicated
      replicas: 2
    labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app-demo.rule=Host(`app-demo.localhost`)"
    - "traefik.http.services.app-demo.loadbalancer.server.port=80"

Im grafischen Dashboard von traefik sind dann die entsprechenden Instanzen zu sehen. Zwischen den Repliken führt traefik Round-Robin-Loadbalancing durch, falls nichts anderes, wie bspw. sticky-Sessions, konfiguriert ist.

traefik Dashboard mit zwei Repliken
Abbildung 1. traefik Dashboard mit zwei Repliken

Als nächster Schritt wird ein Weg zum automatischen Update des Container Images benötigt. Hier gibt es das hervorragende und bewährte Projekt Watchtower. Das Projekt ging schon durch viele Hände, sicherlich nicht zuletzt, weil viele Nutzer zu anderen Stacks, wie beispielsweise Kubernetes, gewechselt sind.
Der aktuell am besten gepflegte Fork von Watchtower befinden sich unter https://watchtower.nickfedor.com/

Watchtower beobachtet Container Registries und konfiguriert bei Bedarf das zu verwendende Image eines Containers um. Dazu muss der Container natürlich kurz gestoppt werden, deswegen benötigen wir auch mehrere Repliken.
Schauen wir uns zunächst die Basiskonfiguration an.

Watchtower Docker Container
watchtower:
  image: nickfedor/watchtower
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - ~/.docker/config.json:/config.json:ro  # pull credentials
  environment:
  - TZ=Europe/Berlin
  ## either schedule or interval
  # - "WATCHTOWER_SCHEDULE=0 0 4 * * *"
  # - WATCHTOWER_POLL_INTERVAL=86400
  - WATCHTOWER_POLL_INTERVAL=300 # 5 minutes

Auch Watchtower kann durch Label gesteuert werden. So soll traefik - aktuell ein Single-Point-of-Failure - nicht automatisch aktualisiert und dazu durchgestartet werden.
Das entsprechende Label sieht dann so aus:
"com.centurylinklabs.watchtower.enable=false"

Das Konstrukt hat jedoch noch kleinere Haken:
Es wäre schön, wenn traefik einen Container aus der Lastverteilung nehmen könnte, bevor dieser von Watchtower heruntergefahren und neu gestartet wird. Zudem soll ein Container ausreichend Zeit zum herunterfahren haben, damit laufende Anfragen nicht kaputt gehen. Als letzter wichtiger Punkt muss verhindert werden, dass Watchtower die zweite Instanz durchstartet, bevor die erste wieder korrekt läuft.

Dazu können zwei Elemente zusammen eine Lösung bringen:
Docker erlaubt Health-Checks, und traefik sowie Watchtower werten diese aus. Wenn wir einen Weg finden, dass Watchtower vor dem Herunterfahren den Healtcheck auf down setzen kann, nimmt traefik die Instanz aus dem Loadbalancing. Watchtower bietet Lifecycle-Hooks an, damit können vor und nach Container Updates Kommandos im Container ausgeführt werden. Es ist also möglich, eine Marker-Datei anzulegen, die der Healthcheck auswertet. Dann muss lediglich lang genug gewartet werden, bis evtl. vorhandene Requests zuende bearbeitet sind und der Container quasi "ausgetrocknet" ist. Dazu reicht ein einfaches sleep, denn der Lifecycle-Hook wartet auf Abarbeitung, bevor Watchtower weiter macht.

Watchtower muss dann lediglich noch so konfiguriert werden, dass zum einen die Lifecycle-Hooks aktiviert sind und zum anderen dass eine Container-Instanz nach der nächsten aktualisiert wird, und nicht alle gleichzeitig.

Umsetzung von Healthchecks und Rolling-Update mit Watchtower und Docker Compose
services:
  app-demo:
    image: localhost:5005/root/app-demo/ci:latest
    restart: on-failure
    deploy:
      mode: replicated
      replicas: 2
    labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app-demo.rule=Host(`app-demo.localhost`)"
    - "traefik.http.services.app-demo.loadbalancer.server.port=80"
  watchtower:
    image: nickfedor/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ~/.docker/config.json:/config.json:ro  # pull credentials
    environment:
    - TZ=Europe/Berlin
    - WATCHTOWER_POLL_INTERVAL=300 # 5 minutes
    - WATCHTOWER_ROLLING_RESTART=true
    - WATCHTOWER_LIFECYCLE_HOOKS=true

  app-demo:
    image: localhost:5005/root/app-demo/ci:latest
    restart: on-failure
    deploy:
      mode: replicated
      replicas: 2
    labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app-demo.rule=Host(`app-demo.localhost`)"
    - "traefik.http.services.app-demo.loadbalancer.server.port=80"
    - "com.centurylinklabs.watchtower.lifecycle.pre-update=touch /tmp/not-ready && sleep 25"
    - "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout=60"
    healthcheck:
      test: "test ! -f /tmp/not-ready && curl -f http://localhost"
      interval: 10s
      timeout: 3s
      retries: 2

Das ganze ist weder wirklich schwer, noch komplex und funktioniert so auch unter hoher Last zuverlässig.
Allerdings muss dazu gesagt werden, dass es sich ganz und gar nicht um eine Standardlösung handelt.

Hier werden mit Docker-Mitteln Lösungen geschaffen, die es so "fertig" in Kubernetes als Konzepte (Readiness-Probe, Service, Ingress) und auch Implementierung gibt.
Es ist zu überlegen, ob es sich da nicht lohnt, auch auf einer Single-Node-Umgebung auf Kubernetes zu setzen, um von robusten Standardlösungen zu profitieren und in Know-How zu investieren, dass auch zukünftig Bestand hat.




Zu den Themen Kubernetes, Docker und Cloudarchitektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel – per E-Mail an (info a-t trion.de) oder über unser Kontaktformular. Wir freuen uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.