Dapr - As a microservice developer, finally focus on the application code again.

27.10.2021Stefan Welsch
DevOps Dapr Edge Computing DevOps CLI Distributed Systems Framework How-to Tutorial Go Kubernetes Microservices

Dapr is a framework written in Golang, which promises that it will support the development of cloud-native applications are greatly simplified so that the developer can focus on the core logic of his application. Dapr was launched in October 2019, after an initial confusion of names in a first alpha version released. The name was originally Dapper. But since this name already existed, a decision was made to keep the same wording and just change the word itself.

Now you can find Dapr in version 1.4 and according to GitHub there are currently 139 contributors.

But what is Dapr now? Here is the original description from the official dapr documentation:

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 codifies the best practices for building microservice applications into open, independent building blocks, which enables you to build portable applications using the language and framework of your choice. Every building block is completely independent and you can use any, some, or all of them in your application.

In addition, Dapr is platform independent, meaning you can run your applications locally, on any Kubernetes cluster and run in other hosting environments that Dapr is integrated with. This way you can use microservice applications that can be run in the cloud and on edge devices.

Structure of Dapr

Let’s take a closer look at the structure of Dapr. As we already read in the official description, Dapr is connected to its own application in a sidecar container. So theoretically, you don’t need to change anything inside your application.

In practice, however, it looks a little different because Dapr provides SDKs for different languages. One uses the SDKs so you have a dependency on Dapr in your code. However, you don’t have to go to an SDK but can also access the various building blocks directly via REST or gRPC. Here we see the basic structure of the architecture.

Building Blocks

Building Blocks are an interface provided via HTTP or gRPC that address and simply solve known problems of a microservice architecture. Dapr delivers the following 7 building blocks as standard.

Service-to-service invocation

The service call allows applications to message each other through known endpoints (HTTP or gRPC) to exchange data. Dapr offers an endpoint that works as a combination of a reverse proxy with integrated service discovery functions and at the same time uses the integrated tracing and error handling. For this purpose Dapr uses Service Discovery components. For example, the Kubernetes Service Discovery component is integrated in the Kubernetes DNS Service.

Dapr also allows custom middleware to be added to the request process. With that it is possible doing additional actions such as authentication or the transformation of a message before the request reaches the application code.

State Management

In order to save the data of an application, you need a persistence. Dapr offers a KeyValue Store API for this to persist data in exchangeable stores. Dapr offers different types of stores here, such as file system, database, storage. Common state store implementations are, for example, Redis or AWS DynamoDB. A complete list can be found here.

Publish and Subscribe

Dapr supports the subscribe pattern, in which a sender can write a message in a queue and a recipient can retrieve this message and handle it. Dapr supports the common providers here like NATS Streaming or Kafka. A full list can be found here.

Resource Bindings and Triggers

This makes it possible to connect an application to an external cloud or on-premise system. Dapr enables you to call external systems with the binding API, or that your own application through events from connected systems can be triggered. Supported components are, for example, Cron, HTTP or Apple Push-Notifications. More can be found here.

Actors

An actor is an isolated and independent unit that enables code and data to be separated from one another.

Observability

Dapr system components and runtime output metrics, logs and traces to identify Dapr system services, components and Debug, operate, and monitor user applications.

Secrets

Dapr offers a Secrets Building Block API and can be used in all common secret stores such as AWS Secrets Manager or Kubernetes. The Secrets API can then be used to save secrets in the code and then again recall them. A full list of all supported secret stores can be found here.

If you need further building blocks, you can use the extension framework to create new blocks by yourself.

Is Dapr a service mesh?

Since Dapr also offers similar functionalities to a service mesh (like Service-to-Service communication with mTLS), and is also deployed as a sidecar container, the question arises whether Dapr will ultimately not just be another service mesh.

Dapr himself gives the following answer:

Unlike a service mesh, which focuses on network matters, Dapr focuses on to offer what are known as building blocks (see above), which make it easy for the developer to build and deliver applications as microservice. Dapr is developer-centric while a service mesh is infrastructure-centric.

Thus, Dapr is not a service mesh. Logically, it follows that Dapr can be used together with a service mesh. It is only recommended that service-to-service communication with mTLS is only allowed with one of the two frameworks, as this functionality is provided by both Dapr and common service mesh implementations like Linkerd or Istio.

Dapr in practice

Now let’s see Dapr in practice. First of all we have to install the Dapr CLI. On the Mac this can be done very easily via the terminal or via Brew. Here I choose the route via the terminal.

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

Then we can use the command dapr to verify whether the CLI has been installed correctly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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.

Now we can install Dapr locally. Normally, Dapr runs as a sidecar container. That means locally, it works as a separate process. With the following command the sidecar binaries are pulled, placed and installed on the local client.

1
dapr init

When everything is installed, we can use dapr --version to check that everything is installed correctly.

1
2
3
dapr --version
CLI version: 1.4.0
Runtime version: 1.4.2

So far so good. Let’s start our first Dapr Sidecar Container. The command dapr run helps us here. We now start a sidecar container with an empty myapp application.

Here the standard components are used which we find in the Dapr configuration folder (~/.dapr/components/). For example, the local Redis Docker container is used as state storage and message broker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 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.

Let’s now write something in our state memory. To do this, we use the following command:

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

As we can see, we are using the Dapr API to write directly to the Redis State Store.

If we want to display the message again, we can do this with the following command:

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

Now we can still verify whether Dapr really uses the Redis Docker container as state storage. To do this, connect directly with our Docker container as follows.

1
docker exec -it dapr_redis redis-cli

Since we are now directly in the redis-cli, we can then call up all keys via keys *.

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

Now we also want to display the value. To do this, we enter the following:

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

Use your own component

Now we have seen how standard components can be used with Dapr. Next, let’s see how to use its own component as a KeyValue store.

First we create a file in the tmp folder /tmp/secrets.json with the following content:

1
2
3
{
  "my-secret": "supersecret"
}

Then we create a folder my-components. In this folder we will create a file my-local-secret-store.yaml with the following content

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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: ":"

That’s all. All we have to do now is to enter the path to your component into the command to create the sidecar container. We should then see the specified string in the output.

1
2
3
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

Let’s see if our secret is really read from our own new store.

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

🚀

Dapr Go SDK

Now we have written and used our first own component. Next we want to have a look at Dapr SDK.

So let’s write a very simple Go program. The first thing we do is create a new Go project. In addition we give the following commands (Go Modules have to be enabled)

1
2
3
4
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

Next we create the main.go file and write the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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"])
}

Now we copy the created folder for our custom component into the Go project. The folder structure should then look like this.

1
2
3
4
5
6
my-go-dapr-sdk
  - go.mod
  - go.sum
  - main.go
  - my-components
    - my-local-secret-store.yaml

In order to test our program now, we still have to start Dapr together with the application. As we see we have successfully retrieved the secret from our custom secret store via the SDK.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 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

Conclusion

Dapr looks like a very interesting approach to making Cloud Native applications easy to develop. The beginning takes a bit of getting used to, but once you understand the concept, it works really easy.

We at b-nova will definitely continue to follow the development of Dapr and of course using it in an internal b-nova project. 😀


This text was automatically translated with our golang markdown translator.

Stefan Welsch

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.