Manchmal soll es einfach schnell gehen. Ob für ein privates Projekt oder einen Proof of Concept im professionellen Umfeld, als Entwickler*innen wollen wir (und unsere Kunden) möglichst früh Resultate sehen, ohne uns allzu lange mit dem Aufsetzen des “Drumherum” zu beschäftigen. Gleichzeitig möchten wir aber auch nicht mehr auf den Komfort von professionellen Entwicklungswerkzeugen wie einer Versionsverwaltung oder eines Continuous Integration Systems verzichten.

Als Alternative zur aufwändigen Integration dieser Entwicklungswerkzeuge in eine bestehende Corporate IT Struktur oder das Bereitstellen als teure Cloud Services bietet sich als eine Art “schnelle Eingreiftruppe” an: das Bereitstellen einer kompletten End-to-End Infrastruktur zum Entwickeln von Anwendungen aus einer Hand.

Als Hardwarebasis für eine solche Infrastruktur kann ein kleiner und dadurch günstiger Server bei einem Hoster (z.B. Host Europe) oder in der eigenen Infrastruktur dienen.

Sehen wir uns also einmal näher an, wie sich bereits mit kleinem Geld und moderater Hardware eine Menge erreichen lässt.

Einordnung

Auch wenn wir als Anwender häufig über sie schimpfen, so hat Corporate IT mit ihren Vorgaben bzgl. erlaubter Komponenten, Sicherheitskonzepten, Berechtigungsvergaben und Registrierung von Diensten durchaus ihre Daseinsberechtigung.

Die hier skizzierte Lösung stellt eine Möglichkeit dar, einfach zu starten und mit wenig Aufwand einen Zustand zu erreichen, in dem ein schnelles Iterieren und das Präsentieren erster Ergebnisse mit minimalem Aufwand zu erzielen ist.

Spätestens sobald die entwickelten Anwendungen produktiv genommen werden, sollte der Übergang in eine mit dem Betrieb abgestimmte und abgenommene Infrastruktur vollzogen werden. Aber bis dahin wollen wir uns auf das schnelle und einfache Setup konzentrieren.

Anforderungen

Als professionelle Softwareentwickler*innen benötigen wir eine Reihe an Werkzeugen, um unseren Entwicklungsprozess schnell und effektiv abbilden zu können. Im Rahmen unseres Beispiels beinhaltet das:

  • Ein Repository, in dem wir den Quellcode und dazugehörige Konfigurationen sowie die erstellten Binaries unserer Anwendungen versioniert ablegen können.
  • Ein Continuous Integration und Continuous Deployment System, mit dem wir sowohl den Quellcode in eine ausführbare Anwendung überführen können, als auch diese Anwendung in eine Laufzeitumgebung deployen können.
  • Eine Laufzeitumgebung, in der unsere Anwendung läuft und von Benutzern aufgerufen werden kann. Um nicht nur unsere eigene Anwendung, sondern auch zusätzliche Services schnell und einfach nutzen zu können, sollte diese Laufzeitumgebung in der Lage sein, Docker Container als Ausgangsformat zu nutzen.

Komponenten

Alle unsere Anforderungen lassen sich glücklicherweise durch moderne und vielfach bewährte Open-Source-Komponenten abdecken, so dass wir außer den Kosten für den Server selbst (und unsere Arbeitskosten dieses aufzusetzen) erst einmal keine weiteren Ausgaben zu tätigen haben.

Als Continuous Integration und Continuous Deployment System mit bereits integriertem Quellcode Repository und Docker Image Repository werden wir GitLab verwenden. Als Zielsystem nutzen wir K3S, was uns eine leichtgewichtige und nahezu wartungsfreie Kubernetes-Installation zur Verfügung stellt. Der Einfachheit halber deployen wir die GitLab-Instanz selbst auch direkt in Kubernetes.

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten

Installation

Server

Als Basis für die Installation weiterer Komponenten verwenden wir Ubuntu Linux, das üblicherweise bei allen bekannten Hostern direkt verfügbar ist.

Zur späteren Ausstellung von TLS-Zertifikaten sowie dem direkten Zugriff gehen wir davon aus, dass der Server unter einer öffentlichen IP-Adresse und einer öffentlichen (Sub-)Domain verfügbar ist. Ein Betreiben in einem privaten Netzwerk ist selbstverständlich auch möglich, die hier präsentierten Automatismen zur Ausstellung von TLS-Zertifikaten funktionieren dann allerdings nicht bzw. nur mit einigem Zusatzaufwand.

Falls keine eigene (Sub-)Domain verfügbar ist, können auch bei einem freien Dienstleister (wie z.B. https://freedns.afraid.org) die entsprechenden DNS-Einträge für eine Subdomain angelegt werden. Für dieses Beispiel werden wir drei Subdomains nutzen (die alle unter FreeDNS reserviert wurden): “eingreiftruppe.onetoone.cl” als direkte Zuordnung zu unserem Server, “gitlab.eingreiftruppe.onetoone.cl” für den Zugriff auf die GitLab-Installation und “apps.eingreiftruppe.onetoone.cl” zum Deployment bzw. zur Bereitstellung der UIs unserer eigenen Anwendungen.

Kubernetes

Nach dem Aufsetzen des Betriebssystems können wir direkt zur Installation von K3S übergehen, die durch ein simples Kommando schnell erledigt ist:

$ curl -sfL https://get.k3s.io | sh -

Nach dieser Installation (die normalerweise innerhalb einiger Sekunden beendet ist) haben wir auf dem Server eine voll funktionsfähige Kubernetes-Laufzeitumgebung, bei der unser Server sowohl als Controlpane als auch als Node für Workloads agiert:

$ kubectl get nodes

NAME STATUS ROLES AGE VERSION
eingreiftruppe.onetoone.cl Ready control-
plane,master 3s v1.28.4+k3s2

Zur Organisation unserer Services innerhalb von Kubernetes legen wir zwei Namespaces an: Im “gitlab”-Namespace werden wir den GitLab-Server sowie dazugehörige Ressourcen deployen, im “apps”-Namespace unsere eigentlichen Anwendungen.

TLS-Zertifikate

Nachdem Kubernetes aufgesetzt und einsatzbereit ist, richten wir als erstes den Kubernetes Cert-Manager ein, der uns mit minimaler Konfiguration valide TLS-Zertifikate zur Verfügung stellt.

Die Installation ist durch das Hinzufügen des Kubernetes-Deskriptoren erledigt:

$ kubectl apply -f https://github.com/cert-manager/cert-
manager/releases/download/v1.13.3/cert-manager.yaml

Als nächstes definieren wir die Authority, von der wir unsere TLS-Zertifikate ausgestellt haben möchten. Hierzu nutzen wir den ACME Issuer von Let’s Encrypt. Die Konfiguration kann direkt als Kubernetes Deployment Descriptor im Cluster definiert werden:

Sobald der Issuer selbst existiert, können wir die Zertifikate anfordern. Auch hier müssen wir lediglich zwei Kubernetes Deployment Deskriptoren im Cluster definieren:

Das Zertifikat für “gitlab.eingreiftruppe.onetoone.cl” legen wir im Namespace “gitlab” an, das Zertifikat für “apps.eingreiftruppe.onetoone.cl” im Namespace “apps”.

Das Ausstellen der eigentlichen TLS-Zertifikate übernimmt nun der Cert-Manager: Nach kurzer Zeit sind die entsprechenden Zertifikate in den definierten Secrets “tls-gitlab.eingreiftruppe.onetoone.cl” im Namespace “gitlab” bzw. “tls-apps.eingreiftruppe.onetoone.cl” im Namespace “apps” vorhanden.

GitLab-Server

Nun können wir als erste Applikation unsere GitLab-Installation in den Kubernetes Cluster deployen. GitLab stellt hierzu ein Docker Image zur Verfügung, das wir als Basis für ein Kubernetes-Deployment verwenden können.

Der vollständige Satz an Deployment-Deskriptoren sieht dann wie folgt aus:

Die erstmalige Installation dauert einige Minuten, in der Zeit sehen wir uns die einzelnen Deployment-Deskriptoren etwas genauer an:

Die Konfiguration der GitLab-Instanz findet über die Datei “gitlab.rb” statt, die wir als Kubernetes ConfigMap hinterlegen, um sie später schnell und einfach im Deployment verwenden zu können.

Als erstes definieren wir die externe URL, unter der die GitLab-Installation verfügbar sein soll. Im nächsten Schritt ändern wir das Standardverhalten von GitLab und lassen den Server nur einen regulären HTTP Port 80 und keinen sicheren HTTPS Port 443 aufbauen. Der eigentliche Verbindungsaufbau über TLS (und TLS Offloading) wird uns vom Traefik Ingress Controller abgenommen, der automatisch mit K3S installiert wurde. Innerhalb des Clusters benötigen wir daher nur einen regulären, nicht sicheren Port.

Im zweiten Teil aktivieren wir die interne Container Registry von GitLab, so dass wir später unsere Docker Images direkt in GitLab speichern können. Da die Registry direkt über Port 5050 bereitgestellt wird (und daher nicht über den Traefik Ingress Controller läuft) müssen wir hier die TLS-Konfiguration selbst übernehmen. Wir nutzen hierzu das über den Cert-Manager generierte TLS-Zertifikat für unsere GitLab-Domain.

Der Pfad “/certificates/tls-gitlab.eingreiftruppe.onetoone.cl/” wird hierbei im Deployment aus dem Secret in den Container gemountet, so dass die Zertifikatsdateien direkt vom Filesystem gelesen werden können.

Im Deployment definieren wir zunächst eine Reihe von Volumes und mounten diese in den Container. Das Ziel hierbei ist, die von GitLab gespeicherten Daten und Konfigurationen dauerhaft auf dem Server zu sichern und so sicherzustellen, dass sie auch nach einem Neustart des Pods zur Verfügung stehen. Ebenso mounten wir die über die ConfigMap definierte Konfigurationsdatei sowie das oben beschriebene TLS Secret in den Container.

Im Anschluss definieren wir den eigentlichen Docker Container, der den GitLab-Server zur Verfügung stellt.

Über die beiden Services “gitlab-server” und “gitlab-registry” erlauben wir den Zugriff auf die GitLab-Oberfläche (Port 80) bzw. die Container Registry (Port 5050). Den Service für die Container Registry definieren wir als “LoadBalancer”, was bewirkt, dass Port 5050 nicht nur innerhalb von Kubernetes für andere Kubernetes-Ressourcen zu erreichen ist, sondern öffentlich vom Server bereitgestellt wird. Somit können wir nicht nur aus dem Cluster heraus auf die Container Registry zugreifen, sondern auch von externen Maschinen (z.B. Entwicklerrechnern) auf die Images in der Registry zugreifen.

Über einen Ingress erlauben wir schließlich einen Zugriff auf den eigentlichen Server über das Internet, konkret über die Domain “gitlab.eingreiftruppe.onetoone.cl”.

Nach einem kubectl apply werden alle GitLab-Ressourcen im Cluster erstellt und hochgefahren:

$ kubectl apply -f gitlab.yml

configmap/gitlab-server created
deployment.apps/gitlab-server created
service/gitlab-server created
service/gitlab-registry created
ingress.networking.k8s.io/gitlab-server created

Die erstmalige Initialisierung dauert eine ganze Weile, aber im Anschluss können wir über die Server-URL (in unserem Beispiel “https://gitlab.eingreiftruppe.onetoone.cl”) auf den GitLab-Server zugreifen.

Abbildung Entwicklungsumgebung aufsetzen - GitLab-Server

Ein zufälliges Passwort für den “root”-Benutzer wurde während der Installation automatisch erstellt, wir können es über Kubernetes direkt aus dem laufenden GitLab-Server extrahieren:

$ kubectl exec --namespace gitlab deployment/gitlab-server -- cat
/etc/gitlab/initial_root_password | grep "Password:"

Mit dem Benutzernamen “root” und dem so erhaltenen Passwort können wir uns nun bei GitLab anmelden:

Abbildung Entwicklungsumgebung aufsetzen - Welcome to GitLab

Hier haben wir nun die Möglichkeit, zusätzliche Benutzer und/oder Projekte anzulegen und diese Benutzer in Benutzer- und Projektgruppen zu sortieren.

GitLab Registry Benutzer

Einen neuen Benutzer müssen wir auf jeden Fall anlegen. Die von GitLab bereitgestellte Container Registry lässt den Zugriff auf die dort hinterlegten Images nur nach einem vorherigen Login zu. Der Kubernetes Cluster, in den wir später unsere Anwendungen deployen wollen, muss auf die Registry zugreifen, um die dort hinterlegten Images abzurufen. Wir benötigen also einen Servicebenutzer für Kubernetes.

Den Benutzer können wir über (“Search or go to…” -> “Admin Area” -> “Overview” -> “Users” -> “New user”) anlegen.

Abbildung Entwicklungsumgebung aufsetzen - GitLab Regestry Benutzer

Nachdem wir den Benutzer angelegt, uns mit ihm angemeldet und sein Passwort geändert haben, melden wir uns nun über “docker login” an der Container Registry an:

$ docker login gitlab.eingreiftruppe.onetoone.cl:5050

Username: kubernetes
Password:
Login Succeeded

Das Login-Token befindet sich jetzt in der Datei “~/.docker/config.json”:

$ cat ~/.docker/config.json
{
"auths": {
"gitlab.eingreiftruppe.onetoone.cl:5050": {
"auth": "a...l"
}
}
}

Den gesamten Bereich “auths” kopieren wir nun in ein Kubernetes Secret und fügen es mit kubectl apply dem Cluster hinzu:

Über dieses Secret (bzw. die dort enthaltenen Credentials) kann Kubernetes sich an der Image Registry identifizieren, um dort enthaltene Images abzurufen.

Beispielprojekt

Unsere GitLab-Installation ist jetzt funktionstüchtig. Wir wollen daher ein erstes Beispielprojekt erstellen, ein klassisches “Hallo Welt”-Programm. Unser Ziel ist es, beim Aufruf der URL “https://apps.eingreiftruppe.onetoone.cl/hallo-welt.html” eine Begrüßung zu erhalten.

Für diese Anwendung erstellen wir zunächst eine einfache HTML-Datei “hallo-welt.html”:

GIST @ https://gist.github.com/perdian/92c6ad68d1eb8fd7290a9d3d9dae43a6

Da unser Ziel ein Docker Container ist, den wir als “Komplettpaket” in unsere Laufzeitumgebung deployen können, verpacken wir unsere HTML-Datei in einen nginx-Webserver, von wo aus sie aufgerufen werden kann. Das entsprechende Dockerfile hierzu sieht wie folgt aus:

Um den Docker Container nach dem Bauen in den Kubernetes Cluster zu deployen und von außen zugreifbar zu machen, benötigen wir noch eine Reihe von Kubernetes-Deskriptoren:

Über das Deployment definieren wir die eigentliche Anwendung, also in unserem Falle das gerade gebaute Docker Image. Der Platzhalter “${CI_REGISTRY_NAME}” wird dabei vom Buildskript mit dem kompletten Pfad des gebauten Images ersetzt, der Platzhalter ${CI_COMMIT_REF_SLUG} mit dem Namen des aktuellen Branches, bevor die Ressourcen von Kubernetes angelegt werden.

Mit dem “imagePullSecret” teilen wir Kubernetes mit, für den Login an der Image Registry den weiter oben angelegten Benutzer “kubernetes” zu verwenden.

Um den Docker Container innerhalb der GitLab-CI-Pipeline zu bauen, müssen wir nun noch das Buildscript definieren:

Nachdem wir alle diese Dateien in ein lokales Git-Repository eingecheckt haben, können wir dieses Repository nun auf unseren eben erstellen Server pushen:

$ git remote add origin https://gitlab.eingreiftruppe.onetoone.cl/root/hallo-
welt.git
$ git push origin main

Als Antwort des Servers erhalten wir die Bestätigung, dass unser Projekt erfolgreich angelegt wurde:

remote:
remote:
remote: The private project root/hallo-welt was successfully created.
remote:
remote: To configure the remote, run:
remote: git remote add origin https://gitlab.eingreiftruppe.onetoone.cl/root/hallo-welt.git
remote:
remote: To view the project, visit:
remote: https://gitlab.eingreiftruppe.onetoone.cl/root/hallo-welt
remote:

Öffnen wir unser Projekt nun über die Weboberfläche unseres GitLab-Servers:

Abbildung Entwicklungsumgebung aufsetzen - Beispielprojekt

Die Inhalte des Repositories wurden erfolgreich übertragen. Wenn wir uns jetzt über “Build -> Pipeline” die CI-Pipeline ansehen, so fällt auf, dass der Status dauerhaft auf “Pending” steht:

Abbildung Entwicklungsumgebung aufsetzen - CI Pipeline

Zusätzlich zum GitLab-Server benötigen wir noch einen GitLab-Runner, der die einzelnen Buildaufträge abarbeitet. Diesen Runner müssen wir zunächst noch als zusätzliche Anwendung in unserem Kubernetes Cluster deployen.

GitLab-Runner

Jeder Runner benötigt ein eindeutiges Token, mit dem er sich am Server authentifizieren kann. Dieses Token erhalten wir, indem wir einen neuen Runner über die Admin-Oberfläche in GitLab registrieren (“Search or go to…” -> “Admin Area” -> “CI/CD” -> “Runners” -> “New instance runner”).

Alle Einstellungen auf der Konfigurationsseite können wir bei den Standardwerten belassen, lediglich die Option “Run untagged Jobs” muss ausgewählt sein, damit der neue Runner alle Jobs aufnimmt und bearbeitet. Nach einem Klick auf “Create runner” ist der Runner erstellt:

Abbildung Entwicklungsumgebung aufsetzen - GitLab-Runner

Wichtig für die weitere Konfiguration ist das unter “Step 1” angezeigte Token (“glrt-…”). Mit diesem Token können wir nun den Runner erstellen.

GitLab bietet verschiedene Runner-Typen, intern “Executors” genannt, an. Diese Executors unterscheiden sich in der Art und Weise wie bzw. wo sie die Buildjobs abarbeiten. Für unser Beispiel werden wir einen Kubernetes Executor nutzen. Dieser Executor erzeugt für jeden Buildjob einen eigenen Pod innerhalb des Kubernetes Clusters und führt den Build innerhalb dieses Pods aus. Für uns hat das den Vorteil, dass wir unsere bereits vorhandene Infrastruktur – den Kubernetes Cluster – weiter nutzen können.

Zusätzlich zur Runner-Instanz müssen noch eine Reihe an weiteren Ressourcen innerhalb des Kubernetes Clusters definiert werden. Sehen wir uns zunächst die kompletten Deployment-Deskriptoren an und gehen diese danach im Detail durch:

Zunächst benötigen wir eine Reihe von Kubernetes Role-based access control (RBAC) Ressourcen. Dadurch, dass der Runner Ressourcen innerhalb von Kubernetes verwalten muss, benötigt er die entsprechenden Rechte dafür. Hierzu erstellen wir einen Kubernetes Service Account mit dem Namen “gitlab-admin”. Mit diesem Service Account wird sowohl der eigentliche Runner gestartet, als auch alle von diesem Runner zur Abarbeitung der Buildjobs erstellten Pods. Über Roles und RoleBindungs erlauben wir diesem Service Account sowohl den Zugriff auf alle Ressourcen im Namespace “gitlab” als auch auf alle Ressourcen im Namespace “apps”.

Über die ConfigMap “gitlab-runner” machen wir die Konfigurationsdatei für den Runner (“config.toml”) verfügbar. Hierin definieren wir sowohl den eigentlichen GitLab-Server, den der Runner kontaktiert, um neue Buildjobs abzurufen und Ergebnisse zurückzuspielen als auch das Token, das wir bei der Registrierung über die Weboberfläche erhalten haben.

In den Runner werden dann noch zwei Filesystem-Ressourcen gemountet:

  • /var/run/docker.sock” mounted den Docker Socket vom Hostsystem (also unserem Server) in die vom Runner gestarteten Pods, so dass die Buildjobs diesen nutzen können, um eigene Docker Container zu bauen.
  • /cache” mounted ein Verzeichnis vom Hostsystem, das die vom Runner gestarteten Pods nutzen können, um wiederholt benötigte Ressourcen dauerhaft speichern zu können.

Nachdem wir die Ressourcen über kubectl apply im Cluster angelegt haben, können wir in der Übersicht sehen, dass der gerade erzeugten Runner sich korrekt am GitLab-Server angemeldet hat und in der Lage ist, Buildaufträge abzuarbeiten:

Abbildung - Runners

Anwendungsdeployment

Nachdem wir nun erfolgreich den Runner deployed haben, können wir zu unserem Anwendungsprojekt zurück wechseln und dort sehen, dass der Buildauftrag nicht länger im Status “Pending” steht, sondern erfolgreich gebaut werden konnte:

Abbildung - Anwendungsdeployment

Auch das Deployment in den Kubernetes Cluster hat für unsere “Hallo Welt”-Anwendung funktioniert:

$ kubectl get pods --namespace apps

NAME READY STATUS RESTARTS AGE
hallo-welt-5b75459d58-vmf4x 1/1 Running 0 2m

Wenn wir nun die URL “https://apps.eingreiftruppe.onetoone.cl/hallo-welt.html” aufrufen, erhalten wir – wie erwartet – unsere Begrüßung:

Abbildung - Begrüßungsfenster-Webanwendung

Eine komplette Entwicklungsumgebung schnell und einfach aufsetzen – Abschluss

Das installierte System kann jetzt weiter auf die eigenen Ansprüche angepasst und abgesichert werden. So sollten zusätzliche Repositories nicht mehr vom “root”-Benutzer, sondern von regulär in GitLab angelegten Benutzern angelegt und verwaltet werden.

Ich konnte aber hoffentlich zeigen, dass nach relativ kurzer Zeit eine vollständige Entwicklungsumgebung inkl. End-to-End Deployment von Anwendungen, die direkt dem Benutzer zur Verfügung stehen, aufgesetzt werden kann. So können wir uns (endlich) wieder auf das konzentrieren, was uns und unsere Kunden nach vorne bringt: Neue Ideen zu realisieren und zu lebendiger Software zu machen.

Titelmotiv: Photo by Christina @ wocintechchat.com on Unsplash

Christian Seifert

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten