Die Build-Pipeline mit gitlab
Die Projektablage befindet sich ja in Gitlab.
GitLab.com ist eine webbasierte DevOps-Plattform, die als Software-as-a-Service (SaaS) angeboten wird und auf der gleichnamigen Open-Source-Software GitLab basiert. GitLab ist eine umfassende Plattform für die Softwareentwicklung, die den gesamten Lebenszyklus der Softwareentwicklung von der Planung und Erstellung von Code über dessen Überprüfung, CI/CD (Continuous Integration/Continuous Delivery), Sicherheit, bis hin zum Monitoring und der Veröffentlichung abdeckt. Die Plattform unterstützt Teams dabei, ihre Softwareentwicklungsprozesse zu automatisieren und zu vereinfachen, wodurch die Effizienz gesteigert und die Zeit bis zur Markteinführung (Time-to-Market) verkürzt wird.
Hauptmerkmale von GitLab.com:
Versionskontrolle: GitLab basiert auf Git, dem de-facto-Standard für verteilte Versionskontrollsysteme, und ermöglicht es Teams, ihren Quellcode effektiv zu verwalten, Branches zu erstellen, Merge-Anfragen zu bearbeiten und den Überblick über Änderungen und Beiträge zu behalten.
CI/CD: GitLab bietet integrierte Continuous Integration und Continuous Delivery, wodurch Teams ihre Codeänderungen automatisch bauen, testen und auf verschiedene Umgebungen deployen können. Dies umfasst auch die Unterstützung für Docker und Kubernetes.
Issue Tracking und Projektmanagement: GitLab ermöglicht es Teams, Aufgaben und Issues effizient zu verfolgen, zu organisieren und zu priorisieren. Es bietet Boards ähnlich wie Kanban für eine agile Projektverwaltung.
Codeüberprüfung: Die Plattform unterstützt die Codeüberprüfung und Kommentierung direkt in Merge-Anfragen, was die Qualitätssicherung und Zusammenarbeit zwischen Entwicklern verbessert.
Sicherheit und Compliance: GitLab bietet zahlreiche Sicherheitsfeatures wie statische und dynamische Anwendungssicherheitstests, Dependency-Scanning und Container-Scanning, um Sicherheitsprobleme in Code und Abhängigkeiten frühzeitig zu identifizieren.
Monitoring und Metriken: GitLab erleichtert das Monitoring von Anwendungen und Infrastrukturen mit integrierten Dashboards für Leistungs- und Betriebsmetriken.
Wir verwenden Gitlab für den Bau pingcs und die Bereitstellung als Debian-Package.
Der Prozess hierfür ist mehrstufig.
Als erstes erstellen wir ein Basis-Image in dem sich alle Komponenten befinden die für den Bau der Anwendung notwendig sind.
ARG IMAGE
FROM --platform=linux/amd64 ${IMAGE}
ARG BFLAT
ARG UPX
RUN apt-get update \
&& apt-get -y upgrade \
&& apt-get install -y ca-certificates curl libc++1 xz-utils file
RUN update-ca-certificates
RUN mkdir -p /opt/upx
RUN curl -o /tmp/upx.tar.xz -L ${UPX}
RUN tar --strip-components=1 -xf /tmp/upx.tar.xz -C /opt/upx
RUN rm -f /tmp/upx.tar.xz
RUN mkdir -p /opt/bflat
RUN curl -o /tmp/bflat.tar.gz -L ${BFLAT}
RUN tar -zxf /tmp/bflat.tar.gz -C /opt/bflat
RUN rm -f /tmp/bflat.tar.gz
ENV PATH="/opt/bflat:${PATH}"
ENV PATH="/opt/upx:${PATH}"
RUN bflat -v
RUN upx --version
Die Parameter / Argumente IMAGE, UPX und BFLAT werden von außen, das heißt während der Erzeugung mittels gitlab-ci in das Dockerfile reingereicht und dort verwendet.
Warum gestalten wir diesen Prozess getrennt vom eigentlichen Build der Anwendung?
Der Grund ist, dass sich am Basis-Paket nicht so häufig etwas ändert, bei jedem erzeugen der Anwendung (was ja durchaus sehr oft am Tag sein kann) würde dann aber immer das gleiche Paket (debian, bflat, upx) aus dem Internet heruntergeladen und installiert.
Das Basis-Image kann jetzt, bspw. einmal täglich erzeugt werden und wird bei der ersten Verwendung im Cache des Build-Prozesses abgelegt. Dadurch kann er solange verwendet werden, bis eine neue Version des Basis-Image vorliegt.
Die ausgeführten Schritte sind schnell erklärt.
Das Basis-Image (debian:latest) wird upgedatet (Zeile 7 – 10).
Danach werden entsprechend die Tools UPX (Zeile 12 – 15) und bflat (Zeile 17 – 20) installiert und konfiguriert.
In Zeile 21 und 22 werden entsprechend die Pfad-Variablen gesetzt, damit die Tools direkt, ohne Angabe eines Pfades aufgerufen werden können.
Um zu prüfen ob alles funktioniert, folgen in Zeile 24 und 25 die jeweils direkten Programmaufrufe mit der Ausgabe der Version der Tools.
Damit ist die Erstellung des Basis-Images erledigt.
Aber wie verwende ich jetzt das was da so erstellt wurde?
Bereitstellung des Basis-Images
Wir haben ja erfahren, dass gitlab Dockerfiles verarbeitet bzw. die dort angegebenen Anweisungen ausführt.
Dies geschieht, in dem wir dem Projekt eine Script-Datei hinzufügen, in dem die Anweisungen stehen, die gitlab „versteht“. Die Script-Datei heißt .gitlab-ci.yml. Der Teil, der das Basis-Image für bflat und upx bereitstellt sieht wie folgt aus.
deployBFlat:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
variables:
PLATFORM: x64
BUILD_PLATFORM: $PLATFORM_X64
BFLAT_DOWNLOAD_URL: https://github.com/bflattened/bflat/releases/download/v8.0.1/bflat-8.0.1-linux-glibc-x64.tar.gz
UPX_DOWNLOAD_URL: https://github.com/upx/upx/releases/download/v4.2.2/upx-4.2.2-amd64_linux.tar.xz
before_script:
- cp -f "${CI_PROJECT_DIR}/config.json" /kaniko/.docker/config.json
script:
- /kaniko/executor
--context ${CI_PROJECT_DIR}
--cache
--dockerfile ${CI_PROJECT_DIR}/docker/Dockerfile_X64_Bflat
--destination ${DOCKER_IO_USER}/${PROJECT_NAME}_${PLATFORM}_bflat:${TAG}
--build-arg IMAGE=debian:latest
--build-arg BFLAT=${BFLAT_DOWNLOAD_URL}
--build-arg UPX=${UPX_DOWNLOAD_URL}
Ich gehe bei den Anweisungen nur auf den script-Block (ab Zeile 13) ein. Alles weitere zu Befehlen und Anweisungen einer gitlab-ci.yml ist durch gitlab ausreichend kommentiert bzw. findet man überall im Internet ausreichend Tutorials.
Es gibt unterschiedliche Möglichkeiten aus einem Dockerfile ein Image zu erzeugen. Früher hat man hier einen Service verwendet. Dieser nannte sich Docker-In-Docker (:dind). Dafür musste man sich, innerhalb eines Docker-Containers gegen einen Docker-Service verbinden (meistens erkennbar an der lustigen Zeile in der .gitlab-ci.yml DOCKER_SERVICE:tcp://docker:2375). Dies brachte jedoch gleich mehrere Nachteile mit sich.
- Sicherheit: Die Ausführung eines Docker-Daemons innerhalb eines Containers erfordert privilegierte Berechtigungen für den Container, was aus Sicherheitssicht problematisch sein kann.
- Komplexität: Die Konfiguration und Verwaltung von DinD kann komplex sein, insbesondere in Bezug auf Netzwerk- und Speicherressourcen.
- Portabilität: Nicht alle Kubernetes-Cluster unterstützen privilegierte Container, was die Portabilität von Lösungen einschränkt, die DinD verwenden.
- Performance: Docker-in-Docker ist nicht optimiert für den Bau von Images. Andere Services optimieren den Build, indem es Schichten-Caching unterstützt, was die Bauzeiten beschleunigen kann.
Anyway, wir bauen mit Kaniko!
Kaniko bietet gegenüber Docker-in-Docker mehrere Vorteile, vor allem in Bezug auf Sicherheit, Einfachheit und Portabilität. Es ermöglicht das Bauen von Docker-Images in Umgebungen, wo der Betrieb eines Docker-Daemons nicht ideal oder sogar unmöglich ist, wie z.B. in streng gesicherten Kubernetes-Clustern. Diese Vorteile machen Kaniko zu einer bevorzugten Wahl immer dann, wenn effiziente und sichere Wege gesucht werden, um Container-Images in CI/CD-Pipelines zu bauen.
Der build mit Kaniko läuft in vielen Dingen gleich ab, wie mit Docker-In-Docker. Der Kaniko-Executor bekommt über unterschiedliche Parameter die Build-Anweisungen eingereicht und stellt über den Parameter destination ein Image in einer Container-Registry bereit, in diesem Fall Docker-Hub.
Aber wer oder was baut denn jetzt eigentlich?
Gitlab.com ist ja in erster Linie eine Website, die neben der verteilten Versionskontrolle u.a. CI/CD-Funktionalitäten bereitstellt. Allerdings müssen diese CI/CD-Funktionalitäten mittels Ressourcen bereitgestellt werden. Ressourcen in Form von CPU, Speicher usw.
Hierfür gibt es die sogenannten gitlab-Runner.
GitLab Runner ist ein essentieller Bestandteil der GitLab CI/CD-Pipeline, der für die Ausführung von Jobs und die Bereitstellung von Ergebnissen an die GitLab-Instanz verantwortlich ist. Es handelt sich um eine Open-Source-Anwendung, die in verschiedenen Umgebungen installiert und betrieben werden kann, um die Automatisierung von Builds, Tests und Deployments im Rahmen der Continuous Integration und Continuous Delivery (CI/CD) zu unterstützen.
GitLab Runner arbeitet, indem es sich mit einem GitLab-Server verbindet, auf dem Ihr Projekt gehostet ist. Sobald eine CI/CD-Pipeline ausgelöst wird, etwa durch ein Git-Push-Event, weist GitLab einem Runner die Ausführung spezifischer Jobs zu, die in der .gitlab-ci.yml-Datei des Projekts definiert sind. Diese Jobs können Build-Skripte, Test-Suites oder Deployment-Skripte umfassen.
Wir verwenden unsere eigenen Gitlab Runner Instanzen. Alternativ können auch Shared-Runner von gitlab selbst verwendet werden. Diese sind jedoch in der Nutzungszeit limitiert (monatlich 400 Minuten in der kostenfreien Version).
Wir bauen unsere Anwendung
Basierend auf dem Basis-Image, dass ja alle Tools enthält, die wir für die Bereitstellung unserer nativ kompilierten Anwendung benötigen, beginnen wir jetzt mit dem Build!
Wir werden jetzt nicht für jede Architektur und Betriebssystem eine native Ausführung unserer Ping-Anwendung bauen, sondern für
- Windows x64
- Linux x64
Mit der nativ gebauten Anwendung für Linux x64 haben wir im nächsten Kapitel noch was besonderes vor.
Aber erst mal hier weiter machen.
Im folgenden schauen wir uns die .gitlab-ci.yml mit den Buildanweisungen an.
image: laszlo/containerruntimeglobal_x64_bflat:23.04_8.0.1
stages:
- build
- deploy
build-job-windows-x64:
stage: build
script:
- cd ./pingcs
- bflat build
-Os
-o pingcs.exe
--os:windows
--stdlib:dotnet
--arch:x64
--no-stacktrace-data
--no-reflection
--no-globalization
--no-exception-messages
--separate-symbols
--no-debug-info
- ls -lah
- upx --brute --lzma --overlay=strip ./pingcs.exe
- file ./pingcs.exe
artifacts:
paths:
- pingcs/pingcs.exe
expire_in: 1 week
build-job-debian-x64:
stage: build
script:
- cd ./pingcs
- bflat build
-Os
-o pingcs
--os:linux
--stdlib:dotnet
--arch:x64
--no-stacktrace-data
--no-reflection
--no-globalization
--no-exception-messages
--separate-symbols
--no-debug-info
- upx --brute --lzma --overlay=strip ./pingcs
- file ./pingcs
- ./pingcs
- ./pingcs 127.0.0.1 t=5 c=0.5
artifacts:
paths:
- pingcs/pingcs
expire_in: 1 hour
Für den Bild der nativen Anwendung verwenden wir unser (oben im Kapitel beschrieben) Basis-Image (Zeile 1).
Und legen 2 Stages als Anweisungsreihenfolge für den gitlab-Runner fest (Zeile 2 – 4).
Build-Stage für native Windows-App
In Zeile 6 – 28 erfolgt die Build-Anweisung für die native Windows-Anwendung. Dafür konfigurieren wir bflat mit den entsprechenden Parametern. Entscheidend für den Windows-x64-Build sind die Parameter –arch (Zeile 15) und –os (Zeile 13).
Der Rest sind Compiler-Flags (Zeilen 16 -21). Als Standard-Library verwenden wir Dotnet.
Im nächsten Schritt verwenden wir das Tool upx um die gebaute, native Executable noch weiter zu verkleinern (Zeile 23). Interessant ist hier die Packdichte. Der Logausgabe des Build-Vorgangs können wir folgendes entnehmen.
$ upx --brute --lzma --overlay=strip ./pingcs.exe
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser
Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
1121792 -> 468992 41.81% win64/pe pingcs.exe
Packed 1 file.
Das Tool ist wirklich effektiv, da er das Endergebnis auf 41.81% des Originals verkleinert.
Zu guter Letzt prüfen wir mit file den Dateityp der gerade gebauten Anwendung. Hierfür wird der Executable Header der Datei ausgewertet und sollte für Windows ungefähr so aussehen.
./pingcs.exe: PE32+ executable (console) x86-64, for MS Windows, 3 sections
Die nächsten 4 Zeilen (25-28) legen die Anwendung im gitlab Zwischenspeicher ab und speichern sie dort für 1 Woche.
Build-Stage für native Linux-App
In den Zeilen 30 – 53 wird die native Linux App für die x64-Architektur erstellt.
In Zeile 34 – 45 wird der Quellcode mit bflat und entsprechenden Compiler-Parametern zur Anwendung kompiliert. Zeile 39 und 37 legt fest, dass das Ziel hier Linux als OS und x64 als Architektur verwendet wird.
In Zeile 46 wird die erstellte Anwendung gepackt. Ein Blick auf die Logausgabe zeigt auch hier ein hervorragendes Packergebnis, bei der die Zielanwendung auf 35.55% des Originals verkleinert wird.
$ upx --brute --lzma --overlay=strip ./pingcs
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser
Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
1758488 -> 625212 35.55% linux/amd64 pingcs
Packed 1 file.
In Zeile 47 wird die kompilierte und gepackte Anwendung überprüft. Das Ergebnis der Executable-Offset-Prüfung sollte in etwa so aussehen:
./pingcs: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), statically linked, no section header
Da wir uns in einem Linux-Container befinden, können wir die Executable auch gleich ausführen (Zeile 48 und 49). Die Datei wird in den Zeilen 50 – 53 im Artifacts-Speicher abgelegt und für eine Stunde aufbewahrt.