Seite wählen

NETWAYS Blog

Ein Ausblick – Traefik Provider OpenStack

Bei unseren Online-Trainings von NETWAYS, werden jedem einzelnen Teilnehmer eine oder mehrere explizite VMs zur Verfügung gestellt, welche in der OpenStack-Umgebung von NWS gehostet werden. Hierbei benötigen die VMs jeweils öffentliche IPs, welche leider nicht unendlich zu Verfügung stehen. Aus dieser Prämisse heraus entstand die Idee, einen sog. reverse Proxy zu benutzen, welcher Anfragen von Clients an Webserver weiterleitet – in diesem Fall an die entsprechende VM.

Das Problem an dieser Stelle ist, dass es für Traefik noch keinen Provider für OpenStack gibt. Daher wurde ein experimenteller openstack-provider entwickelt, welchen ich hier kurz vorstelle.

 

Funktionsweise des openstack-providers

Der openstack-traefik-provider benutzt den http-provider von Traefik um die VMs/Instanzen innerhalb eines OpenStack-Projektes zu suchen/finden. Damit Traefik die Informationen über die Instanzen erhält, wird ein OpenStack-Go-Client innerhalb eines Docker-Containers gestartet, welcher anschließend über die URL http://openstack:8080/traefik (diese URL kann im Code selbst angepasst werden), alle Informationen des Projektes über ein HTTP-Response zur Verfügung stellt. Traefik selbst pollt alle 10 sek. von dieser Adresse, um die Routen, Middlewares, etc. zu aktualisieren.

 

Beispielhafte Konfiguration

Jeglicher HTTPS-Traffic soll am Proxy terminiert werden, sodass innerhalb des internen Netzwerks HTTP benutzt wird:

main.go
[...]
settings := discovery.DefaultSettings
	settings.DefaultRule = "Host(`{{ .Name }}.trainig.netways.de`)"
	settings.DefaultEnable = true
	settings.AddressType = "floating"
	settings.DefaultLabels = map[string]string{
		// HTTP
		"traefik.http.routers.{{ .Name }}-http.service":                   "{{ .Name }}-http",
		"traefik.http.routers.{{ .Name }}-http.rule":                      "Host(`{{ .Name }}.trainig.netways.de`)",
		"traefik.http.routers.{{ .Name }}-http.entrypoints":               "http",
		"traefik.http.services.{{ .Name }}-http.loadBalancer.server.port": "80",
		"traefik.http.routers.{{ .Name }}-http.middlewares":               "http-to-https",

		// HTTPS
		"traefik.http.routers.{{ .Name }}-https.service":                   "{{ .Name }}-https",
		"traefik.http.routers.{{ .Name }}-https.rule":                      "Host(`{{ .Name }}.trainig.netways.de`)",
		"traefik.http.routers.{{ .Name }}-https.entrypoints":               "https",
		"traefik.http.services.{{ .Name }}-https.loadBalancer.server.port": "80",
		"traefik.http.routers.{{ .Name }}-https.tls":                       "true",

		// Redirect HTTP to HTTPS
		"traefik.http.middlewares.http-to-https.redirectscheme.scheme":    "https",
		"traefik.http.middlewares.http-to-https.redirectscheme.permanent": "true",
	}
[...]

 

Anschließend muss über den Befehl docker-compose up der Docker-Container mit der neuen Konfiguration gestartet werden und die Routen + Middleware sollten im Frontend zu sehen sein:

 

Da das Projekt sich derzeit noch in Entwicklung befindet, würde ich mich über Vorschläge, PullRequests oder Kritik von der Open Source Community sehr freuen!

Sobald das Projekt abgeschlossen ist, werde ich auch davon berichten!

 

DockerCon 2022: Wie geht Containersecurity?

Buzzwords wie Software Supply Chain, Container Security Scanning oder Software Bill of Materials (SBOM) sind in den vergangenen zwei Jahren vermehrt in aller Munde, nicht zuletzt aufgrund des anhaltenden Trends zur Containerisierung vormals monolithischer Anwendungen und deren Betrieb als sog. Microservices. Allerdings kann nach wie vor nicht jeder, der auf die ein oder andere Weise mit Docker oder Containern im Allgemeinen zu tun hat, etwas mit diesen Begriffen anfangen. Aus diesem Grund habe ich den Fokus meines virtuellen Besuchs der diesjährigen DockerCon auf genau diesen Themebereich – Containersecurity – gelegt und möglichst viele Best Practices für Dich zusammengefasst.

 

Die Ausgangslage

Die großen Sicherheitslücken der vergangenen zwei Jahre, sei es der Solarwinds-Breach oder die Log4J/Log4Shell-Exploits haben einmal mehr schmerzlich bewusst gemacht: In fast jedem Softwareprodukt, egal ob proprietär oder Open Source, befinden sich zahlreiche mehr oder weniger gut gepflegte Abhängigkeiten verschiedenster Maintainer – in vielen Applikationen stecken bis zu 80% Open Source Code, die es „auf dem Schirm“ zu behalten gilt. Dies gilt natürlich auch für Container(-images), die letzten Endes nichts anderes tun, als die Applikation zu bündeln und (weitestgehend) losgelöst vom ausführenden Hostsystem ausführbar zu machen.

Dennoch gelten Container paradoxerweise oft als besonders sicher, sei es aufgrund der von außen wahrgenommenen „Kapselung“ oder der geringen Größe – soviel potentiell vulnerable oder bösartige Software kann da doch gar nicht drinstecken, oder? Mögen diese Wahrnehmungen in der Theorie und im Bestfall auch stimmen, sieht die Praxis in den allermeisten Fällen anders aus, wie die ein oder andere Keynote im Rahmen der DockerCon gezeigt hat.

Laut SysDig, einem Spezialisten für Cloudsecurity, laufen 58% der in den einschlägigen Containerregistries (DockerHub, Google, Github, etc.) verfügbaren Containerimages als root, anstatt einen geeigneteren, unprivilegierten Nutzer zu verwenden. Außerdem beinhalten selbst die offiziellen, von den Registries kuratierten Containerimages beliebter Frameworks oder Distributionen dutzende detektierbare Schwachstellen.

Offensichtlich gibt es also zum Einen ein falsches Gefühl von Sicherheit innerhalb der Nutzergemeinschaft von Containern, zum Anderen aber schlichtweg keine Blaupause oder „OneFitsAll“-Lösung, die für beliebige Container(-images) absolute Sicherheit verspricht. Nur eine Sache ist klar: „Hinten“ anzufangen, ist nicht rentabel – Container einfach zu deployen und dann im Betrieb nach Sicherheitslücken zu suchen, ist teuer, erzeugt vermeidbaren zusätzlichen Aufwand und nimmt Flexibilität. Die Absicherung der Container muss „nach links“ geschoben werden, möglichst an den Beginn der Containerisierung. Doch wo ansetzen?

 

Die Möglichkeiten

Um den Ursprung für Schwachstellen in Container(-images) und damit einhergehend mögliche Ansatzpunkte zur Absicherung zu identifizieren, muss man die „Lebensphasen“ eines Container(-images) verstehen. Diese lassen sich kurz und bündig wie folgt darstellen:

 

  • Bau des Images (lokal oder via CI/CD)
  • Distribution des Images – Push in eine Registry (Self-hosted oder bei einem Anbieter), Pull von Usern/Programmen
  • Deployment des Images (Openshift, (Managed) Kubernetes, etc.) und Betrieb des Containers

 

An dieser Stelle wird hoffentlich klar, warum ich permanent „Container(-image)“ schreibe – abhängig von der betrachteten Lebensphase haben wir es beim Thema Containersicherheit entweder mit einem erstellten Image oder mit einem laufenden Container, quasi einer Instanziierung dieses Images zu tun. Mit dieser Aufschlüsselung in verschiedene Abschnitte können wir uns nun mögliche Schritte zur Absicherung unserer Container anschauen.

Containerimages werden entweder lokal von Entwickler:innen, DevOps-Engineers, etc. auf Grundlage eines sog. Dockerfiles gebaut – alternativ lässt sich dieser Vorgang aber auch von CI/CD Pipelines umsetzen, wenn man den Dockerfile und andere benötigten Ressourcen bspw. in GitLab eincheckt. Die meisten Möglichkeiten zur Absicherung des späteren Container(-images) ergeben sich bereits in diesem ersten Schritt – der Quellcode der zu containerisierenden Applikation liegt vor, ebenso die Definition des Containers selbst, und auch möglicherweise vulnerable oder bösartige Abhängigkeiten wurden noch nicht in das spätere Containerimage eingebettet.

Ist das Containerimage lokal oder in einer Pipeline gebaut worden, muss man es zur späteren Nutzung in Produktion in eine Containerregistry übertragen. Diese kann entweder selbst gehosted werden, als private, aber gemanagte Instanz in der Cloud laufen oder öffentlich von jedem beliebigen Nutzer einsehbar sein. Auch bei diesem Vorgang gibt es Möglichkeiten, die Supply Chain zwischen Entwickler und Endnutzer abzusichern.

Auch wenn im Dockerfile gewisse Best Practices befolgt werden, kann der Container diese in vielen Fällen beim Deployment überschreiben – das letzte Wort haben hierbei immer Kubernetes, Openshift, Docker Desktop und Konsorten. Aus diesem Grund müssen einige der Überlegungen, die in der Build-Phase des Containerimages stattgefunden haben, auch hier noch einmal herangezogen, betrachtet und evaluiert werden, um die für die konkrete Nutzung besten Einstellungen und Kompromisse zu finden.

Ist der Container erst einmal deployed, gibt es nicht mehr viele Möglichkeiten, ihn „von innen“ weiter abzusichern. Dennoch kann und sollte man sich fortlaufend Gedanken bspw. um die Erreichbarkeit innerhalb des Clusters, des Netzwerkes, der Cloud etc. machen, Regeln nachziehen wo nötig und natürlich auch Update- und Backupstrategien im Hinterkopf behalten. Am Ende des Tages merkt man spätestens jetzt: Nach dem Deployment eines Containers seine Sicherheit zu überprüfen und zu garantieren, ist nicht sinnvoll – wir brauchen einen Left Shift.

 

Die Umsetzung

Buildphase

Nach viel Theorie und Konjunktiv können wir uns jetzt konkrete Umsetzungsmöglichkeiten der besprochenen Ansätze anschauen. Beginnen werden wir „ganz links“ beim Bau der Containerimages. Vieles lässt sich hier über verschiedene Direktiven im genutzten Dockerfile festlegen – Docker listet alle möglichen Direktiven sowie sinnvolle Best Practices und Caveats in der Docker Dokumentation auf. Für uns besonders interessant sind die Direktiven ADD, COPY und USER. Wie eingangs erwähnt, nutzen mehr als die Hälfte aller Containerimages per Default den root Nutzer innerhalb des Containers, obwohl das in vielen Fällen gar nicht notwendig wäre – auf einem klassischen Server läuft ein Apache2 Webserver schließlich auch als User apache, warum sollte/müsste das innerhalb eines Containers bspw. auf Debian-Basis anders sein?

Die anderen beiden erwähnten Direktiven beziehen sich auf die Abwägung, ob man gewisse Verzeichnisse und Dateien aus seiner Entwicklungsumgebung denn tatsächlich im endgültigen Container braucht – oder ob man die Applikation direkt lokal baut, und lediglich die fertige Binary via COPY in das Containerimage überträgt. In diesem Fall muss man natürlich darauf achten, dass etwaige zur Laufzeit benötigte Tools (ich meine dich, curl) und Bibliotheken sich auch im endgültigen Containerimage befinden. Alternativ kann man innerhalb eines Dockerfiles eine build und eine run Umgebung schaffen – auf diese Weise kann man die Applikation innerhalb des Containers bauen und im Anschluss lediglich die benötigten binären Artefakte und andere benötigten Ressourcen in das Lauzeitimage kopieren. Für diese Vorgehensweise würde es sich anbieten, das Entwicklungsrepository via ADD in die build Umgebung des Containerimages zu übertragen.

Diese Vorgehensweise bringt uns direkt zur nächsten „beliebten“ Unsicherheit in Container(-images): Lokal hinterlegte Credentials, Entwicklertokens, Cloudzugänge etc. Es gibt vermutlich keine Art von geheimen Daten, die nicht bereits versehentlich in Containerimages „vergessen“ wurde. Das kann schnell passieren – ein Entwickler nutzt ein Shellscript mit Zugangsdaten, um von seiner Entwicklungsumgebung in ein Testcluster zu deployen, eine .yaml-Datei, um Daten aus einer Entwicklerdatenbank zu lesen etc. Im „schlimmsten Fall“ überträgt er wissentlich eine Datei mit Zugangsdaten, die die containerisierte Applikation später in Produktion nutzen soll, oder hinterlegt sensible Daten im Containerimage als Umgebungsvariable mittels ENV.

Zur Bewältigung dieser Problematik gibt es die Möglichkeit, ähnlich wie eine .gitignore Datei für Git-Repositories eine .dockerignore Datei für den Buildvorgang eines Containerimages zu hinterlegen. Alle in dieser Datei aufgeführten Dateien und Verzeichnisse werden vom Dockerdaemon bei der Verarbeitung des Build-Contexts, von ADD und von COPY Direktiven ignoriert und finden sich somit zu keinem Zeitpunkt im Containerimage wieder. Dringend benötigte Umgebungsvariablen zur Konfiguration der containerisierten Applikation können auch zum Zeitpunkt des Deployments noch übergeben werden, bspw. mittels des Parameters -e via Docker-CLI oder dem Einlesen von Secrets als Umgebungsvariablen in Kubernetes.

Grundlegende Maßnahmen wie die Nutzung eines passenden Base-Images in der FROM Direktive (es muss nicht immer debian:buster oder ubuntu:20.04 sein), das Vermeiden der Öffnung unbenötigter Ports via EXPOSE sowie der Nutzung sog. Kannibalen-Tags (latest zeigt nach jedem Imageupdate auf eine neue Version) sollten darüber hinaus natürlich immer befolgt und beachtet werden.

Distributionsphase

Haben wir nun lokal oder in der Pipeline ein in sich möglichst sicheres Containerimage gebaut, muss es auf die eine oder andere Art und Weise seinen Weg in eine Containerregistry finden, um von anderen Nutzern heruntergeladen und deployed werden zu können. Für diese Vorgänge werden einem von den meisten Containerregistries Werkzeuge an die Hand gegeben, mit deren Hilfe wir den Prozess des Up-/Downloads von Containerimages absichern können.

Letzten Endes sind Containerregistries nichts anderes als APIs mit der Fähigkeit, Nutzer zu authentifizieren und zu authorisieren, Containerimages in Empfang zu nehmen, zu speichern und deren Versionierung im Blick zu behalten. Wie immer, wenn man über das Internet mit einer API spricht, gilt: HTTPS ist Pflicht! Darüber hinaus bieten Registries verschiedene Möglichkeiten, zusätzliche Maßnahmen gegen die Verbreitung unsicherer Images oder die Manipulation vorhandener Containerimages zu treffen. So können Imageregistries oftmals alle verwalteten Containerimages auf bekannte Schwachstellen scannen und den Download solcher Images durch Endnutzer untersagen. Auch die digitale Signierung von verwalteten Containerimages ist oftmals möglich, z.B. mittels sigstore und cosign.

Deploymentphase

Wird unser Containerimage nun im Rahmen eines Kubernetes-Deployments oder Docker-CLI-Befehls aus der Registry gepulled und deployed, haben wir einmal mehr die Möglichkeit, Sicherheit zu forcieren: Wie bereits erwähnt, können wir zum Einen die Voreinstellungen in Hinblick auf Umgebungsvariablen, User- und Gruppenkontext uvm. überschreiben, zum Anderen bietet natürlich auch die Absicherung der ausführenden Infrastruktur selbst die Möglichkeit, dass Deployment so sicher wie möglich zu gestalten.

Hierzu zählen z.B. die Nutzung sog. rootless Container, die von einem Container Runtime Interface (CRI) wie Docker oder containerd ausgeführt werden, die selbst nicht im root Kontext laufen. Auch die Nutzung restriktiver Netzwerk- und Firewallpolicies kann dabei helfen, die Risiken durch möglicherweise vulnerable oder bösartige Container zu minimieren. Konfiguration und Forcierung dieser Maßnahmen innerhalb eines Clusters können schnell zur Sisyphusarbeit werden – hier kann ein gemanagtes Kubernetes-Cluster von Vorteil sein, bspw. Managed Kubernetes von NETWAYS Web Services. Darüber hinaus sollte man eine nachhaltige Update-Strategie für seine Containerimages verfolgen: Es gilt, einen guten Kompromiss zwischen regelmäßigen Updates zu finden, aber nicht sofort jede neue Version (und deren evtl. neu eingeführte Schwachstellen) in Produktion zu deployen.

 

Das Fazit

Container(-images) sicher zu erstellen, zu verwalten und zu deployen ist ein langer Weg voller Stolpersteine. Wie so oft beim Thema Sicherheit wird man die 100% aller Voraussicht nach nicht erreichen – Nichtexistenz von Sicherheitslücken lässt sich nun einmal nicht beweisen. Dennoch habe ich Dich hoffentlich für das Thema und die teils falschen, gängigen Annahmen und Praktiken sensibilisieren können und nebenbei einige Best Practices an die Hand gegeben, die das Risiko durch vulnerable Container deutlich verringern.

Solltest Du nun mehr über den Vorgang des Imagebaus, den Betrieb von Containern in Kubernetes oder Containerisierung im Allgemeinen erfahren wollen, schau Dir doch einmal unser Kubernetes Schulungsangebot von NETWAYS an. In diesem eintägigen Workshop vermittle ich Dir praxisnah und einsteigerfreundlich alles, was Du für die ersten eigenen Schritte mit Docker und Kubernetes wissen musst. Ich freue mich auf Dich!

Daniel Bodky
Daniel Bodky
Platform Advocate

Daniel kam nach Abschluss seines Studiums im Oktober 2021 zu NETWAYS und beriet zwei Jahre lang Kunden zu den Themen Icinga2 und Kubernetes, bevor es ihn weiter zu Managed Services zog. Seitdem redet und schreibt er viel über cloud-native Technologien und ihre spannenden Anwendungsfälle und gibt sein Bestes, um Neues und Interessantes rund um Kubernetes zu vermitteln. Nebenher schreibt er in seiner Freizeit kleinere Tools für verschiedenste Einsatzgebiete, nimmt öfters mal ein Buch in die Hand oder widmet sich seinem viel zu großen Berg Lego. In der wärmeren Jahreszeit findet man ihn außerdem oft auf dem Fahrrad oder beim Wandern.

Docker – ein erster Eindruck!

Ich habe in den letzten Monaten meiner Ausbildung schon viel über verschiedene Software, Strukturen und Programiersprachen gelernt. Seit einigen Wochen arbeite ich in der Managed Services-Abteilung mit Docker und bin wirklich fasziniert, was Docker alles kann und wie es funktioniert.

Was genau ist Docker?

Docker ist eine Containerisierungstechnologie, die die Erstellung und den Betrieb von Linux-Containern ermöglicht, die man nicht mit virtuellen Maschinen verwechseln sollte.

Docker vereinfacht die Bereitstellung von Anwendungen, weil sich Container, die alle nötigen Pakete enthalten, leicht als Dateien transportieren und installieren lassen. Container gewährleisten die Trennung und Verwaltung der auf einem Rechner genutzten Ressourcen. Das beinhaltet laut Aussage der Entwickler: Code, Laufzeitmodul, Systemwerkzeuge, Systembibliotheken – alles was auf einem Rechner installiert werden kann.

Welche Vorteile bringt Docker?

Ein Vorteil ist, dass man diese Container sehr flexibel erstellen, einsetzen, kopieren und zwischen Umgebungen hin- und her verschieben kann. Sogar ein Betrieb in der Cloud ist dadurch möglich und eröffnet viele neue Möglichkeiten. Das Ausführen verschiedener Container bietet zusätzlich einige Sicherheitsvorteile. Wenn man Anwendungen in verschiedenen Containern ausführt, hat jeder Container nur Zugriff auf die Ports und Dateien, die der andere Container explizit freigibt.

Darüber hinaus bieten Container ein höheres Maß an Kontrolle darüber, welche Daten und Software auf dem Container installiert sind. Schadsoftware, die in einem Container ausgeführt wird, wirkt sich nicht auf andere Container aus.

Was ist der Unterschied zwischen Docker-Containern und virtuellen Maschinen?

Virtuelle Maschinen enthalten von der simulierten Hardware, über das Betriebssystem bis zu den installierten Programmen eine große Menge an Informationen. Die Folge: Sie verbrauchen viel Speicherplatz und Ressourcen.

Der große und schöne Unterschied zu herkömmlichen virtuellen Maschinen ist, dass die Container kein Betriebssystem beinhalten und booten müssen, sondern lediglich die wichtigen Daten der Anwendung enthalten. Wollten wir früher beispielsweise eine Datenbank und einen Webserver getrennt voneinander starten, benötigen wir zwei VMs. Mit Docker werden hier nur noch zwei Container benötigt, ohne dass man die VMs booten muss, die kaum Ressourcen verbrauchen – im Vergleich zu den virtuellen Maschinen.

Virtuelle Maschinen jedoch haben nach wie vor ihre Daseinsberechtigung: zum Beispiel wenn auf einem Host mehrere Maschinen mit jeweils unterschiedlichen Betriebssystemen oder Hardware-Spezifikationen simuliert werden müssen. Denn ein Docker-Container enthält kein eigenes Betriebssystem und keine simulierte Hardware. Hier wird auf das System des Hosts zugegriffen, so dass alle Container Betriebssystem und Hardware gemeinsam nutzen.

Fazit zum Thema Docker:

Den Einsatz von Docker betrachte ich als sinnvoll. Es ergeben sich viele Vorteile und so ist es inzwischen möglich, eine Softwareumgebung rascher aufzubauen, ohne viel Speicherplatz und Ressourcen zu verbrauchen.

GitLab CI Runners with Auto-scaling on OpenStack

 

With migrating our CI/CD pipelines from Jenkins to GitLab CI in the past months, we’ve also looked into possible performance enhancements for binary package builds. GitLab and its CI functionality is really really great in this regard, and many things hide under the hood. Did you know that „Auto DevOps“ is just an example template for your CI/CD pipeline running in the cloud or your own Kubernetes cluster? But there’s more, the GitLab CI runners can run jobs in different environments with using different hypervisors and the power of docker-machine.

One of them is OpenStack available at NWS and ready to use. The following examples are from the Icinga production environment and help us on a daily basis to build, test and release Icinga products.

 

Preparations

Install the GitLab Runner on the GitLab instance or in a dedicated VM. Follow along in the docs where this is explained in detail. Install the docker-machine binary and inspect its option for creating a new machine.

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
apt-get install -y gitlab-runner
  
curl -L `uname -s`-`uname -m` -o /usr/local/bin/docker-machine
chmod +x /usr/local/bin/docker-machine
  
docker-machine create --driver openstack --help

Next, register the GitLab CI initially. Note: This is just to ensure that the runner is up and running in the GitLab admin interface. You’ll need to modify the configuration in a bit.

gitlab-runner register \
  --non-interactive \
  --url https://git.icinga.com/ \
  --tag-list docker \
  --registration-token SUPERSECRETKEKSI \
  --name "docker-machine on OpenStack" \
  --executor docker+machine \
  --docker-image alpine

 

Docker Machine with OpenStack Deployment

Edit „/etc/gitlab-runner/config.toml“ and add/modify the „[[runners]]“ section entry for OpenStack and Docker Machine. Ensure that the MachineDriver, MachineName and MachineOptions match the requirements. Within „MachineOptions“, add the credentials, flavors, network settings just as with other deployment providers. All available options are explained in the documentation.

vim /etc/gitlab-runner/config.toml

  [runners.machine]
    IdleCount = 4
    IdleTime = 3600
    MaxBuilds = 100
    MachineDriver = "openstack"
    MachineName = "customer-%s"
    MachineOptions = [
      "openstack-auth-url=https://cloud.netways.de:5000/v3/",
      "openstack-tenant-name=1234-openstack-customer",
      "openstack-username=customer-login",
      "openstack-password=sup3rS3cr3t4ndsup3rl0ng",
      "openstack-flavor-name=s1.large",
      "openstack-image-name=Debian 10.1",
      "openstack-domain-name=default",
      "openstack-net-name=customer-network",
      "openstack-sec-groups="mine",
      "openstack-ssh-user=debian",
      "openstack-user-data-file=/etc/gitlab-runner/user-data",
      "openstack-private-key-file=/etc/gitlab-runner/id_rsa",
      "openstack-keypair-name=GitLab Runner"
    ]

The runners cache can be put onto S3 granted that you have this service available. NWS luckily provides S3 compatible object storage.

  [runners.cache]
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      ServerAddress = "s3provider.domain.localdomain"
      AccessKey = "supersecretaccesskey"
      SecretKey = "supersecretsecretkey"
      BucketName = "openstack-gitlab-runner"

Bootstrap Docker in the OpenStack VM

Last but not least, these VMs need to be bootstrapped with Docker inside a small script. Check the „–engine-install-url“ parameter in the help output:

root@icinga-gitlab:/etc/gitlab-runner# docker-machine create --help
  ...
  --engine-install-url "https://get.docker.com"							Custom URL to use for engine installation 

You can use the official way of doing this, but putting this into a small script also allows customizations like QEMU used for Raspbian builds. Ensure that the script is available via HTTP e.g. from a dedicated GitLab repository 😉

#!/bin/sh
#
# This script helps us to prepare a Docker host for the build system
#
# It is used with Docker Machine to install Docker, plus addons
#
# See --engine-install-url at docker-machine create --help

set -e

run() {
  (set -x; "$@")
}

echo "Installing Docker via get.docker.com"
run curl -LsS https://get.docker.com -o /tmp/get-docker.sh
run sh /tmp/get-docker.sh

echo "Installing QEMU and helpers"
run sudo apt-get update
run sudo apt-get install -y qemu-user-static binfmt-support

Once everything is up and running, the GitLab runners are ready to fire the jobs.

 

Auto-Scaling

Jobs and builds are not run all the time, and especially with cloud resources, this should be a cost-efficient thing. When building Icinga 2 for example, the 20+ different distribution jobs generate a usage peak. With the same resources assigned all the time, this would tremendously slow down the build and release times. In that case, it is desirable to automatically spin up more VMs with Docker and let the GitLab runner take care of distributing the jobs. On the other hand, auto-scaling should also shut down resources in idle times.

By default, one has 4 VMs assigned to the GitLab runner. These builds run non-privileged in Docker, the example below also shows another runner which can run privileged builds. This is needed for Docker-in-Docker to create Docker images and push them to GitLab’s container registry.

root@icinga-gitlab:~# docker-machine ls
NAME                                               ACTIVE   DRIVER      STATE     URL                      SWARM   DOCKER     ERRORS
runner-privileged-icinga-1571900582-bed0b282       -        openstack   Running   tcp://10.10.27.10:2376           v19.03.4
runner-privileged-icinga-1571903235-379e0601       -        openstack   Running   tcp://10.10.27.11:2376           v19.03.4
runner-non-privileged-icinga-1571904408-5bb761b5   -        openstack   Running   tcp://10.10.27.20:2376           v19.03.4
runner-non-privileged-icinga-1571904408-52b9bcc4   -        openstack   Running   tcp://10.10.27.21:2376           v19.03.4
runner-non-privileged-icinga-1571904408-97bf8992   -        openstack   Running   tcp://10.10.27.22:2376           v19.03.4
runner-non-privileged-icinga-1571904408-97bf8992   -        openstack   Running   tcp://10.10.27.22:2376           v19.03.4

Once it detects a peak in the pending job pipeline, the runner is allowed to start additional VMs in OpenStack.

root@icinga-gitlab:~# docker-machine ls
NAME                                               ACTIVE   DRIVER      STATE     URL                      SWARM   DOCKER     ERRORS
runner-privileged-icinga-1571900582-bed0b282       -        openstack   Running   tcp://10.10.27.10:2376           v19.03.4
runner-privileged-icinga-1571903235-379e0601       -        openstack   Running   tcp://10.10.27.11:2376           v19.03.4
runner-non-privileged-icinga-1571904408-5bb761b5   -        openstack   Running   tcp://10.10.27.20:2376           v19.03.4
runner-non-privileged-icinga-1571904408-52b9bcc4   -        openstack   Running   tcp://10.10.27.21:2376           v19.03.4
runner-non-privileged-icinga-1571904408-97bf8992   -        openstack   Running   tcp://10.10.27.22:2376           v19.03.4
runner-non-privileged-icinga-1571904408-97bf8992   -        openstack   Running   tcp://10.10.27.23:2376           v19.03.4

...

runner-non-privileged-icinga-1571904534-0661c396   -        openstack   Running   tcp://10.10.27.24:2376           v19.03.4
runner-non-privileged-icinga-1571904543-6e9622fd   -        openstack   Running   tcp://10.10.27.25:2376           v19.03.4
runner-non-privileged-icinga-1571904549-c456e119   -        openstack   Running   tcp://10.10.27.27:2376           v19.03.4
runner-non-privileged-icinga-1571904750-8f6b08c8   -        openstack   Running   tcp://10.10.27.29:2376           v19.03.4

 

In order to achieve this setting, modify the runner configuration and increase the limit.

vim /etc/gitlab-runner/config.toml

[[runners]]
  name = "docker-machine on OpenStack"
  limit = 24
  output_limit = 20480
  url = "https://git.icinga.com/"
  token = "supersecrettoken"
  executor = "docker+machine"

This would result in 24 OpenStack VMs after a while, and all are idle 24/7. In order to automatically decrease the deployed VMs, use the OffPeak settings. This also ensures that resources are available during workhours while spare time and weekend are considered „off peak“ with shutting down unneeded resources automatically.

    OffPeakTimezone = "Europe/Berlin"
    OffPeakIdleCount = 2
    OffPeakIdleTime = 1800
    OffPeakPeriods = [
      "* * 0-8,22-23 * * mon-fri *",
      "* * * * * sat,sun *"
    ]

Pretty neat functionality 🙂

 

Troubleshooting & Monitoring

„docker-machine ls“ provides the full overview and tells whenever e.g. a connection to OpenStack did not work, or if the VM is currently unavailable.

root@icinga-gitlab:~# docker-machine ls
NAME                                               ACTIVE   DRIVER      STATE     URL                      SWARM   DOCKER     ERRORS
runner-privileged-icinga-1571900582-bed0b282       -        openstack   Error                                      Unknown    Expected HTTP response code [200 203] when accessing [GET https://cloud.netways.de:8774/v2.1/servers/], but got 404 instead

In case you have deleted the running VMs to start fresh, provisioning might take a while and the above can be a false positive. Check the OpenStack management interface to see whether the VMs booted correctly. You can also remove a VM with „docker-machine rm <id>“ and run „gitlab-runner restart“ to automatically provision it again.

Whenever the VM provisioning fails, a gentle look into the syslog (or runner log) unveils what’s the problem. Lately we had used a wrong OpenStack flavor configuration which was fixed after investigating in the logs.

Oct 18 07:08:48 3 icinga-gitlab gitlab-runner[30988]:  #033[31;1mERROR: Error creating machine: Error in driver during machine creation: Unable to find flavor named 1234-customer-id-4-8#033[0;m  #033[31;1mdriver#033[0;m=openstack #033[31;1mname#033[0;m=runner-non-privilegued-icinga-1571375325-3f8176c3 #033[31;1moperation#033[0;m=create

Monitoring your GitLab CI runners is key, and with the help of the REST API, this becomes a breeze with Icinga checks. You can inspect the runner state and notify everyone on-call whenever CI pipelines are stuck.

 

Conclusion

Developers depend on fast CI feedback these days, speeding up their workflow – make them move fast again. Admins need to understand their requirements, and everyone needs a deep-dive into GitLab and its possibilities. Join our training sessions for more practical exercises or immediately start playing in NWS!

Cleanup your Docker Environment

Using Docker is pretty common meanwhile and a very good idea for development. Using many versions of your favourite language without messing up your host system, different types of deployments (e.g. web servers) or just testing production environment without operational support. The only drawback is that normally you don’t have a clue what’s going on behind the scenes. If you run out of disk space for the first time, you’re exactly at that point.

To apply first aid, you will be advised to use some curious cli hacks to clean up your system. Breaking fingers between grep, sed and awk works out well but is not very helpful – Especially if you want to remember what you did 3 months before 😉

Since Docker Api version 1.25 you have a couple of high level cli commands available doing exactly this job:

Minimal Cheat Sheet:

[bash light=“false“]
$ # Claimed disk space
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 59 5 10.74GB 9.038GB (84%)
Containers 6 1 991kB 991kB (99%)
Local Volumes 216 1 5.876GB 5.876GB (100%)
Build Cache 0 0 0B 0B

$ # Cleanup disk space
$ docker system prune
WARNING! This will remove:
– all stopped containers
– all networks not used by at least one container
– all dangling images
– all dangling build cache
Are you sure you want to continue? [y/N] Y
Deleted Containers:
02401e1555e8e752d36198d982b5e4114d0999c7cca34a2353e8dc332faa4db5
997eac76d4a46515797027103967c61b46219ff8c70f6e0bb39bc2b975297fa5
23983ed8abaa60198b497e4b3788bb6de7d39d03f171f43e4ee865c0df318ab8
65bb90b9e7edcd2d13da3129664f8b74a72b011d56136cb28c687f1f8dd8e473
5218788bff77cc0c0cc03f79888ea61c3e27bf3ef0003e41fc231b8b6ecdcdc2

Deleted Images:
deleted: sha256:dccdc3cf7d581b80665bad309b66ba36d88219829e1ade951912dc122b657bfc
[…]
[/bash]

There is also an equivalent for images only:

[bash light=“false“]
$ docker image prune
[/bash]

You should definitely take a deeper look into the CLI commands. There are a lot of things that helps you to solve your every day problems!

Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.