Die konsequente Trennung von Infrastrukturdefinition und Anwendungscode ist ein zentrales Prinzip moderner Softwareentwicklung. In mehreren Projekten der WhereGroup setzen wir dieses Prinzip um, indem zentrale Bestandteile wie Docker-Compose-Konfigurationen, Umgebungsvariablen und Deployment-Skripte in einem zentralen Infrastruktur-Repository verwaltet werden. Dockerfiles und die gitlab-ci.yml-Datei befinden sich allerdings weiterhin im Services-Repository. In diesem Blogartikel wird der Aufbau dieses Repositories, die Funktionsweise der GitLab CI/CD-Pipeline für Continuous Integration und Continuous Deployment sowie den Ablauf des Ausrollens von Änderungen mit bewährten Automatisierungswerkzeugen erläutert.
Das Infrastruktur-Repository „infrastructure“ enthält sämtliche Konfigurationsdateien, Vorlagen für Umgebungsvariablen und globale Docker-Compose-Dateien. Parallel dazu existiert ein Services-Repository „services“. In diesem Repository besitzt jeder Service im Ordner „service“ ein eigenes Unterverzeichnis mit einem Dockerfile, Skripten, Konfigurationsfragmenten und seinem Quellcode. Die Datei „.gitlab-ci.yml“ im Services-Repository legt die Pipeline für Continuous Integration und Continuous Deployment fest. Sie beschreibt die Build- und Deploy-Jobs für jeden Service, die anschließend vom GitLab Runner automatisch ausgeführt werden.
Die Verzeichnisstruktur ist dabei klar gegliedert:
.
├── infrastructure/
└── services/
├── service/
└── .gitlab-ci.yml
Im Infrastruktur-Repository befinden sich im Wurzelverzeichnis die Docker-Compose-Dateien für verschiedene Zielumgebungen sowie Vorlagen und Konfigurationsdateien. Dienste, wie „printserver“ oder „testwfs“, haben einen eigenen Ordner für spezifische Einstellungen. Dadurch lassen sich globale Infrastrukturdefinitionen und dienstspezifische Anpassungen klar voneinander trennen.
.
├── .env.template
├── README.md
├── .gitignore
├── docker-compose.dev.yml
├── docker-compose.test.yml
├── docker-compose.staging.yml
├── docker-compose.prod.yml
├── docker-compose.yml
├── printserver/
├── testwfs/
└── local.yml
Die Datei „docker-compose.yml“ bildet die zentrale Basis und definiert alle Dienste, Netzwerke und Volumes. Umgebungsspezifische Dateien wie „docker-compose.dev.yml“, „docker-compose.test.yml“, „docker-compose.staging.yml“ und „docker-compose.prod.yml“ erweitern diese Basiskonfiguration um die jeweils passenden Einstellungen und Image-Tags. Die Datei „.env.template“ dokumentiert alle benötigten Umgebungsvariablen mit Platzhaltern und Beschreibungen. Dank der Versionierung sind sämtliche Konfigurationen jederzeit nachvollziehbar und reproduzierbar.
Der Build-Job und der Deploy-Job übernehmen im Automatisierungsprozess unterschiedliche Aufgaben. Dabei erstellt der Build-Job aus dem Quellcode eines Services ein neues Container-Image und lädt dieses in die zentrale Container-Registry hoch. Erst wenn ein neues Image bereitsteht, folgt der Deploy-Job. Dieser aktualisiert die entsprechende Compose-Datei im Infrastruktur-Repository, sodass dort das neue Image-Tag referenziert wird. Durch diese Trennung ist sichergestellt, dass nur getestete und freigegebene Images tatsächlich in der Produktivumgebung verwendet werden.
Die zentrale Konfiguration für Continuous Integration und Continuous Deployment befindet sich in der Datei „.gitlab-ci.yml“. Um Konsistenz und Wartbarkeit zu gewährleisten, werden Build- und Deploy-Jobs auf sogenannte Vorlagen (Templates) zurückgeführt. In GitLab CI/CD werden diese Vorlagen durch einen Punkt am Anfang des Namens gekennzeichnet, zum Beispiel .parallel oder .build-template.
Die Definition der Stages in der GitLab CI/CD-Pipeline legt die Reihenfolge und die logische Struktur der einzelnen Pipeline-Schritte fest.
Zunächst werden die Container-Images gebaut (build und build-hotfix). Anschließend folgen die Deployments in die jeweiligen Umgebungen: Test, Staging und Produktion, jeweils für reguläre Deployments und Hotfixes.
Durch diese klare Trennung und Reihenfolge ist sichergestellt, dass Images erst nach einem erfolgreichen Build in die Zielumgebungen ausgerollt werden. Hotfixes können gezielt und unabhängig von regulären Deployments verarbeitet werden.
Jede Stage wird dabei nur ausgeführt, wenn die vorherige erfolgreich abgeschlossen wurde. Besonders zu beachten ist, dass die Deployments auf die Staging- und Produktionsumgebungen grundsätzlich nur durch ein manuelles Triggern angestoßen werden. Dadurch wird ein unbeabsichtigtes Ausrollen verhindert und der gesamte Prozess bleibt nachvollziehbar und kontrollierbar.
stages:
- build
- deploy-test
- deploy-staging
- deploy-prod
- build-hotfix
- deploy-hotfix-test
- deploy-hotfix-staging
- deploy-hotfix-prod
In der Vorlage .parallel sorgt das GitLab-Feature parallel dafür, dass mehrere Jobs gleichzeitig ausgeführt werden können. Jeder Eintrag in der matrix entspricht einem eigenen Job, der für einen bestimmten Dienst (SERVICE) und das zugehörige Konfigurationsverzeichnis (CONFIG_FOLDER) zuständig ist. Durch diese parallele Ausführung lassen sich die einzelnen Services unabhängig voneinander bauen und bereitstellen, was die Pipeline deutlich beschleunigt. Es sollte jedoch darauf geachtet werden, dass nur solche Jobs parallel laufen, die sich nicht gegenseitig beeinflussen. In manchen Fällen ist eine sequenzielle Ausführung weiterhin sinnvoll.
.parallel:
parallel:
matrix:
- SERVICE: rproxy
CONFIG_FOLDER: "./"
- SERVICE: frontend
CONFIG_FOLDER: "./"
- SERVICE: backoffice
CONFIG_FOLDER: "./"
- SERVICE: djangoapi
CONFIG_FOLDER: "./"
- SERVICE: db
CONFIG_FOLDER: "./"
- SERVICE: fileserver
CONFIG_FOLDER: "./"
- SERVICE: qgis_print_service
CONFIG_FOLDER: "./printserver/"
- SERVICE: testwfs
CONFIG_FOLDER: "./testwfs/"
Eine Vorlage „.build-template“ kapselt generische Schritte wie das Bereitstellen des Quellcodes und das Erstellen der Container-Images. Die Pipeline prüft mit Regeln, ob sich Dateien im jeweiligen Service-Unterordner geändert haben. Nur bei tatsächlichem Änderungsbedarf wird ein Build-Job (build oder build-hotfix) gestartet, sodass unnötige Ausführungen vermieden werden. Die Variable „IMAGE_TAG“ sorgt für eine eindeutige Versionskennung der Images. Die Ausführung des Build-Jobs erfolgt auf dem Standard-Branch automatisch und auf Hotfix-Branches (für dringende Korrekturen) manuell.
.build-template:
image: docker:latest
extends: .parallel
services:
- docker:dind
before_script:
- set -x
script:
- docker build -t $CONTAINER_REGISTRY/$SERVICE:${BUILD_TAG} service/$SERVICE/
- docker push $CONTAINER_REGISTRY/$SERVICE:${BUILD_TAG}
build:
stage: build
extends: .build-template
rules:
- changes:
- "service/$SERVICE/**/*"
if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
BUILD_TAG: "$IMAGE_TAG"
build-hotfix:
stage: build-hotfix
extends: .build-template
rules:
- if: $CI_COMMIT_BRANCH =~ /^hotfix-.*$/
variables:
BUILD_TAG: "hotfix-$IMAGE_TAG"
Nach erfolgreichem Build-Job folgt der Deployment-Schritt. Der GitLab Runner klont das Infrastruktur-Repository, aktualisiert die Compose-Datei mit dem neuen Image-Tag und schreibt die Änderung als Commit zurück. So entsteht ein push-basierter Workflow, der jede Infrastrukturänderung nachvollziehbar dokumentiert.
.deploy-template:
image: docker:latest
extends: .parallel
services:
- docker:dind
resource_group: deploy-infrastructure
before_script:
- set -x
- apk add --no-cache git
- git config --global user.email "$MAINTAINER_EMAIL"
- git config --global user.name "CI_TOKEN"
- git config --global user.password "$CI_TOKEN"
script:
- git clone $INFRASTRUCTURE_REPO
- cd $INFRASTRUCTURE_REPO_NAME/$CONFIG_FOLDER
- sed -i "s|/$SERVICE:.*|/$SERVICE:${DEPLOY_TAG}|g" docker-compose.$TARGET_ENV.yml
- cat docker-compose.$TARGET_ENV.yml
- ls -al && git status
- git commit -a -m "CI-Deploy $TARGET_ENV Update $SERVICE image tag to ${DEPLOY_TAG}"
- git push
.deploy:
stage: deploy
extends: .deploy-template
variables:
DEPLOY_TAG: "$IMAGE_TAG"
.deploy-hotfix:
stage: deploy-hotfix
extends: .deploy-template
variables:
DEPLOY_TAG: "hotfix-$IMAGE_TAG"
In beiden Deploy-Job Vorlagen, welche von .deploy-template erben bleibt die Logik identisch, lediglich das Tag-Format und die Auslösebedingungen unterscheiden sich. Die Variable „IMAGE_TAG“ versieht das erzeugte Container-Image mit einer eindeutigen Versionskennung. Jede Änderung an der Infrastrukturdefinition wird per Git-Commit festgehalten. Sofern die entsprechenden Images noch in der Registry vorhanden sind, kann ein Rollback jederzeit durchgeführt werden, entweder mit dem Befehl git revert <commit-hash>
oder indem der Deploy-Job eines früheren Commits erneut ausgeführt wird. Sind die Images nicht mehr verfügbar, sollte zunächst der Build-Job für den gewünschten Commit erneut gestartet werden.
Die Deployments für die verschiedenen Umgebungen (also Test, Staging, Produktion sowie die jeweiligen Hotfix-Deployments) werden jeweils als eigene Jobs entsprechend der definierten Stages in der GitLab CI/CD-Pipeline angelegt. Diese Jobs nutzen die beschriebenen Vorlagen .deploy und .deploy-hotfix und erben damit alle notwendigen Einstellungen und Skripte.
Beispielhaft sieht das für die Testumgebung so aus:
deploy-test:
extends: .deploy
Für Hotfixes in der Testumgebung entsprechend:
deploy-hotfix-test:
extends: .deploy-hotfix
Nach diesem Muster werden auch die Deployments für Staging und Produktion sowie deren Hotfix-Varianten definiert. Dadurch können alle Deployments für die jeweiligen Services und Umgebungen konsistent und nachvollziehbar ausgeführt werden. Die Deploy-Jobs für den Entwicklungsserver werden automatisch getriggert und die Jobs für Staging und Produktiv werden manuell gestartet.
Das Ausrollen neuer Container-Versionen auf den Zielservern kann, je nach Umgebung, automatisiert oder manuell erfolgen:
git pull
durchgeführt, anschließend mit docker compose up -d
die aktualisierten Dienste im Hintergrund neu gestartet und falls notwendig weitere Aufgaben wie das Ausführen von Datenbankmigrationen erledigt. So sind diese Umgebungen immer automatisch auf dem neuesten Stand, ohne dass ein manueller Eingriff erforderlich ist.Voraussetzungen für beide Varianten:
Ein Ansible-Playbook ist eine strukturierte Sammlung von Aufgaben, die auf Zielsystemen automatisiert ausgeführt werden. Wird es regelmäßig per Cronjob gestartet, kann es beispielsweise Konfigurationsdateien aktualisieren, Software bereitstellen oder Dienste neu starten. In diesem Fall sorgt das Playbook dafür, dass Änderungen aus dem Infrastruktur-Repository erkannt und die gewünschte Systemkonfiguration sowie aktuelle Images automatisch angewendet werden.
Beispiel für einen Cronjob-Eintrag:
Ein typischer Cronjob, der alle fünf Minuten das Ansible-Playbook zur Aktualisierung startet, könnte so aussehen:
*/5 * * * * /bin/bash -c "/var/<project_name>/deploy_test.sh"
Der vollständige Workflow gliedert sich in drei Phasen, die nahtlos ineinandergreifen:
1. Änderungserkennung und Build:
Die Pipeline prüft, ob sich Dateien im jeweiligen Service-Unterverzeichnis (service/$SERVICE) geändert haben.
2. Compose-Datei aktualisieren und Commit:
Im Deploy-Job klont der GitLab Runner das Infrastruktur-Repository, wechselt in das konfigurierte Verzeichnis und aktualisiert das Image-Tag in der passenden Compose-Datei. Die Änderung wird als Commit zurück ins Repository geschrieben. Damit ist die Infrastrukturkonfiguration stets versioniert und nachvollziehbar.
3. Ausrollen auf dem Server:
Das eigentliche Ausrollen der neuen Container-Versionen erfolgt entweder automatisiert mit einem per Cronjob gestartete Ansible-Playbook (Entwicklung) oder manuell (Staging und Produktion).
Mit diesem Workflow, bestehend aus GitLab Runner für den Build- und Deploy-Job sowie dem per Cronjob gestartete Ansible-Playbook oder manueller Ausführung für das Ausrollen, bleibt jederzeit ersichtlich, welche Service-Version in welcher Umgebung aktiv ist. Ein Zurücksetzen gelingt einfach durch das Rückgängig machen des entsprechenden Commits im Infrastruktur-Repository.
Ein zentrales Infrastruktur-Repository, modulare Vorlagen für Continuous Integration und Continuous Deployment und ein klarer, push-basierter Workflow schaffen Transparenz, Nachvollziehbarkeit und automatisierte Bereitstellungen für Entwicklungs- und Staging-Umgebungen. In der Produktion sorgt ein manueller Schritt für maximale Kontrolle. Der Ansatz setzt auf bewährte Automatisierungswerkzeuge wie GitLab Runner und Ansible. So entstehen konsistente Rollouts, einfache Zurücksetzungen und minimale Ausfallzeiten mit klaren Prozessen und hoher Flexibilität.