Consistent Delivery mit Werf

28.01.2021 Raffael Schneider
DevOps Cloud CI/CD GitOps Kubernetes Hands-on

Wir bei b-nova haben uns dem GitOps-Pattern verschrieben. Dieser Artikel ist ein weiterer Teil einer mehrteiligen Serie über das GitOps-Thema. Falls Sie das GitOps-Pattern noch nicht kennen sollten, schauen Sie sich vielleicht zuerst den ersten Teil GitOps als Devops oder den zweiten Teil über Argo CD an. Kurz zusammengefasst ist die Idee bei GitOps Git als Single-Source-of-Truth für den Applikations-, aber auch Infrastruktur-Code (Infrastructure-as-Code) zu nutzen.

Mit Argo CD eignet sich zum Beispiel ideal, um über Infrastruktur-Repos automatisierte Pipelines in Ihren Kubernetes-Cluster zu integrieren. Dabei wird geprüft, ob der Zielzustand, beschrieben in dem entsprechenden Git-Repo, auf dem Cluster bereits vorhanden ist. Falls nicht, würde Argo CD den Prozess anstossen um diesen Zielzustand auf dem Cluster zu etablieren.

Die Idee GitOps wird mit werf –dem sogenannten 'Consistent Delivery Tool' von Flant– noch konsequenter als mit Argo CD umgesetzt. Anstatt nur die Infrastruktur-Codebase als Single-Source-of-Truth zu nutzen, wird bei werf auch die Applikations-Codebase berücksichtigt. Somit ist der ganze Prozess aus GitOps-Sicht deterministisch und idempotent: Der ganze Prozess wird von Git-Repo bis zum laufendem Pod im Kubernetes-Cluster für alle DevOps-Stakeholder noch transparenter.

In diesem Beitrag werden wir uns die Grundfunktionalität, die werf bietet anschauen. Danach werden wir gemeinsam in einem kurzen Handson eine Git-Repo mit werf bauen lassen.

Der Bauplan mit werf

werf ist in Go geschrieben und ist im Grunde ein einfaches CLI-Tool. Es ist genau für einen GitOps-Fall gedacht; nämlich in Zusammenschluss mit Git-Repositories für Applikation und Infrastruktur, einer Docker-Repository und Kubernetes als Platform.

Im folgenden Diagram ist der Aufbau eines GitOps-Patterns mit werf ersichtlich: werf-concept

werf spielt dabei das Bindeglied zwischen den Git-Repositories (oben), der Docker-Repositories (rechts) und dem Kubernetes-Cluster (unten). werf bietet mit werf converge, dem Hauptbefehl von werf, die Möglichkeit den Zustand auf der Git-Repo mit dem Zustand der Docker-Repository mit dem laufendem Zustand auf dem Kubernetes-Cluster zu synchronisieren.

In anderen Worten löst werf converge den Prozess aus, wobei werf prüft ob es für den vorhandenen Code-Zustand in der Git-Repo bereits entsprechende Docker-Images in der Docker-Repository gibt, falls ja, prüft werf ob diese Docker-Images in dieser Form bereits auf dem Kubernetes-Cluster am laufen sind. Falls nicht, veranlasst werf den Bau, den Push in die Docker-Repository, sowie das Ausrollen der neuen Docker-Images. Dieser Prozess wird auf der offiziellen Introduction-Seite noch detaillierter beschrieben.

Somit kümmert sich werf um:

  • das Bauen von Docker-Images
  • das Ausrollen der Applikation in den Kubernetes-Cluster
  • das Sicherstellen, dass die Applikation lauffähig und healthy auf dem Cluster läuft
  • falls nötig, das erneute Bauen von Docker-Images, wenn der Code sich geändert hat
  • falls nötig, das erneute Ausrollen der Applikation auf den Cluster
  • das Aufräumen von obsoleten oder irrelevanten Docker-Images in der Docker-Registry

Die Kernfunktionen die werf nutzt und dem Nutzer bereitstellt sind:

  • werf build: Docker-Images bauen
  • werf publish: Docker-Images taggen und auf Docker-Repository hochladen
  • werf publish-and-publish: Zuerst build, danach publish
  • werf deploy: Zustand von Docker-Images mit Zustand von Kubernetes synchronisieren
  • werf converge: Zuerst build, danach publish, zuletzt deploy
  • werf run: Unit-Testing des gebauten Docker-Images
  • werf dismiss: Applikation im Kubernetes-Cluster terminieren
  • werf clean: Ungenutzte Docker-Images aus der Repository löschen

Die wichtigste Funktion ist, wie vorhin bereits beschrieben, werf converge. Man kann werf jedesmal von Hand oder werf irgendwo automatisiert ausführen lassen. Um dies zu veranschaulichen, werden wir werf im nächsten Schritt zusammen praktisch ausprobieren.

Werf im Einsatz

Um die Kernfunktionalität und das Feeling von werf besser zu verstehen, werden wir jetzt werf aufsetzen und ein Beispiel-GitOps-Pattern mit einer personalisierten Git-Repository durchspielen. Dabei lehnen wir uns an den offiziellen Quickstart von werf an.

Die Idee dabei ist, dass Docker-Images für mehrere Applikationen, die in einer Git-Repo vorhanden und definiert sind, per werf bauen lassen und den lokalen Kubernetes-Cluster damit bespielen. Nach dem Ausrollen der Applikation nehmen wir einen Change an der Codebase einer Applikation vor und schauen, ob und wie werf danach das GitOps-Pattern handiert.

Voraussetzungen

Damit Sie auch mitmachen können, sollten Sie folgende Voraussetzungen im Vorfeld erfüllen:

  • Docker installiert und running
  • Docker Hub-Account (oder gleichwertige Docker-Repository) vorhanden
  • Lokales Minikube installiert und running (siehe hier Minikube Installation)

Git-Repo forken

Unter github.com/b-nova/quickstart-application gibt es eine Quickstart-Applikation-Repo. Am besten forken Sie sich diese Git-Repo in Ihr persönliches Repo. Anschliessend klonen Sie sich die geforkte Repo auf ihren Rechner (hier exemplarisch mit der Repo von b-nova):

$ git clone https://github.com/b-nova/quickstart-application.git

Architektur der Applikation im Git-Repo

Die obige Git-Repository beinhaltet 5 Komponenten, davon 3 Applikationen (vothing-app, result-app und worker) und zwei Persisiterungseinheiten (redis und db). Mit der Gesamtapplikation lassen sich Abstimmungen vornehmen, wobei voting-app als Eingabe-UI der einzelnen Abstimmungen und result-app als Darstellungs-UI der Abstimmungsresultate dient. Das folgende Schema zeigt dies auf: werf-concept

Für uns interessanter ist die Folder-Struktur. Unter den jeweiligen Sub-Foldern result/.., vote/.. und worker/.. liegt jeweils ein Dockerfile, die die entsprechende Applikation als Container beschreibt. Unter .helm/.. liegen Helm-Charts, die die Cluster-Konfiguration von Kubernetes beschreiben. Auf Root-Ebene liegt noch die zentrale werf.yaml, die werf die GitOps-relevanten Informationen zukommen lässt. Hier die werf.yaml von unserer Quickstart-Applikation Git-Repository:

configVersion: 1
project: quickstart-application
---
image: vote
dockerfile: Dockerfile
context: vote
---
image: result
dockerfile: Dockerfile
context: result
---
image: worker
dockerfile: Dockerfile
context: worker

Die werf.yaml ist sehr einfach gehalten und gänzlich selbsterklärend. Installieren wir nun als Nächstes werf.

werf installieren

werf hat ein flexibles Installationstool, das es Ihnen erlaubt die richtige werf-Binary für Ihr Zielsystem zu ziehen. Das heisst multiwerf, und so geht es:

# add ~/bin into PATH
export PATH=$PATH:$HOME/bin
echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc

# install multiwerf into ~/bin directory
mkdir -p ~/bin
cd ~/bin
curl -L https://raw.githubusercontent.com/werf/multiwerf/master/get.sh | bash

Einfach ausführen und schon kann man mit werf version prüfen, ob man die letzte Version aufrufen kann.

Falls alles funktioniert hat, sind wir bereit werf auf unsere Applikationen loszulassen.

werf converge

Bevor wir hier weitermachen, vergewissern Sie sich, dass Minikube bei Ihnen lokal läuft und einsatzbereit ist. Falls nicht, versuchen Sie es wie folgt. Wichtig ist docker als Treiber anzugeben.

minikube start --driver=docker

Wechseln Sie nun in die Repository.

cd quickstart-application

Und nun führen Sie werf converge aus. Dabei geben wir mit dem --repo-Flag unsere Docker-Repository (ich nutze hier meine), mit der werf die zu bauenden Docker-Images synchronisierne soll. Und mit dem --repo-docker-hub-token-Flag geben wir einen Security-Token mit, dass die Docker-Registry weiss wer wir sind.

werf converge --repo raffaelschneider/quickstart-application --repo-docker-hub-token <some-token-caf02c3e-...>

Der Output von werf converge seht in etwa so aus: werf-concept

Man sieht dabei, dass das Ausrollen der Docker-Images auf den Cluster mit jeweils einer Replicas erfolgreich am laufen ist.

Die zwei Frontends, voting-app und results-app kann man unter folgenden Endpunkten http://127.0.0.1:51226/ und http://127.0.0.1:51329/ aufrufen. Falls die Ports anders sein sollten, kann man per minikube service die entsprechenden Endpunkte ausfindig gemacht werden:

minikube service --namespace quickstart-application --url vote
minikube service --namespace quickstart-application --url result

Die UIs sollten so aussehen. Links die Result-App, rechts die Vote-App. werf-concept

Jetzt ist es interessant zu wissen, wie sich werf verhält wenn man was in der Repository anpasst und werf converge nochmals ausführt. Dafür können Sie beispielsweise den Titel 'Cats vs Dogs!' der Result-App in der result/views/index.html austauschen:

<title>Katzen vs Hunde -- Result</title>

Pushen Sie anschliessend den Change in die Repo und lassen Sie werf converge nochmals laufen. Sie werden sehen, dass die Docker-Images neu gebaut werden, mit einem neuen Tag in die Docker-Registry hochgeladen werden, und das Frontend den neuen Titel tatsächlich ausgibt, sobald das Ausrollen in den Minikube-Cluster erfolgt ist.

Fazit

werf ist ein kleines, aber feines CLI-Tool um einfach und schnell ein GitOps-Pattern in Ihrem Kubernetes-Cluster einzuführen. Dabei wird nicht nur der Infrastruktur-Code, sondern auch der Applikationscode berücksichtigt. Dies ermöglicht ein ganzeinheitliches GitOps-Pattern, das von der Wiege zur Senke das Paradigma zulässt.

Falls Ihnen der Beitrag zu werf gefallen hat, lassen Sie es uns wissen und verpassen Sie es nicht Dmitry Stolyarov, Entwickler von werf und CEO von Flant, persönlich in seinem CNCF Webinar zu Delivering cloud-native apps to Kubernetes using werf zuzuhören.

Wir bei b-nova sind begeistert von GitOps und würden auch Ihnen gerne unterstützen Ihr GitOps-Pattern beispielsweise mit werf zum Erfolg zu verhelfen.

Raffael Schneider – Crafter, Disruptor, Freigeist. Als begeisteter GitOps-Enthusiast schreibt Raffael gerne über Programmiersprachen, Themen rund um DevOps und hat ein Faible für die neusten IT-Hypes aller Art.