CI/CD Multi-Container-Buildchain mit Docker Compose (Teil 1)
Wenn kleine Projekte größer werden, steht man oftmals vor dem Problem, dass mehrere Services voneinander abhängig gestartet werden müssen. Die Frage, ob der Quellcode dieser Services in einem oder in mehreren Repositories gepflegt werden, ist eine Glaubensfrage und kann verschieden beantwortet werden. Ich habe mich bei der Umsetzung für dieses Projekt für einen Ansatz mit mehreren Repositories entschieden. Sie sind in einer Gruppe in Gitlab zusammengefasst. Die eigentliche Kernapplikation steuert das Deployment. Da die Deployment-Umgebung beim Kunden noch nicht so groß ist, dass sich der Einsatz von Kubernetes lohnt, kommt docker compose zum Einsatz. Dieser Blogartikel soll die Eckpfeiler dieses Konzeptes zeigen. Die Buildchain baut zwei unabhängig voneinander existierende Umgebungen auf, einmal Staging und einmal Production. Diese können sowohl auf einem als auch auf verschiedenen Servern betrieben werden.
Das Konzept
Das Beispielprojekt besteht aus einem Haupt- und drei Microservices. Es gibt eine Staging- und eine Produktionsumgebung. Alle Services sind in getrennten Repositories innerhalb einer Gruppe untergebracht. Eingesetzt wird Gitlab in Zusammenhang mit virtuellen Linuxservern mit Debian 12. Die Staging-Umgebungen werden durch den Branch 'staging' und die Produktionsumgebung durch den Branch 'main' abgebildet. In Letzterem findet eine Versionierung in der Form vx.x.x statt (z.B. v1.1.0). Durch diese Versionierung können dann per Gruppenvariablen die Versionen der verschiedenen Microservices für die Produktionsumgebung definiert werden.
Diese Variablen werden innerhalb der Gruppe in Settings > CI/CD > Variables festgelegt.
Die Buildchain
Herzstück dieses Ansatzes sind zwei Dateien: .gitlab-ci.yml und docker-compose.tmpl. Letztere Datei definiert die Docker-Umgebung mit ihren Services, dem Volume und dem Netzwerk. Sie wird von docker compose interpretiert.
version: "3"
name: cms-backend-${ENV}
services:
app:
container_name: cms-api-${ENV}
image: gitlab.meinedomain.de/cms/cms-api/${CI_COMMIT_REF_NAME}:latest
restart: 'unless-stopped'
ports:
- "${PORT}:3000"
depends_on:
- postgres
- maintain
- mailer
command: ["npm","run","app"]
networks:
- internal
maintain:
container_name: cms-maintain-${ENV}
restart: 'unless-stopped'
image: gitlab.meinedomain.de/cms/cms-maintain/${MAINTAINTAG:-staging}:latest
depends_on:
- postgres
networks:
- internal
mailer:
container_name: cms-mailer-${ENV}
restart: 'unless-stopped'
image: gitlab.meinedomain.de/cms/cms-mailer/${MAILERTAG:-staging}:latest
networks:
- internal
postgres:
container_name: cms-db-${ENV}
restart: 'unless-stopped'
image: gitlab.meinedomain.de/cms/cms-database/${DATABASETAG:-staging}:latest
ports:
- "${PGSQL_MAINTENANCE_PORT}:5432"
expose:
- "5432"
volumes:
- data:/var/lib/postgresql/data
networks:
- internal
networks:
internal:
name: cms-${ENV}
volumes:
data:
name: cms-db-pgdata-${ENV}
Die Variablen in diesem Template werden von der Buildchain eingesetzt.
Variablenname | Zweck |
---|---|
ENV | Umgebung (Production oder Staging) |
CI_COMMIT_REF_NAME | Tag wenn Produktionsumgebung |
MAINTAINTAG | Version des Maintainservices |
MAILERTAG | Version des Mailerservices |
DATABASETAG | Version des Databaseservices |
PGSQL_MAINTENANCE_PORT | Externer Port für PostgreSQL |
PORT | Der Port der Anwendung nach "außen" |
Der Ausdruck ${MAILERTAG:-staging}:latest bezweckt, dass bei Vorhandensein einer definierten Version (in der Produktionsumgebung) wird diese verwendet. Andernfalls wird das Docker-Image aus der Staging-Umgebung genutzt.
Im hier gezeigten Beispiel ist nicht die Möglichkeit gezeigt wichtige Laufzeitvariablen wie z.B. API-Keys oder Verbindungsdaten für die Datenbank zu definieren. Dafür müsste die Datei docker-compose.tmpl erweitert werden. Dafür steht der Key "environment" zur Verfügung. Kommen die Informationen für diesen Key aus CI/CD-Variablen und nicht aus dem Repository, so bleiben sowohl Repository als auch die gebauten Images frei von sensiblen Zugangsdaten. Diese werden dann erst im Deploymentprozess ausgelesen und dann den Deployment-Server übergeben.
Pipeline im Repo oder außerhalb?
Diese Frage ist im Gegensatz zur Entscheidung Multi- oder Monorepo einfach zu beantworten: Wird die Pipeline derzeit oder potenziell in mehreren Projekten gebraucht, empfiehlt es sich die Pipeline in ein eigenes Repository zu legen. Natürlich ist dies auch selbst dann möglich, wenn man nur ein Projekt hat. Daher gehen die weiteren Teile dieses Artikels auf dieses Szenario ein. Dort, wo die Pipeline eigentlich im Repository liegt, wird dann die externe Pipeline eingebunden:
# .gitlab-ci.yml
include:
- project: $GLOBAL_CICD_REPOSITORY_PATH
ref: 'main'
file: '/CICD.gitlab-ci.yml'
variables:
DOCKERFILE: docker/Dockerfile
COMPOSE_TEMPLATE: docker/docker-compose.tmpl
An dieser Stelle wird die globale Pipeline eingebunden. Weiterhin können an dieser Stelle Variablen definiert werden. Allerdings empfiehlt es sich hier nur die Struktur des Repos zu definieren. Variablen, die für das Deployment und die Laufzeit- oder Buildumgebung verantwortlich sind, sollten im Projekt selbst unter CI/CD / Variables definiert werden. Ein Pattern hierfür und wie dieses dann in der Pipeline ausgelesen werden kann, folgt dann in Teil 2.
Ausblick
In Teil 2 dieses Artikels wird die Pipeline vorgestellt. Teil 3 behandelt dann das Deployment in der Praxis und das Debugging.