Software-Tipps
Modernes Deployment: Infrastruktur-Repository und GitLab-CI/CD-Pipeline
Einleitung
Die konsequente Trennung von Infrastrukturdefinition und Anwendungscode ist ein zentrales Prinzip moderner Softwareentwicklung. In mehreren Projekten der WhereGroup setzen wir dieses Prinzip um, indem sämtliche Docker-Compose-Konfigurationen, Umgebungsvariablen und Deployment-Skripte in einem zentralen Infrastruktur-Repository verwaltet werden. In diesem Blogartikel erläutere ich den Aufbau dieses Repositories, die Funktionsweise der GitLab-Continuous-Integration- und Continuous-Deployment-Pipeline sowie den Ablauf des Ausrollens von Änderungen mit bewährten Automatisierungswerkzeugen.
Zentrale Idee: Infrastruktur-Repository und Services-Repository
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 Microservice im Ordner „service“ ein eigenes Unterverzeichnis mit Dockerfiles, Skripten und Konfigurationsfragmenten. 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 Microservice, die anschließend vom GitLab Runner automatisch ausgeführt werden.
Die Verzeichnisstruktur ist dabei klar gegliedert:
.
├── infrastructure/
└── services/
├── service/
└── .gitlab-ci.yml
Aufbau des Infrastruktur-Repositories
Im Infrastruktur-Repository befinden sich im Wurzelverzeichnis die Docker-Compose-Dateien für verschiedene Zielumgebungen sowie Vorlagen und Konfigurationsdateien. Jeder Dienst, wie beispielsweise „printserver“ oder „testwfs“, hat 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.
Die GitLab CI/CD-Pipeline: Build und Deploy

Der Build-Job und der Deploy-Job übernehmen im Automatisierungsprozess unterschiedliche Aufgaben. Dabei erstellt der Build-Job aus dem Quellcode eines Microservices 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 Infrastruktur 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 .buil-template.
Stages
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. So bleibt der gesamte Prozess nachvollziehbar und kontrollierbar.
stages:
- build
- deploy-test
- deploy-staging
- deploy-prod
- build-hotfix
- deploy-hotfix-test
- deploy-hotfix-staging
- deploy-hotfix-prod
Parallel
In der Vorlage .parallel sorgt das GitLab-Feature parallel dafür, dass mehrere Jobs gleichzeitig ausgeführt werden können. Jeder dieser Jobs ist für einen bestimmten Dienst (SERVICE) und das zugehörige Konfigurationsverzeichnis (CONFIG_FOLDER) zuständig. Durch diese parallele Ausführung lassen sich die einzelnen Microservices 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. Jeder Eintrag in der Matrix entspricht einem eigenen Job mit den jeweils angegebenen Parametern.
.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/"
Build-Jobs
Eine Vorlage „.build-template“ kapselt generische Schritte wie das Bereitstellen des Quellcodes und das Erstellen der Container-Images. Die Pipeline prüft mit sogenannten 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 sogenannten 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"
Deploy-Jobs
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. Ein Rollback ist jederzeit mit dem Befehl git revert <commit-hash> möglich. Alternativ kann, sofern die entsprechenden Images noch in der Registry vorhanden sind, auch der Deploy-Job eines früheren Commits erneut ausgeführt werden. 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
# weitere Konfigurationen wie stage, environment, etc.
Für Hotfixes in der Testumgebung entsprechend:
deploy-hotfix-test:
extends: .deploy-hotfix
# weitere Konfigurationen wie stage, environment, etc.
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.
Deployment auf den Zielservern: Automatisiert und Manuell
Das Ausrollen neuer Container-Versionen auf den Zielservern kann, je nach Umgebung, automatisiert oder manuell erfolgen:
- Automatisiertes Deployment (Entwicklung):
Auf dem Entwicklungs-Server übernimmt ein Cronjob, der regelmäßig ein Ansible-Playbook ausführt, die Aktualisierung der Umgebung. In festgelegten Intervallen prüft dieses Playbook, ob Änderungen im geklonten Infrastruktur-Repository vorliegen. Falls ja, wird ein 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. - Manuelles Deployment (Staging und Produktion):
In der Staging- und Produktionsumgebung empfiehlt sich ein manuelles Vorgehen. Dabei werden die gleichen Schritte wie beim automatisierten Deployment ausgeführt, jedoch gezielt und einzeln angestoßen. So können mögliche Fehler nach jedem Schritt frühzeitig erkannt und behoben werden, bevor sie sich auf das gesamte System auswirken. Dieses Vorgehen gewährleistet maximale Kontrolle über den Rollout und hilft, unerwünschte Änderungen zu vermeiden. Je nach Umgebung und weiteren Anforderungen kann es sinnvoll sein, ergänzende Skripte einzusetzen, um Fehlerquellen weiter zu minimieren.
Voraussetzungen für beide Varianten:
- Das Infrastruktur-Repository wurde vorab auf dem Zielserver geklont.
- Docker und Docker Compose sind installiert.
- Der Zugriff auf die Container-Registry ist korrekt eingerichtet.
Was macht das Ansible-Playbook im Cronjob?
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 Deployment-Ablauf – Zusammengefasst
Der vollständige Workflow gliedert sich in drei Phasen, die nahtlos ineinandergreifen:
- Änderungserkennung und Build:
Die Pipeline prüft, ob sich Dateien im Verzeichnis service/$SERVICE geändert haben. Nur bei Bedarf wird ein Build-Job gestartet, was Ressourcen spart und die Übersichtlichkeit erhöht. - 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. - 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ängigmachen (Revert) des entsprechenden Commits im Infrastruktur-Repository.
Fazit
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.
Über den Autor

Steckbrief
Marwin Ludwig hat 2023 seine Ausbildung zum Fachinformatiker für Anwendungsentwicklung bei der WhereGroup GmbH in Bonn abgeschlossen. Neben der Entwicklung von MapComponents ist er auch an weiteren Projekten beteiligt.