CI/CD Multi-Container-Buildchain mit Docker Compose (Teil 1)

CI/CD Multi-Container-Buildchain mit Docker Compose (Teil 1)
Image by rawpixel.com on Freepik

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.

Oliver Lott

Oliver Lott

Vielen Dank fürs Lesen! Benötigen Sie Hilfe? Meine Kooperationspartner und ich helfen Ihnen gerne. Schreiben Sie mir einfach eine Mail oder benutzen Sie das Kontaktformular.