Dapr – Als Microservice-Entwickler endlich wieder auf den Applikationscode konzentrieren.

27.10.2021 Stefan Welsch
DevOps edge-computing devops cli distributed-systems framework howto tutorial golang k8s microservices

Dapr ist ein in Golang geschriebenes Framework, welches verspricht, dass es das Development von Cloud-Native Applikationen stark vereinfacht, so dass der Entwickler sich auf die Kernlogik seiner Applikation konzentrieren kann. Dapr wurde im Oktober 2019, nach anfänglichem Namen-Wirrwarr in einer ersten Alpha-Version veröffentlicht. Der Name war ursprünglich Dapper. Da es diesen Namen aber bereits gab, entschied man sich dazu den gleichen Wortlaut zu behalten und einfach das Wort selbst zu ändern.

Mittlerweile findet man Dapr in der Version 1.4 und laut Github gibt es momentan 139 Contributors.

Aber was ist nun Dapr? Hier die originale Beschreibung von der offiziellen Darp-Dokumentation:

Dapr is a portable, event-driven runtime that makes it easy for any developer to build resilient, stateless and stateful applications that run on the cloud and edge and embraces the diversity of languages and developer frameworks. Leveraging the benefits of a sidecar architecture, Dapr helps you tackle the challenges that come with building microservices and keeps your code platform agnostic.

Dapr kodifiziert die Best Practices zum Erstellen von Microservice-Anwendungen in offene, unabhängige Bausteine, die es Ihnen ermöglichen, portable Anwendungen mit der Sprache und dem Framework Ihrer Wahl zu erstellen. Jeder Baustein ist völlig unabhängig und Sie können einen, einige oder alle davon in Ihrer Anwendung verwenden.

Darüber hinaus ist Dapr plattformunabhängig, d. h. Sie können Ihre Anwendungen lokal, auf jedem Kubernetes-Cluster und in anderen Hosting-Umgebungen ausführen, in die Dapr integriert ist. Auf diese Weise können Sie Microservice-Anwendungen erstellen, die in der Cloud und auf Edge-Devices ausgeführt werden können.

Aufbau von Dapr

Wollen wir uns den Aufbau von Dapr etwas genauer anschauen. Wie wir bereits in der offiziellen Beschreibung lesen können, wird Dapr in einem Sidecar-Container an die eigene Applikation angebunden. Somit braucht man die eigentliche Applikation theoretisch nicht zu verändern.

In der Praxis sieht es jedoch etwas anders aus, da Dapr für verschiedene Sprachen SDKs zur Verfügung stellt. Nutzt man die SDKs so hat man also eine Abhängigkeit zu Dapr in seinem Code. Man muss hier allerdings nicht auf ein SDK zurückgreifen, sondern kann auch direkt per REST oder gRPC auf die verschiedenen Building Blocks zugreifen. Hier sehen wir den grundsätzlichen Aufbau der Architektur.

Building Blocks

Building Blocks sind ein über HTTP order gRPC bereitgestellte Schnittstelle, die bekannte Probleme einer Microservice-Architektur simpel lösen sollen. Dapr liefert standardmässig die folgenden 7 Building Blocks aus.

Service-to-service Invocation

Der Serviceaufruf ermöglicht es Anwendungen, über bekannte Endpunkte (HTTP oder gRPC) Nachrichten miteinander auszutauschen. Dapr bietet einen Endpunkt, der als Kombination aus einem Reverse-Proxy mit integrierter Serviceerkennung fungiert und gleichzeitig das integrierte Tracing- und Fehlerbehandlung nutzt. Dazu nutzt Dapr Service Discovery -Komponenten. Die Kubernetes-Service Discovery-Komponente ist beispielsweise in den Kubernetes DNS Service integriert.

Dapr erlaubt es ausserdem eine Custom-Middleware in den Request-Prozess einzuhängen. Damit ist es dann möglich zusätzliche Aktionen wie beispielsweise eine Authentifizierung oder die Transformation einer Nachricht zu machen, bevor der Request dann den Applikationscode erreicht.

State Management

Um Daten einer Applikation zu speichern, benötigt man eine Persistierung. Dapr bietet hierfür eine KeyValue-Store-API um Daten dann in austauschbaren Stores zu persistieren. Dapr bietet hier verschiedene Arten von Stores an, wie Dateisystem, Datenbank, Speicher. Gängige State-Store-Implementationen sind beispielsweise Redis oder AWS DynamoDB. Eine vollständige Liste findet man hier.

Publish und Subscribe

Dapr unterstützt das https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern , bei dem ein Sender eine Nachricht in eine Queue schreiben kann und ein Empfänger diese Nachricht abrufen und verarbeiten kann. Dapr unterstützt hier die gängigen Anbieter wie NATS Streaming oder Kafka. Eine vollständige Liste findet man hier.

Resource Bindings and Triggers

Hiermit ist es möglich eine Applikation mit einem externen Cloud- oder On-Premise System zu verbinden. Dapr ermöglicht es externe Systeme mit der Binding API aufzurufen, oder dass die eigene Applikation durch Events von verbunden Systemen getriggert werden kann. Unterstützte Komponenten sind hier beispielsweise Cron, HTTP oder auch Apple Push -Notifications. Weitere findet man hier.

Actors

Ein Actor ist eine isolierte und unabhängige Einheit, welche es ermöglicht Code und Daten voneinander zu trennen.

Observability

Dapr-Systemkomponenten und -laufzeit geben Metriken, Protokolle und Traces aus, um Dapr-Systemdienste, -komponenten und Benutzeranwendungen zu debuggen, zu betreiben und zu überwachen.

Secrets

Dapr bietet eine Secrets Building Block API und lässt sich in alle gängigen Secret-Stores wie beispielsweise AWS Secrets Manager oder Kubernetes integrieren. Über die Secrets-API kann man im Code dann Secrets speichern und auch wieder abrufen. Eine vollständige Lite aller unterstützten Secret Sores findet man hier.

Braucht man noch weitere Building Blocks, so kann man sich über das Extension Framework neue Blocks selbst programmieren.

Ist Dapr ein Service Mesh?

Da Dapr auch ähnliche Funktionalitäten wie ein Service Mesh bietet (beispielsweise Service-zu-Service Kommunikation mit mTLS), und auch als Sidecar Container deployed wird, liegt die Frage nahe, ob Dapr schlussendlich nicht einfach ein weiterer Service Mesh ist.

Dapr selbst gibt darauf die folgende Antwort:

Anders als ein Service Mesh, welcher sich auf Netzwerk Angelegenheiten fokussiert hat Dapr den Fokus darauf sogenannte Building Blocks (siehe oben) anzubieten, welche es dem Entwickler einfach machen, Applikationen als Microservice zu bauen. Dapr ist Entwickler-zentriert, während ein Service-Mesh Infrastruktur-zentriert ist.

Somit ist Dapr kein Service Mesh. Logischerweise folgt daraus, dass man Dapr gemeinsam mit einem Service Mesh nutzen kann. Es wird einzig empfohlen die Service-zu-Service-Kommunikation mit mTLS nur mit einem der beiden Frameworks zu nutzen, da diese Funktionalität sowohl von Dapr als auch von gängigen Service-Mesh Implementationen wie Linkerd oder Istio genutzt wird.

Dapr in der Praxis

Wollen wir uns Dapr nun in der Praxis anschauen. Zunächst einmal müssen wir uns die Dapr-CLI installieren. Auf dem Mac geht das sehr einfach über das Terminal oder über Brew. Ich wähle hier den Weg übers Terminal.

curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

Danach können wir mit dem Befehl dapr verifizieren, ob das CLI richtig installiert wurde.

dapr

	 __
    ____/ /___ _____  _____
   / __  / __ '/ __ \/ ___/
  / /_/ / /_/ / /_/ / /
  \__,_/\__,_/ .___/_/
	      /_/

===============================
Distributed Application Runtime

Usage:
  dapr [command]

Available Commands:
  build-info     Print build info of Dapr CLI and runtime
  completion     Generates shell completion scripts
  components     List all Dapr components. Supported platforms: Kubernetes
  configurations List all Dapr configurations. Supported platforms: Kubernetes
  dashboard      Start Dapr dashboard. Supported platforms: Kubernetes and self-hosted
  help           Help about any command
  init           Install Dapr on supported hosting platforms. Supported platforms: Kubernetes and self-hosted
  invoke         Invoke a method on a given Dapr application. Supported platforms: Self-hosted
  list           List all Dapr instances. Supported platforms: Kubernetes and self-hosted
  logs           Get Dapr sidecar logs for an application. Supported platforms: Kubernetes
  mtls           Check if mTLS is enabled. Supported platforms: Kubernetes
  publish        Publish a pub-sub event. Supported platforms: Self-hosted
  run            Run Dapr and (optionally) your application side by side. Supported platforms: Self-hosted
  status         Show the health status of Dapr services. Supported platforms: Kubernetes
  stop           Stop Dapr instances and their associated apps. Supported platforms: Self-hosted
  uninstall      Uninstall Dapr runtime. Supported platforms: Kubernetes and self-hosted
  upgrade        Upgrades or downgrades a Dapr control plane installation in a cluster. Supported platforms: Kubernetes

Flags:
  -h, --help          help for dapr
      --log-as-json   Log output in JSON format
  -v, --version       version for dapr

Use "dapr [command] --help" for more information about a command.

Nun können wir Dapr lokal installieren. Normalerweise läuft Dapr ja als Sidecar Container. Das bedeutet lokal, läuft es als separater Prozess. Mit dem folgenden Befehl werden also die Sidecar Binaries gezogen und auf dem lokalen Client installiert.

dapr init

Wenn alles installiert ist, können wir mit dapr --version überprüfen, ob alles korrekt installiert ist.

dapr --version
CLI version: 1.4.0
Runtime version: 1.4.2

Soweit so gut. Starten wir nun unseren ersten Dapr Sidecar Container. Hier hilft uns der Befehl dapr run. Wir starten nun einen Sidecar Container mit einer leeren Applikation myapp.

Hierbei werden die Standard-Komponenten genutzt welche wir im Dapr Konfigurationsordner (~/.dapr/components/) finden. Beispielsweise wird der lokale Redis Docker Container als State Speicher und Message Broker verwendet.

# dapr run --app-id myapp --dapr-http-port 3500
WARNING: no application command found.
ℹ️  Starting Dapr with id myapp. HTTP Port: 3500. gRPC Port: 58355
ℹ️  Checking if Dapr sidecar is listening on HTTP port 3500
INFO[0000] starting Dapr Runtime -- version 1.4.2 -- commit 786e808a98ea0cc51948cff04196604ef3728565  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] log level set to: info                        app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] metrics server started on :58356/             app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.metrics type=log ver=1.4.2
INFO[0000] standalone mode configured                    app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] app id: myapp                                 app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] mTLS is disabled. Skipping certificate request and tls validation  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] local service entry announced: myapp -> 192.168.178.20:58360  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.contrib type=log ver=1.4.2
INFO[0000] Initialized name resolution to mdns           app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] loading components                            app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] component loaded. name: pubsub, type: pubsub.redis/v1  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] waiting for all outstanding components to be processed  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] component loaded. name: statestore, type: state.redis/v1  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] all outstanding components processed          app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] enabled gRPC tracing middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.grpc.api type=log ver=1.4.2
INFO[0000] enabled gRPC metrics middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.grpc.api type=log ver=1.4.2
INFO[0000] API gRPC server is running on port 58355      app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] enabled metrics http middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.http type=log ver=1.4.2
INFO[0000] enabled tracing http middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.http type=log ver=1.4.2
INFO[0000] http server is running on port 3500           app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] The request body size parameter is: 4         app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] enabled gRPC tracing middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.grpc.internal type=log ver=1.4.2
INFO[0000] enabled gRPC metrics middleware               app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.grpc.internal type=log ver=1.4.2
INFO[0000] internal gRPC server is running on port 58360  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.actor type=log ver=1.4.2
WARN[0000] app channel not initialized, make sure -app-port is specified if pubsub subscription is required  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
WARN[0000] failed to read from bindings: app channel not initialized   app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] dapr initialized. Status: Running. Init Elapsed 29.511ms  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2
INFO[0000] placement tables updated, version: 0          app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime.actor.internal.placement type=log ver=1.4.2
ℹ️  Checking if Dapr sidecar is listening on GRPC port 58355
ℹ️  Dapr sidecar is up and running.
✅  You're up and running! Dapr logs will appear here.

Wollen wir nun was in unseren State Speicher schreiben. Dazu nutzen wir den folgenden Befehl:

curl -X POST -H "Content-Type: application/json" -d '[{ "key": "message", "value": "Hello from b-nova"}]' http://localhost:3500/v1.0/state/statestore

Wie wir sehen können, nutzen wir die Dapr API um direkt in den Redis-State Store zu schreiben.

Wollen wir uns die Nachricht wieder anzeigen lassen, so können wir dies mit dem folgenden Befehl tun:

curl http://localhost:3500/v1.0/state/statestore/message
"Hello from b-nova!"%

Nun können wir noch verifizieren, ob Dapr wirklich den Redis-Docker-Container als State-Speicher nutzt. Dazu verbinden wir uns folgendermassen direkt mit unserem Docker Container.

docker exec -it dapr_redis redis-cli

Da wir nun direkt in der redis-cli sind, können wir dann per keys * alle Keys abrufen.

127.0.0.1:6379> keys *
1) "myapp||message"

Nun wollen wir uns auch noch den Wert anzeigen lassen. Dazu geben wir Folgendes ein:

127.0.0.1:6379> hgetall "myapp||message"
1) "data"
2) "\"Hello from b-nova!\""
3) "version"
4) "1"

Eine eigene Komponente verwenden

Nun haben wir gesehen, wie man mit Dapr Standardkomponenten nutzen kann. Wollen wir uns als Nächstes anschauen, wie man seine eigene Komponente als KeyValue-Store nutzen kann.

Wir erstellen uns erstmal eine Datei im tmp Ordner /tmp/secrets.json mit dem folgenden Inhalt:

{
  "my-secret": "supersecret"
}

Danach erstellen wir uns einen Ordner my-components. In diesem Ordner erstellen wir eine Datei
my-local-secret-store.yaml mit dem folgenden Inhalt

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: my-local-secret-store
  namespace: default
spec:
  type: secretstores.local.file
  version: v1
  metadata:
  - name: secretsFile
    value: /tmp/secrets.json
  - name: nestedSeparator
    value: ":"

Das ist schon alles. Wir müssen nun lediglich beim Befehl zum Erstellen des Sidecar-Containers den Pfad zu unseren Komponenten angeben. Wir sollten dann in der Ausgabe den angegebenen String sehen.

dapr run --app-id myapp --dapr-http-port 3500 --components-path ./my-components

INFO[0000] component loaded. name: my-local-secret-store, type: secretstores.local.file/v1  app_id=myapp instance=Stefan-Welsch-MacBook-Pro.fritz.box scope=dapr.runtime type=log ver=1.4.2

Wollen wir schauen, ob unser Secret nun wirklich aus dem neuen eigenen Store gelesen wird.

curl http://localhost:3500/v1.0/secrets/my-local-secret-store/my-secret
{"my-secret":"supersecret"}%

(blue star)

Dapr Go SDK

Nun haben wir unsere erste eigene Komponente geschrieben und genutzt. Als Nächstes wollen wir uns noch eine Dapr-SDK anschauen.

Schreiben wir uns also ein ganz einfaches Go-Programm. Als Erstes erstellen wir ein neues Go-Projekt. Dazu geben wir die folgenden Befehle ein (Go Modules müssen enabled sein)

mkdir my-go-dapr-sdk
cd my-go-dapr-sdk
go mod init github.com/b-nova-techhub/my-go-dapr-sdk
go get github.com/dapr/go-sdk/client

Als Nächstes erstellen wir das main.go-File und schreiben den folgenden Code:

package main 

import (
    "context"
	"fmt"
    dapr "github.com/dapr/go-sdk/client"
	"log"
)

const (
	stateStoreName = `my-local-secret-store`
	daprPort       = "3500"
)

func main() {
    client, err := dapr.NewClient()
	if err != nil {
		panic(err)
	}
	defer client.Close()
	ctx := context.Background()

    item, err := client.GetSecret(ctx, "my-local-secret-store", "my-secret", make(map[string]string))
	if err != nil {
		fmt.Printf("Failed to get state: %v\n", err)
		return
	}
	log.Printf(item["my-secret"])
}

Nun kopieren wir den erstellten Ordner für unsere Custom-Komponente in das Go-Projekt. Die Ordnerstruktur sollte dann so aussehen.

my-go-dapr-sdk
  - go.mod
  - go.sum
  - main.go
  - my-components
    - my-local-secret-store.yaml

Um jetzt unser Programm zu testen, müssen wir noch Dapr gemeinsam mit der Applikation starten. Wie wir sehen, haben wir erfolgreich über das SDK das Secret aus unserem Custom-Secret-Store abgerufen.

 dapr run --app-id myapp --dapr-http-port 3500 --components-path ./my-components go run main.go
 
 ....
INFO[0000] dapr initialized. Status: Running. Init Elapsed 14.037ms  app_id=myapp instance=stefan-welsch-macbook-pro.home scope=dapr.runtime type=log ver=1.4.2
INFO[0000] placement tables updated, version: 0          app_id=myapp instance=stefan-welsch-macbook-pro.home scope=dapr.runtime.actor.internal.placement type=log ver=1.4.2
ℹ️  Checking if Dapr sidecar is listening on GRPC port 64963
ℹ️  Dapr sidecar is up and running.
ℹ️  Updating metadata for app command: go run main.go
✅  You're up and running! Both Dapr and your app logs will appear here.

== APP == dapr client initializing for: 127.0.0.1:64963
== APP == 2021/10/14 07:52:15 supersecret
✅  Exited App successfully
ℹ️  
terminated signal received: shutting down
✅  Exited Dapr successfully

Fazit

Dapr sieht nach einem sehr interessanten Ansatz aus, um die einfache Entwicklung von Cloud Native-Applikationen zu starten. Der Anfang ist etwas gewöhnungsbedürftig, aber wenn man das Konzept erstmal verstanden hat, geht die Entwicklung wirklich leicht von der Hand.

Wir bei b-nova werden die Entwicklung von Dapr auf jeden Fall weiter verfolgen und natürlich in einem b-nova internen Projekt exzessiv nutzen. (smile)

Stefan Welsch – Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.