Writings
Blog
Gitlab Virtualbox Runner
02.02.24Gitlab Parallels/Virtualbox Runner
1. Einleitung
In diesem Artikel zeigen wir Ihnen Schritt für Schritt, wie Sie einen GitLab Runner mit Docker-Root-Rechten einrichten können. Dabei werden sowohl der Parallels Runner als auch der VirtualBox Runner behandelt. Zusätzlich erläutern wir die Verwendung des Ubuntu Auto Installers zur automatischen Erstellung eines Basis-Images. Dieser Leitfaden ist für Entwickler und Systemadministratoren gedacht, die ihre CI/CD-Pipeline optimieren möchten. Die geschätzte Lesedauer beträgt etwa 15 Minuten.
Kurzer Überblick über die genutzten Tools:
- CI/CD: Ein Ansatz zur Automatisierung der Softwareentwicklung und -bereitstellung.
- GitLab: Eine All-in-One-Plattform für Versionskontrolle und CI/CD.
- GitLab Runner: Die Softwarekomponente, die CI/CD-Jobs auf Maschinen ausführt.
- Parallels Runner: Ein spezifischer Typ von GitLab Runner, der auf Parallels virtualisierten Maschinen läuft.
- VirtualBox Runner: Ein weiterer Typ von GitLab Runner, der auf VirtualBox virtualisierten Maschinen läuft.
- Docker: Ein Werkzeug zur Containerisierung von Anwendungen.
2. Warum virtuelle Maschinen für CI/CD-Jobs? Sicherheits- und Isolationsaspekte
Die Verwendung von Containern wie Docker für CI/CD-Jobs ist weit verbreitet, birgt jedoch einige Herausforderungen und Risiken:
Ein Anwendungsfall: Ende-zu-Ende-Tests mit Mehrkomponenten-Architektur
In einer realen Anwendung werden oft mehrere Komponenten wie ein Frontend, ein Backend und eine Datenbank benötigt. Ende-zu-Ende-Tests sind in diesem Kontext besonders wichtig, um das Zusammenspiel aller Komponenten zu überprüfen.
Oftmals reicht ein einziger Docker-Container nicht aus, um ein realistisches Testsetting zu erzeugen. Darüber hinaus stellt der Einsatz von Docker-in-Docker (DinD) eigene Herausforderungen dar, da dieser Ansatz Zugriff auf die Docker-Socket des Host-Runners erfordert. Dies kann wiederum zu Sicherheitsrisiken führen und die Testumgebung beeinflussen.
Warum Parallels Runner und VirtualBox Runner?
Sicherheitsrisiken: Der Einsatz von
docker-compose
auf einem Bare-Metal-Runner kann ein erhebliches Sicherheitsrisiko darstellen, insbesondere wenn Docker mit Root-Rechten läuft.Seiteneffekte: Docker-Container können Artefakte und Einstellungen hinterlassen, die nachfolgende Builds unerwartet beeinflussen könnten.
Isolationsanforderungen: Für Integrationstests, die ein Cluster mit
docker-compose
oder Kubernetes hochfahren, ist eine garantiert saubere Umgebung erforderlich.
Hier bieten die spezialisierten GitLab Runner-Typen wie Parallels Runner und VirtualBox Runner eine Alternative. Sie können für jeden CI/CD-Job eine neue, saubere virtuelle Maschine erstellen. Das ermöglicht die isolierte Ausführung von Ende-zu-Ende-Tests mit mehreren Komponenten, ohne dabei Sicherheitsrisiken oder Seiteneffekte zu riskieren.
Dieser Ansatz bietet nicht nur eine höhere Sicherheit, sondern ermöglicht auch eine bessere Isolation und Reproduzierbarkeit der Testumgebungen. Der folgende Artikel konzentriert sich darauf, wie Sie diese Art von Isolation mit Parallels Runner und VirtualBox Runner in Ihrer GitLab CI/CD-Pipeline erreichen können.
3. Skalierbarkeit und Kosteneffizienz: GitLab vs. Bamboo
Ein wichtiger Aspekt, der bereits in der Einleitung angesprochen wurde, ist die Performance der Runner. GitLab-Runner, die auf VirtualBox oder Parallels laufen, können im Vergleich zu reinen Docker-Runnern eine höhere Flexibilität und Isolierung bieten. Darüber hinaus können durch den Einsatz von Hardware- oder Cloud-Ressourcen leicht sehr viele dieser Maschinen erstellt und somit einfach skaliert werden.
GitLab: Flexibilität durch vielfältige Lizenzierungsoptionen
GitLab ist einzigartig, da es eine kostenlose Open-Source-Version anbietet, die bereits eine breite Palette an Features beinhaltet. Die Lizenzierung der kommerziellen Version erfolgt auf Benutzerbasis. Das heißt, Sie können so viele Runner einrichten wie nötig, ohne zusätzliche Kosten zu verursachen, eine Option, die sogar in der kostenlosen Version verfügbar ist.
Bamboo: Begrenzte Skalierbarkeit und Kritik aus der Entwickler-Community
Im Gegensatz dazu wird die Lizenzierung bei Bamboo nach der Anzahl der Worker bemessen, was die Skalierbarkeit natürlich einschränkt. Zusätzlich gibt es Kritik aus der Entwickler-Community bezüglich der Bedienbarkeit von Bamboo durch Code. Hier fehlen oft Features in der YML- bzw. Bamboo-Spec, die eine nahtlose Integration in moderne Softwareentwicklungsprozesse erschweren.
Fazit: Was bedeutet das für die Skalierbarkeit?
GitLab bietet durch seine flexible Lizenzierung und seine Open-Source-Option ein hohes Maß an Skalierbarkeit und Anpassungsfähigkeit. Im Gegensatz dazu setzt Bamboo durch seine workerbasierte Lizenzierung und eingeschränkten Code-Konfigurationsmöglichkeiten natürliche Grenzen.
4. Praxis: Einrichtung eines GitLab-Runners mit VirtualBox / Parallels und Docker
In diesem Abschnitt wird die Einrichtung eines GitLab-Runners behandelt, der auf einer virtuellen oder physischen Maschine läuft. Die Einrichtung umfasst drei Hauptschritte:
Automatische Installation von Ubuntu: Zunächst wird Ubuntu auf einer Maschine (virtuell oder physisch) automatisch installiert. Für dieses Beispiel wird ein altes MacBook verwendet. Die Installation erfolgt vollständig automatisiert über eine Cloud-Konfiguration, die beim Boot-Prozess des Installationsmediums angegeben wird. Konfiguration der Maschine per Ansible: Nach der Installation von Ubuntu wird die Maschine mit Ansible konfiguriert. Ansible wird ebenfalls dazu verwendet, innerhalb der Maschine eine Ubuntu-VM zu erstellen. Erstellung einer internen Ubuntu VM für GitLab Runner: Die für den GitLab Runner genutzte VM wird automatisch eingerichtet und für jeden Build verwendet. In diese VM wird automatisiert alles Notwendige installiert, beispielsweise Docker. Die Erstellung und Konfiguration der VM erfolgen ebenfalls über eine Cloud-Konfiguration, die eine nahtlose und automatisierte Bereitstellung innerhalb von VirtualBox ermöglicht.
4.1 Installation von Ubuntu Server per Autoinstaller
Um eine Ubuntu-Installation automatisch zu starten, nutzen Sie die Boot-Option mit dem Autoinstaller. Bei dem Start von Ubuntu Server können Sie folgende Boot-Option setzen, um eine automatische Installation zu initiieren:
linux /casper/vmlinuz ip=dhcp autoinstall ds='nocloud-net;s=http://IhrServer.example.com/'
Diese Option teilt dem Installer mit, dass er Konfigurationsdaten von einer angegebenen URL abrufen soll – in diesem Fall von einem Server unter Ihrer Kontrolle. Die Angabe nocloud-net
signalisiert, dass die Daten über das Netzwerk bezogen werden und s=http://IhrServer.example.com/
gibt die spezifische Adresse an, unter der die Autoinstall-Konfiguration und gegebenenfalls weitere Dateien zu finden sind.
4.2 Bereitstellung eines Konfigurationsservers mit Go
package main import ( "fmt" "io/ioutil" "net/http" "os" "strings" ) func userDataHandler(w http.ResponseWriter, r *http.Request) { hostname := os.Getenv("HOSTNAME") username := os.Getenv("USERNAME") password := os.Getenv("PASSWORD") sshKey := os.Getenv("SSH_KEY") if hostname == "" { http.Error(w, "Environment variable HOSTNAME not set", http.StatusInternalServerError) return } if sshKey == "" { http.Error(w, "Environment variable SSH_KEY not set", http.StatusInternalServerError) return } if username == "" { http.Error(w, "Environment variable USERNAME not set", http.StatusInternalServerError) return } if password == "" { http.Error(w, "Environment variable PASSWORD not set", http.StatusInternalServerError) return } data, err := ioutil.ReadFile("config/user-data") if err != nil { http.Error(w, "Unable to read config file", http.StatusInternalServerError) return } config := strings.ReplaceAll(string(data), "HOSTNAME_PLACEHOLDER", hostname) config = strings.ReplaceAll(config, "USERNAME_PLACEHOLDER", username) config = strings.ReplaceAll(config, "PASSWORD_PLACEHOLDER", password) config = strings.ReplaceAll(config, "SSH_KEY_PLACEHOLDER", sshKey) w.Header().Set("Content-Type", "text/plain") fmt.Fprint(w, config) } func metaDataHandler(w http.ResponseWriter, r *http.Request) { data, err := ioutil.ReadFile("config/meta-data") if err != nil { http.Error(w, "Unable to read meta-data file", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") fmt.Fprint(w,string(data)) } func indexHtmlHandler(w http.ResponseWriter, r *http.Request) { url := os.Getenv("URL") if url == "" { http.Error(w, "Environment variable URL not set", http.StatusInternalServerError) return } data, err := ioutil.ReadFile("config/index.html") index := strings.ReplaceAll(string(data), "URL_PLACEHOLDER", url) if err != nil { http.Error(w, "Unable to read index.html file", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html") fmt.Fprint(w, index) } func main() { http.HandleFunc("/user-data", userDataHandler) http.HandleFunc("/meta-data", metaDataHandler) http.HandleFunc("/index.html", indexHtmlHandler) http.HandleFunc("/", indexHtmlHandler) http.ListenAndServe(":8080", nil) }
Der bereitgestellte Go-Code fungiert als einfacher HTTP-Server, der dynamisch die Installationskonfiguration (user-data
) und Meta-Daten (meta-data
) für die automatische Installation bereitstellt. Der Server verwendet Umgebungsvariablen, um spezifische Konfigurationsparameter wie Hostnamen, Benutzernamen, Passwort und einen SSH-Schlüssel in die Vorlagen einzusetzen.
userDataHandler
: Stellt dieuser-data
-Datei bereit, die für die automatische Installation notwendige Anweisungen enthält, inklusive Benutzerinformationen und SSH-Schlüsseln. Die Platzhalter in der Vorlage werden durch die Umgebungsvariablen ersetzt.metaDataHandler
: Liefert diemeta-data
-Datei, die üblicherweise Hostnamen und andere spezifische Daten für die Instanz enthält.indexHtmlHandler
: Ein einfacher Handler, der eine HTML-Seite für das Root-Verzeichnis oder/index.html
Pfad bereitstellt, möglicherweise um Informationen über den Server oder den Status der Installation anzuzeigen.
Dieser Server ermöglicht es, die Autoinstallationskonfiguration flexibel zu halten und bei Bedarf anzupassen, ohne die Konfigurationsdateien manuell bearbeiten zu müssen. Nach dem Hochfahren der Maschine und der erfolgreichen Installation kann die weitere Konfiguration und Einrichtung des GitLab Runners, einschließlich der Installation von Docker und der Einrichtung von VirtualBox oder Parallels, über Ansible erfolgen.
4.3 Ansible-Konfiguration des Servers
Nach der Installation des Betriebssystems wird der Server mittels Ansible für die Ausführung von GitLab Runnern vorbereitet. Hierbei wird unter anderem VirtualBox auf dem Linux Server installiert. Für detaillierte Ansible-Tasks verweise ich auf das entsprechende GitHub-Repository, da die Installation von VirtualBox und ähnlichen Paketen ziemlich standardmäßig ist.
Besonderes Augenmerk liegt auf dem Task, der eine neue VM mittels Ansible erstellt, wenn diese noch nicht existiert. Der folgende Ansible-Code-Ausschnitt zeigt, wie eine VM inklusive Cloud-Init-Konfiguration für ein automatisiertes Setup erzeugt wird:
--- - name: Check if VM exists shell: VBoxManage list vms | grep \"{{ VM_NAME }}\" ignore_errors: true register: vm_check - name: Create VM if not exists when: vm_check.rc != 0 block: - name: Check if SSH key exists stat: path: ~/.ssh/id_rsa.pub register: ssh_key_check - name: Ensure SSH key exists for the user command: ssh-keygen -t rsa -f ~/.ssh/id_rsa -N "" when: ssh_key_check.stat.exists == False - name: Download Ubuntu ISO get_url: url: "{{ UBUNTU_ISO_URL }}" dest: "~/ubuntu.iso" - name: Ensure the directory exists file: path: "{{ CLOUD_INIT_CONFIG | dirname }}" state: directory - name: Lesen der id_rsa.pub vom Zielserver slurp: src: ~/.ssh/id_rsa.pub register: remote_ssh_key - name: Generate cloud-init config copy: content: | #cloud-config runcmd: - [eval, 'echo $(cat /proc/cmdline) "autoinstall" > /root/cmdline'] - [eval, 'mount -n --bind -o ro /root/cmdline /proc/cmdline'] - [eval, 'snap restart subiquity.subiquity-server'] - [eval, 'snap restart subiquity.subiquity-service'] autoinstall: version: 1 identity: hostname: {{ VM_NAME }} password: "$6$exDY1mhS4KUYCE/2$zmn9ToZwTKLhCw.b4/b.ZRTIZM30JZ4QrOQ2aOXJ8yk96xpcCof0kxKwuX1kqLG/ygbJ1f8wxED22bTL4F46P0" #ubuntu username: ubuntu ssh: install-server: yes authorized-keys: - "{{ remote_ssh_key['content'] | b64decode }}" packages: - curl - rsync late-commands: - "curl -fsSL https://get.docker.com -o /target/root/get-docker.sh" - "curtin in-target --target=/target -- sh /root/get-docker.sh" - "curtin in-target --target=/target -- sed -i '/^docker:/ s/$/,ubuntu/' /etc/group" - "rm -f /target/root/get-docker.sh" - "curtin in-target --target=/target -- curl -L 'https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh' | bash" - "curtin in-target --target=/target -- apt update" - "curtin in-target --target=/target -- apt install -y gitlab-runner" dest: "{{ CLOUD_INIT_CONFIG }}" - name: Generate cloud-init ISO command: cloud-localds {{ CLOUD_INIT_ISO }} {{ CLOUD_INIT_CONFIG }} - name: Create a new VirtualBox VM shell: | VBoxManage createvm --name {{ VM_NAME }} --register VBoxManage modifyvm {{ VM_NAME }} --memory {{ MEMORY_SIZE }} --cpus {{ CPU_COUNT }} VBoxManage createmedium disk --filename ~/{{ VM_NAME }}_disk.vdi --size {{ HDD_SIZE }} VBoxManage storagectl {{ VM_NAME }} --name "SATA Controller" --add sata VBoxManage storageattach {{ VM_NAME }} --storagectl "SATA Controller" --port 0 --device 0 --type hdd --medium ~/{{ VM_NAME }}_disk.vdi VBoxManage storagectl {{ VM_NAME }} --name "IDE Controller" --add ide VBoxManage storageattach {{ VM_NAME }} --storagectl "IDE Controller" --port 0 --device 0 --type dvddrive --medium ~/ubuntu.iso VBoxManage storageattach {{ VM_NAME }} --storagectl "IDE Controller" --port 0 --device 1 --type dvddrive --medium {{ CLOUD_INIT_ISO }} VBoxManage modifyvm {{ VM_NAME }} --nic1 nat VBoxManage natnetwork add --netname natnet1 --network "192.168.15.0/24" --enable VBoxManage modifyvm {{ VM_NAME }} --natpf1 "ssh,tcp,,8022,,22" VBoxManage modifyvm {{ VM_NAME }} --boot1 disk --boot2 dvd --boot3 none --boot4 none VBoxManage startvm {{ VM_NAME }} --type headless - name: Wait for SSH at Port 8022 command: ssh -i ~/id_rsa -o StrictHostKeyChecking=no ubuntu@localhost -p 8022 "rm ~/.bash_logout && echo 'ok'" register: result until: result.stdout == 'ok' retries: 60 delay: 40 - name: Cleanup shell: | rm -rf ~/cidata/ rm ~/seed.iso
Ein Hack um die Sicherheitsprüfung zu umgehen
Innerhalb der Cloud-Init-Konfiguration wird ein spezieller Hack angewendet, um die Notwendigkeit, autoinstall
beim Booten manuell anzugeben, zu umgehen. Dies wird durch folgende Befehle erreicht:
runcmd: - [eval, 'echo $(cat /proc/cmdline) "autoinstall" > /root/cmdline'] - [eval, 'mount -n --bind -o ro /root/cmdline /proc/cmdline'] - [eval, 'snap restart subiquity.subiquity-server'] - [eval, 'snap restart subiquity.subiquity-service']
Diese Befehle fügen effektiv autoinstall
zur Befehlszeile des laufenden Systems hinzu, indem sie /proc/cmdline
temporär anpassen. Dies ermöglicht es dem System, die automatische Installation ohne manuelle Eingaben durchzuführen, was für die Skalierung und Automatisierung von CI/CD-Umgebungen entscheidend ist.
Fazit
Durch die Kombination von Ubuntu Server Autoinstallation und Ansible für die nachfolgende Konfiguration kann ein effizienter und automatisierter Prozess zur Einrichtung von VMs für GitLab Runner geschaffen werden. Der beschriebene Hack erlaubt eine reibungslose Autoinstallation, die für dynamische und skalierte CI/CD-Umgebungen unerlässlich ist.