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
Als TYPO3-Agentur bauen wir 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.
Tobias Hein
Head of DXP
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.