ADMIN

2021

07

2021-07-01T12:00:00

Container- und Applikationsmanagement

SCHWERPUNKT

082

Containermanagement

Kubernetes

Tools

Container-Vorlagen erstellen und ausrollen

Planen, packen und verschiffen

von Andreas Stolzenberger

Veröffentlicht in Ausgabe 07/2021 - SCHWERPUNKT

Künftig sollen so viele Anwendungen wie möglich in Containern auf verteilten Kubernetes-Umgebungen laufen. Damit das funktioniert, müssen die Applikationen aber zuerst einmal in einen Container hineingelangen. Wir stellen Tools und Konzepte vor, um eigene Container nebst Vorlagen zu bauen, zu lagern und auf Kubernetes-Plattformen auszurollen.

Technologien wie Docker und Kubernetes stehen hoch im Kurs. Große Unternehmen mit eigener Software-Entwicklung investieren Millionen in Microservices und Scale-out-Plattformen, auf denen diese Dienste laufen. Der Weg von klassischen monolithischen Applikationen in VMs hin zu skalierenden Microservices in Containern ist jedoch lang und kompliziert. Kleinere und mittelgroße Unternehmen können und wollen bestehende Dienste nicht sofort migrieren und in eine nicht gerade billige Kubernetes-Umgebung mit eingebauter Entwicklungsplattform investieren. Die Migration bestehender Applikationen muss behutsam und Schritt für Schritt erfolgen.
Der Container und seine Applikation
Ein Container ist ein isolierter Prozess auf einem Betriebssystem, der wie eine eigene VM mit Betriebssystem erscheint, aber den Kern mit dem darunter liegenden System teilt. Jeder Container soll dabei jeweils nur einen Dienst betreiben und das nach Möglichkeit in einer Form, dass mehrere identische Container parallel laufen können und die zugehörige Applikation schneller machen. Ein beliebtes Beispiel ist der Anwendungsfall Webshop. An vorderster Front steht ein Container oder eine Appliance, die das HTML-Loadbalancing übernimmt. Dahinter arbeiten Webserver mit der Applikationslogik eines Warenkorbes in Containern. Diese beziehen den statischen HTML-Inhalt aus einem Shared Storage und die variablen Daten von einer Datenbank. Letztere wiederum läuft – wenn es die Architektur erlaubt – selbst in mehreren Containern.
Jetzt stellt sich also die erste Frage: Wie kommt der Dienst in den Container? Docker sowohl als Unternehmen als auch als Technologie hat sehr viel dazu beigetragen, den Umgang mit Containern zu vereinfachen und populär zu machen. Auf Basis bestehender Container-Tools und Docker entstand die "Open Container Initiative", kurz OCI. Sie beschreibt offene Standards zum Umgang mit Containern, deren Runtimes und Hilfsmitteln. Dank OCI gibt es heute eine Fülle zueinander kompatibler Tools, um Container zu verwalten und zu betreiben. Dazu gehören Docker, Podman, Buildah, Skopeo, Quay, CRI-O, runc oder containerd, um nur einige davon zu nennen.
Technologien wie Docker und Kubernetes stehen hoch im Kurs. Große Unternehmen mit eigener Software-Entwicklung investieren Millionen in Microservices und Scale-out-Plattformen, auf denen diese Dienste laufen. Der Weg von klassischen monolithischen Applikationen in VMs hin zu skalierenden Microservices in Containern ist jedoch lang und kompliziert. Kleinere und mittelgroße Unternehmen können und wollen bestehende Dienste nicht sofort migrieren und in eine nicht gerade billige Kubernetes-Umgebung mit eingebauter Entwicklungsplattform investieren. Die Migration bestehender Applikationen muss behutsam und Schritt für Schritt erfolgen.
Der Container und seine Applikation
Ein Container ist ein isolierter Prozess auf einem Betriebssystem, der wie eine eigene VM mit Betriebssystem erscheint, aber den Kern mit dem darunter liegenden System teilt. Jeder Container soll dabei jeweils nur einen Dienst betreiben und das nach Möglichkeit in einer Form, dass mehrere identische Container parallel laufen können und die zugehörige Applikation schneller machen. Ein beliebtes Beispiel ist der Anwendungsfall Webshop. An vorderster Front steht ein Container oder eine Appliance, die das HTML-Loadbalancing übernimmt. Dahinter arbeiten Webserver mit der Applikationslogik eines Warenkorbes in Containern. Diese beziehen den statischen HTML-Inhalt aus einem Shared Storage und die variablen Daten von einer Datenbank. Letztere wiederum läuft – wenn es die Architektur erlaubt – selbst in mehreren Containern.
Jetzt stellt sich also die erste Frage: Wie kommt der Dienst in den Container? Docker sowohl als Unternehmen als auch als Technologie hat sehr viel dazu beigetragen, den Umgang mit Containern zu vereinfachen und populär zu machen. Auf Basis bestehender Container-Tools und Docker entstand die "Open Container Initiative", kurz OCI. Sie beschreibt offene Standards zum Umgang mit Containern, deren Runtimes und Hilfsmitteln. Dank OCI gibt es heute eine Fülle zueinander kompatibler Tools, um Container zu verwalten und zu betreiben. Dazu gehören Docker, Podman, Buildah, Skopeo, Quay, CRI-O, runc oder containerd, um nur einige davon zu nennen.
Dockerfile nur für Tests
Wer via Docker Container baut, nutzt dazu meist das sogenannte Dockerfile. Dabei handelt es sich prinzipiell um ein Skript, das die Installation des Container-OS und der Applikation beschreibt. Die Syntax erinnert an Skripte, die Windows-Admins von Sysprep und Linux-Nutzer von Kickstart kennen. Ein sehr simples Dockerfile kann in etwa so aussehen:
FROM debian:latest
RUN apt update
RUN apt install -y mariadb-server
EXPOSE 3306
LABEL description="MariaDB Server auf Debian"
CMD ["mysqld"]
In der Praxis fallen Dockerfiles allerdings deutlich aufwendiger aus. Sie setzen weitere Umgebungsvariablen, deklarieren Pfade und kopieren falls nötig Dateien vom Host in den Container.
Ein Dockerfile ist allerdings nur für Container in der Phase von Entwicklung und Test geeignet. Das gilt insbesondere, wenn Sie Runtimes wie Python, PHP oder Node­JS in den Container installieren, um damit die Anwendung zu betreiben. Dockerfiles ziehen die jeweils aktuellen Versionen von Betriebssystem und den Runtimes nebst Abhängigkeiten in den Container. Dabei kann es zu Inkompatibilitäten zwischen der eigenen Applikation und der Runtime im Container kommen, wenn Sie die Versionen nicht abstimmen. Also wird der Administrator einen Container aus dem Dockerfile bauen, diesen ausführlich auf die Kompatibilität zum Code testen und danach aus diesem Container und dem getesteten Code ein Vorlagen-Image (S2I, Source to Image) bauen. Produktive Umgebungen klonen ihre Anwendungscontainer dann aus diesem Image.
Bild 1: Das auf Fedora mit Podman und Buildah erstellte Container-Image läuft problemlos per Docker-Desktop unter Windows
Container bleibt datenfrei
Sie müssen Ihre Container so konzipieren, dass keine langfristig benötigten Daten innerhalb des Containers lagern. Das Dateisystem des Containers selbst ist temporär und wird gelöscht, sobald der Container stoppt. Sie haben die Möglichkeit, externe Verzeichnisse von persistenten Datenspeichern in den Container einzubauen oder einzelne Dateien zum Containerstart in den Container hineinzukopieren.
Zurück zum Beispiel: Wir bauen einen Webserver-Container mit PHP-Interpreter. Beim Start eines Containers mountet dieser das Verzeichnis "/var/www/html" mit den statischen HTML/CSS-Seiten und dem PHP-Code von einem gemeinsam genutzten Speicher wie einer NFS-Freigabe als read-only. Dieses Verzeichnis teilen sich alle parallel laufenden Webserver-Container.
Die Konfiguration des Webservers in Form einer conf-Datei injizieren wir beim Containerstart. Häufig kommen auch Um­gebungsvariablen zum Einsatz, die das Start-Skript im Container auswertet und dementsprechend eine Konfigurationsvorlage anpasst. Datenbanken in Containern erfordern etwas mehr Startlogik. Der Container muss darauf reagieren, ob ein extern eingebundenes Verzeichnis leer oder mit einer bestehenden Datenbank bestückt ist.
Bauen mit Buildah
Viele IT-Profis sind es nicht gewöhnt, ihre Applikation komplett deklarativ zu beschreiben, und tun sich anfangs mit Dockerfiles sehr schwer. Daher gibt es auch einen interaktiven Ansatz mit dem Tool "Buildah". Das Toolgespann Podman, Buildah und Skopeo kommt ohne Root-Rechte aus, was den Umgang mit Containern erheblich erleichtert.
Zunächst braucht es einen leeren Container mit einem Betriebssystem-Image, also beispielsweise
c=$(buildah from fedora:latest)
oder
c=$(buildah from debian:latest)
Die Variable "$c" enthält dann den Namen des erstellten Containers und erleichtert die weiteren Kommandos. Via
buildah run $c bash
öffnen Sie eine interaktive Shell innerhalb des Containers. Um dabei ein Verzeichnis des Hosts in den Container einzublenden, nutzen Sie die Option"-v", also:
buildah -v /home/user/dir:/var/www/html:Z run $c bash
Dabei geben Sie zuerst das lokale Verzeichnis und danach den Mount Point im Container an. Das ":Z" am Ende benötigen Host-Systeme mit aktivem SELinux, sonst schlägt der Mount fehl.
Möchten Sie einfach nur Dateien vom lokalen Rechner in den Container integrieren, erledigen Sie das mit buildah copy. Allerdings sollten Sie nicht schon in der frühen Template-Phase zu viele Konfigurationsdaten einbauen, denn sonst ist das resultierende Image später nicht mehr flexibel verwendbar.
Nun können Sie im Container Anwendungen installieren und testen, als würden diese sich in einer regulären virtuellen Maschine befinden. Es gibt jedoch Einschränkungen: Einem Container-Image fehlt in der Regel ein init-System wie systemd. Da jedoch der Container ohnehin nur einen einzigen Dienst betreiben soll, müssen Sie folglich einen "Entrypoint" definieren und damit festlegen, welches Kommando der Container beim Start ausführt. Dieser Entrypoint im Container zeigt selten direkt auf den Dienst, sondern vielmehr auf ein Start-Skript, das zunächst einige Vorbereitungen trifft, die Umgebung prüft und erst danach den gewünschten Dienst ausführt. Ferner sollten Sie nach der Konfiguration im Container aufräumen und temporäre Daten entfernen. Dazu zählt beispielsweise der Cache des Paketmanagers.
Listing 1: Von Null zum Container
Wollen Sie nicht nur die Applikation, sondern auch gleich das Betriebssystem Ihres Containers manuell aufsetzen, können Sie dies mit "buildah from scratch" erledigen. Ein Beispiel auf Fedora 33 sieht so aus: c=$(buildah from scratch) m=$(buildah mount $c) ← braucht root rechte dnf install –installroot $m bash coreutils –releasever=33 dnf clean all -y --installroot $m --releasever 33 buildah run $c bash Ein so erzeugter Container fällt noch einmal kleiner aus als die offiziellen Linux-Container-Images aus der Docker- oder Quay-Registry.
Der Vorteil der interaktiven Buildah-Methode ist, dass Sie die Dienste im Container ausprobieren und debuggen, falls nötig weitere Abhängigkeiten installieren oder unbenutzte Bibliotheken löschen können. Wenn dann alles klappt wie gewünscht, lässt sich die Bash-History, also die Verlaufsliste der eingegebenen Kommandos, als Vorlage für ein Dockerfile benutzen.
Ein exit bringt Sie jetzt auf die Kommandozeile des Hosts zurück, ohne dabei jedoch den Container zu löschen. Jetzt kann buildah den Namen, die Metadaten und den Entrypoint festlegen:
buildah config --entrypoint /entrypoint.sh $c
buildah config --label name=my_template1 $c
buildah commit $c my_template1
Das Kommando Commit baut aus den Schichten des Arbeitscontainers ein neues Image, das Tools wie Docker oder Podman später als Vorlage für neue Container verwenden.
Container in Registry laden
Der mit Buildah erstellte Container liegt nun auf dem lokalen Rechner des Entwicklers. Jetzt laden Sie das Image in eine Registry, auf die andere Systeme Zugriff haben. Es gibt eine ganze Reihe freier und kommerzieller Docker-Registry-Server und -Services. Die Installationen von Diensten wie Pulp 3, Docker Registry oder Quay können komplex ausfallen, insbesondere wenn diese Dienste im Internet laufen und eine sichere Authentisierung benötigen.
Für simple erste Tests nutzen wir hier die Registry-Funktion von www.gitlab.com. Auch kostenfreie Gitlab-Konten können private Git-Repositories anlegen und innerhalb dieser Repositories mit dem Docker-Registry-Feature arbeiten.
Um das im Beispiel erstellte Abbild in einer Registry abzulegen, muss sich Buildah dort erst einmal authentisieren:
buildah login registry.gitlab.com/<User-Name>
Mit dem Commit-Kommando erstellt buildah aus dem Working-Container ein Abbild. Alle lokalen Abbilder listet buildah images auf. Im oben gezeigten Beispiel heißt das Abbild "my_template1". Also laden Sie es wie folgt in die Registry:
buildah push my_template1 registry.gitlab.com/<User-Name>/<Repo-Name>: my_template1
Jetzt steht das Image auch anderen Systemen zur Verfügung. In der Praxis dürften Sie Ihre containerisierten Applikationen auf anderen Umgebungen laufen lassen als dem Developer-System. Das können dann Kubernetes-Angebote von einem der großen Hyperscaler wie Amazon und Google sein, ebenso wie Kubernetes-Cluster im eigenen Rechenzentrum auf Basis von Rancher, Tanzu, OpenShift oder CDK.
Damit diese Kubernetes-Plattformen auf die Registry zugreifen können, bedarf es zweierlei: eines passenden Read/Only-Accounts auf der privaten Registry und eines passenden Kubernetes-Secret. Die Details dazu entnehmen Sie dem Kasten "Kubernetes-Setup".
Listing 2: Kubernetes-Setup
Kubernetes verbietet, Passwörter im Klartext zu verwenden. Eine Pod-Definition kann daher keine direkten Login-Credentials zu einer speziellen Registry enthalten. Damit eine der genannten Kubernetes-Plattformen Images vom persönlichen Gitlab lesen kann, braucht es ein Kubernetes-Secret, das Sie als Teil eines Projekts sichern: kubectl create secret docker-registry <Secret-Name> \ --docker-server=https://registry.gitlab.com \ --docker-username=<Gitlab-Login> \ --docker-password=<Gitlab-Token> \ --docker-email=<Mailadresse> Ein Kubernetes-Deployment nutzt dann Secret und Image der privaten Registry: kind: Deployment apiVersion: apps/v1 metadata:        name: <Name des Deployments> spec:     replicas: 1     template:        metadata:           labels:              app: <Name der Applikation>        spec:           containers:           - name: <Name des Container Images>              image: registry.gitlab.com/<User-Name>/<Registry-Name>:<Image-Name>              ports:              - containerPort: <Port>                 protocol: TCP           imagePullSecrets:           - name: <Secret-Name> Das Deployment sorgt dafür, dass stets die unter "replica" angegebene Zahl an Containern läuft. Stürzt ein Container ab, rollt diese Deployment-Konfiguration sofort einen neuen aus. Soll der Container auf ein Persistent Volume zugreifen, muss dies im Abschnitt "containers:" mit angegeben werden, beispielsweise: - name "xyz"      …      volumeMounts:      - mountPath "/var/www/html"          name: my_pv Das setzt voraus, dass das persistente Laufwerk mit dem Namen "my_pv" existiert. Sollen mehrere Container simultan darauf zugreifen, muss es zuvor mit einem Claim des Typs "ReadWriteMany" erstellt worden sein, beispielsweise: apiVersion: v1 kind: PersistentVolumeClaim metadata:       name: my_pv_claim spec:       storageClassName: <storage-class>       accessModes:            - ReadWriteMany       resources:            requests:                 storage: 5Gi Der Claim bedingt wiederum, dass eine Storage-Klasse namens "storage-class" existiert, die automatisch ein passendes Volume bereitstellen kann.
Im Beispiel kommt die Gitlab-Registry zum Einsatz. In den Nutzereinstellungen von Gitlab über "User / Preferences" lassen sich "Access-Token" mit eingeschränkten Zugriffsrechten generieren. Somit können Sie mit Ihrem uneingeschränkten Account Ihre Images verwalten, ändern und hochladen. Ein "read_registry"-Token darf dabei jedoch nur auf Registry-Images, nicht aber auf sonstige Repository-Inhalte lesend zugreifen.
So hat es das Container-Image von der lokalen Build-Umgebung zur Registry und in eine Kubernetes-Umgebung geschafft. Bis aus dem Image produktive Applikationen entstehen, bedarf es noch ein paar anderer Dinge. Wie eingangs erwähnt, müssen Sie die Daten der containerisierten Applikation außerhalb des Containers lagern. Nur so kann der Container "stateless" arbeiten und ist jederzeit ohne Datenverlust ersetzbar. Davon ausgenommen sind moderne Scale-out-Applikationen, die ihre Datenbestände redundant über mehrere Container verteilen.
Aktuell greifen viele Webapplikationen jedoch nach wie vor auf SQL-Datenbanken wie PostgreSQL oder MariaDB zu. Diese Dienste erreichen Hochverfügbarkeit jedoch nur über klassische Active-Passive-Failover-Setups. Das setzt zwangsweise voraus, dass die Datenbankdaten nicht im Container lagern. Webentwickler sollten prüfen, ob Ihre Anwendungen unbedingt eine klassische SQL-Datenbank benötigen oder ob moderne Scale-out-Datenbanken wie Apache Cassandra, MongoDB, Couchbase oder auch Elasticsearch nicht vielleicht eine bessere Wahl wären.
Entwickler muss vorplanen
Die Trennung von Daten und Applikation konfrontiert den Entwickler mit neuen Problemen, denn die Anwendung im Container kann in zwei Zuständen starten: Im ersten Szenario rollt der Nutzer eine neue Applikation ohne bestehende Daten aus. Das externe Laufwerk am Container (ein sogenanntes PV oder Persistent Volume) ist in diesem Fall leer. Die Applikation im Container muss entsprechend reagieren und gegebenenfalls eine initiale Datenstruktur erstellen. Fehlt diese Logik, würde der Container einfach mit einem "File not Found" Fehler abstürzen.
Im zweiten Szenario übernimmt der Container eine bestehende Datenstruktur auf dem PV. Diese kann von einem zuvor abgestürzten Container stammen oder von einer früheren Applikationsversion. Auch in diesem Fall muss der startende Container zunächst die Datenstruktur prüfen, nötigenfalls updaten und erst danach die zugehörige Applikation starten. Die passende Logik muss der Anwendungsentwickler von Anfang an mit in seinen Container packen – oder er verlässt sich auf externe Verwaltungstools.
Bild 2: Der Git-Service Gitlab.com offeriert auch eine Container-Registry. Der Zugriff darauf lässt sich mit Access-Token einschränken.
Die neuen Installer
Wie bei nativen Windows- oder Linux-Applikationen gibt es für Kubernetes-Cluster Paketmanager und Installationstools. Ein beliebtes Werkzeug, um containerisierte Applikationen auf Kubernetes auszurollen, ist Helm. So genannte Helm-Charts beschreiben die Applikation samt Services und Routen. Helm-Code lehnt sich an der YAML-Syntax der Kubernetes-Deklarationen an und setzt zudem ein Variablensystem und Templates ein. Das eigentliche Helm-Chart erstellen Sie auf der Entwicklerstation. Helm braucht zudem einen Client namens "Tiller", der auf der Kubernetes-Zielplattform im Namespace der zu verwaltenden Applikation läuft. Helm sichert Versionsnummern und Abhängigkeiten in den Charts. So stellt das Tool sicher, dass benötigte Abhängigkeiten auf dem Cluster existieren und ein Update nicht zwingend die komplette Applikation, sondern nur die geänderten Komponenten ersetzt.
Flexibler als Helm arbeiten Kubernetes-Operatoren. Neben dem regulären Applikationsmanagement können Operatoren auch Jobs starten, also temporäre Container für Verwaltungsaufgaben als "run once" starten. Wer mit Operatoren arbeitet, kann sich im besten Fall den Verwaltungscode in seiner Applikation ersparen und stattdessen das Management von PVs und den darauf gesicherten Daten eigenen Maintenance-Containern übertragen. Teilweise können Kubernetes-Operatoren bestehende Helm-Charts oder Ansible-Playbooks übernehmen. Wer jedoch die volle Funktionsvielfalt eines Kubernetes-Operators ausschöpfen will, kommt nicht um Golang-Code herum. Wie Kubernetes selbst sind die Operatoren in Googles Programmiersprache Go geschrieben.
Bild 3: Ein mit Buildah erstelltes und auf Gitlab gelagertes Image lässt sich in Kubernetes-Deployments ausrollen.
Fazit
Der Weg von monolithischen Applikationen in virtuellen Maschinen hin zu Containern ist langwierig und komplex. Wichtig dabei ist, dass Sie nicht versuchen, ihre bestehenden Anwendungen 1:1 in einen Container zu stopfen. Ein großes Problem stellen dabei vor allem die klassischen Datenbanken dar, die (noch) keinen Scale-out-Betrieb in parallelen Knoten erlauben. Für Operatoren und Entwickler bedeuten alte Active-Passive-Konzepte mit geteilten PV-Laufwerken einen erheblichen Mehraufwand beim Container- und Deployment-Setup. Langfristig wird es sich jedoch lohnen, monolithische Anwendungen zu konvertieren und sie damit sowohl im heimischen Rechenzentrum als auch in Public Clouds lauffähig zu machen.
(ln)