Nightly Builds und automatische Updates dank Docker und CI/CD

Software und Systeme müssen regelmäßig aktualisiert werden, insbesondere um Sicherheitslücken schnell zu schließen oder Bugfixes und Patches anzuwenden. Im besten Fall läuft das automatisch ab, damit kein Mensch ständig daran denken muss, Updates zu installieren. Dieser Artikel beschreibt das Setup für Nightly Builds unserer Docker-Images und automatische Rollouts auf die Systeme unserer Kunden.

Vorgehensweise Nightly Build mit Docker

Wir bauen und maintainen für zahlreiche Kunden verschiedenste Systeme, häufig auf Basis von TYPO3 oder Magento. Diese Systeme müssen permanent auf dem aktuellen Stand gehalten werden. Dabei gilt es, zahlreiche Komponenten zu berücksichtigen, wie z. B. Updates für das Betriebssystem, PHP, Webserver, Cachingserver, Datenbank-Engines usw.

Ein Großteil der von uns entwickelten Systeme läuft bereits in Form von Docker-Containern, was die Automatisierung des Update-Prozesses stark vereinfacht. Und Automatisierung ist immer unser Ziel, getreu dem Motto: “I don’t wanna do it, I want the silly machine to do it!

Dabei gehen wir in zwei Schritten vor.

Schritt 1: Nightly Build

Die Docker-Images für unsere Applikationen müssen regelmäßig neu gebaut werden. Beim Build-Prozess sollen alle verfügbaren Updates installiert werden.

Weil wir uns nicht darauf verlassen, dass die Maintainer der Basis-Docker-Images immer sofort die neuesten Linux-Updates installieren, erweitern wir den Build per Dockerfile um einen entsprechenden upgrade-Befehl:

FROM php:8.0-fpm-alpine as base
RUN apk upgrade --no-cache --available

Der Build erfolgt per Gitlab CI. Da wir die darin integrierte Registry nutzen, können wir die vordefinierten Variablen von Gitlab verwenden, was das Handling von Secrets (z. B. für den Registry-Login) sehr vereinfacht. Beispiel aus einer .gitlab-ci.yml:

docker-build:
  image: docker:latest
  stage: build
  services:
    - name: $CI_REGISTRY/ci-helper/gitlab-worker-dind:latest
      alias: docker
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - tag='latest'
    - docker build --pull -t "$CI_REGISTRY_IMAGE:${tag}" .
    - echo "Installed versions are:"
    - docker run --rm "$CI_REGISTRY_IMAGE:${tag}" nginx -v
    - docker run --rm "$CI_REGISTRY_IMAGE:${tag}" php -v
    - echo "Syntax check for nginx:"
    - docker run --rm "$CI_REGISTRY_IMAGE:${tag}" nginx -t
    - docker push --quiet "$CI_REGISTRY_IMAGE:${tag}"
    - docker logout $CI_REGISTRY
  rules:
    - if: ($CI_PIPELINE_SOURCE == "schedule")

Für den Build ist ein Docker-in-Docker-Setup nötig (DIND), weil der CI-Job nicht direkt mit dem Docker-Daemon des Gitlab-Workers sprechen darf. Wir stellen also einen Service mit einem separaten Docker-Daemon bereit (Zeilen 4-6), der nur für den Build verwendet und anschließend weggeworfen wird.

Damit Fehler in der Konfiguration von nginx sofort auffallen, wird die Syntax vor dem Push geprüft (Zeile 15). Ein Fehler in der Konfiguration würde dazu führen, dass nginx nicht starten kann und das Image damit unbenutzbar wäre. Deswegen lassen wir den Build an dieser Stelle abbrechen. Gitlab sagt uns darüber per Chat Bescheid.

Der Build kann nur per Gitlab Schedule angestoßen werden (Zeile 19) und soll nicht bei jedem “git push” loslaufen. Bei Bedarf kann man aber den Prozess auch manuell per Play-Button im Browser anstoßen und ist nicht gezwungen, auf den Schedule zu warten.

Das oben gezeigte Setup existiert in Gitlab für alle Komponenten (sprich Container) der Applikation. Die Schedules laufen leicht versetzt, um die Last auf den Gitlab-Workern zeitlich zu verteilen.

Schritt 2: Rollout

Für die Rollouts recyclen wir unseren bestehenden Deployment-Prozess, der per Ansible ausgeführt wird. Es genügt, das Playbook mit entsprechenden Tags so zu unterteilen, dass bei Updates die nicht benötigten Schritte ausgelassen oder angepasst werden. Auszug aus dem Ansible-Playbook:

 - name: Pull image
   community.docker.docker_compose:
     project_src: "{{ remote_app_directory }}"
     pull: true
   register: docker_compose_output
   retries: 3
   delay: 30
   until: docker_compose_output is not failed
   tags:
     - always
  - name: Stop services and remove volumes + orphans
    community.docker.docker_compose:
      project_src: "{{ remote_app_directory }}"
      state: absent
      remove_orphans: true
      remove_volumes: true
    register: docker_compose_output
    tags:
      - deploy
  - name: Stop services, keep volumes
    community.docker.docker_compose:
      project_src: "{{ remote_app_directory }}"
      state: absent
      remove_volumes: false
    register: docker_compose_output
    tags:
      - update

Die Ausführung des Playbooks und damit das tatsächliche Update der Kundensysteme erfolgt per Timer Trigger in Concourse CI. Wir triggern Concourse nicht durch neu gebaute Images, damit wir die mit den Kunden vereinbarten Zeitfenster möglichst exakt einhalten können.

In diesem Beispiel versorgen wir drei Kundensysteme mit Updates, jeweils in unterschiedlichen Intervallen, um den Betrieb so wenig wie möglich zu stören.

Gitlab vs. Concourse CI

Warum nutzen wir für die Deployments/Updates nicht ebenfalls Gitlab CI?

Technisch wäre das durchaus möglich, aber in unserem konkreten Fall benötigt der Prozess Credentials und weitere Secrets, die in Hashicorp Vault abgelegt sind. Concourse CI bietet einen einfacheren, transparenteren Weg, um auf diese Secrets während der Pipeline-Ausführung zuzugreifen. In Gitlab ist das zwar auch möglich, aber es erfordert mehr Klimmzüge – oder man bezahlt alternativ die Premium-Variante, was wir momentan nicht wollen.

Fazit

Zusammengefasst sieht unser Setup so aus:

  • Gitlab CI baut Docker-Images, testet sie und pusht sie in die Registry
  • Concourse CI führt Updates zeitgesteuert per Ansible aus. Docker-Images werden auf Kundensystemen aktualisiert und Container neu gestartet
  • Im Fehlerfall bekommen wir (und je nach Projekt auch der Kunde) eine Meldung per Mail/Chat

Das Hauptziel ist die Gewährleistung der Sicherheit durch sehr schnelle Installation von Updates und Patches (Stichwort Zero Day Exploit).

Ein entscheidender Faktor ist, dass die Applikation vollständig dockerisiert ist. Dadurch werden sichere, klar abgegrenzte Updates möglich, ohne Gefahr zu laufen, den ganzen Server versehentlich vollautomatisch stillzulegen. Falls aber ein Container nach einem Update nicht mehr funktioniert, kann schnell ein Rollback auf einen früheren Image-Tag gemacht werden.

Ein detailliertes Monitoring der Zielsysteme ist aber trotzdem wichtig, damit wir es sofort merken, wenn doch mal ein Update Probleme bereitet. Dafür nutzen wir u. a. Uptime Robot, Newrelic und Instana.


Sie sind neugierig geworden oder haben auch ein TYPO3-Projekt, für das Sie einen kompetenten Ansprechpartner mit über 20 Jahren Erfahrung suchen? Dann zögern Sie nicht und kontaktieren Sie noch heute unseren TYPO3-Experten und Head of DXP Tobias Hein.

Artikel teilen:

Newsletter abonnieren

 kostenlos & unverbindlich
 B2B-Commerce- & TYPO3-Themen
 IT- und branchenrelevante News

Neueste Beiträge

20 Jahre AIDA: TYPO3-Highlights aus 20 Jahren

Von Caroline Kuhn

Netresearch und AIDA: Unsere Projekt-Highlights aus 20 Jahren

Das Kreuzfahrtunternehmen AIDA Cruises zählt seit 2002 zu unseren wichtigsten Kunden. Wir bedanken…

Weiterlesen
Setup für Nightly Builds, Docker, CI/CD
Von Norman Golatka
Nightly Builds und automatische Updates dank Docker und CI/CD

Jedes System muss aktuell gehalten werden, insbesondere um Sicherheitslücken zu schließen – und zwar…

Weiterlesen
Oro Partner Connect 2022 in Feyburg
Von Luca Becker
Netresearch bei der Oro Partner Connect 2022

Netresearch war vom 16. bis 18. Oktober bei der ersten Oro Partner Connect in Freyburg. Dort haben…

Weiterlesen
Friedrich Verlag - Klett Gruppe - TYPO3-Projekt
Von Thomas Wilhelm
TYPO3-Projekt für Friedrich Verlag (Klett Gruppe)

Mit dem Friedrich Verlag konnte ein weiteres Unternehmen der Klett Gruppe als neuer Kunde gewonnen…

Weiterlesen