Dapr: Distributed Application Runtime

Veröffentlicht von Max Körbächer am

Dapr – a toolbox for distributed systems

The growing complexity of distributed, containerized systems presents many developers with new challenges. This creates new interfaces within an application, which also require new approaches for the integration. Further problems arise not only technically but also organizationally for projects and their operation.

Essentially, this raises some questions. Who must provide the necessary systems for the integration layer within a distributed application? Who manages this layer? Who is responsible for the operation? How long does it take to set up the various services required for integration (message bus, state store, keystore, TLS, auth)?

A developer-driven open-source project called Dapr (Distributed Application Runtime) is an offensive proposal to solve these questions, use necessary middleware on your own and still have a guarantee to rely on robust and reliable services.

Here, Dapr is not to be compared with a Java Runtime. It rather provides a multitude of functions and simple integrations as needed by all distributed systems. We consider Dapr as a toolbox from which we can help ourselves with the appropriate tools.

The Dapr building blocks

Dapr sees itself as a modular construction kit or toolbox from which the appropriate building blocks can be obtained. Currently, seven of these building blocks are available, whereby the framework is continuously being developed as an open-source project and can be flexibly expanded. The goal of each building block is to answer widespread challenges in a simple, scalable, robust and best-practice solution. The following figure is intended to give a brief overview of the current building blocks.

Source: https://docs.dapr.io/concepts/building-blocks-concept/

Dapr is designed for high reusability, not only in providing services and their use directly in the code but also within its own source code structures. These so-called components are used in different building blocks. For example, the state management and the actor building block use the component „state“ to store certain conditions. The component „state“ then speaks in the background, for example, with a Redis database. 

The functions and services provided by the individual building blocks will be explained in the following chapters. First, however, we look at the general functionality of Dapr.

Basic Functionality

Dapr is cloud or infrastructure agnostic and can run both as a standalone solution or as a container on a Kubernetes cluster. The latter simplifies some steps in the deployment and is most commonly used. The following image, therefore, shows a Kubernetes installation.

For the operation and configuration of Dapr, several management pods are initialized. Each of these pods performs a specific task:

  • Operator: Manages component updates and Kubernetes service endpoints for Dapr (such as state stores, pub/subs, etc.).
  • Sentry: Manages mTLS between services and acts as a certificate authority.
  • Sidecar Injector: Injects Dapr into annotated deployments and adds the environment variables DAPR_HTTP_PORT and DAPR_GRPC_PORT to allow applications to easily communicate with Dapr without the need for hard-coding Dapr ports.
  • Actor Placement: This is only used for actors. Creates mapping tables that map actor instances to pods.
Source: https://docs.dapr.io/concepts/overview/

As already explained in the example of the „state“ component, Dapr is able to address different databases. Other exemplary tools that can be integrated as components are shown on the right side under labeled components.

The most exciting part of Dapr is the sidecar container that runs within a pod parallel to the container with the application logic. The sidecar pattern is widely used in the Kubernetes context to extend deployments without installing additional agents, daemons, or anything else directly into the application container. We will not discuss the sidecar pattern further here, but one last thing is essential to understand; containers running inside a pod share all resources. In the case of Dapr, the application communicates with the Dapr sidecar via HTTP or gRPC. We will look at an example of this in a moment.

How does Dapr knows which Pod requires a sidecar?

For Dapr to instruct Kubernetes to place a sidecar in a pod, just annotations need to be added to a deployment manifest. Custom settings can be set via annotations. The „dapr.io/“ annotation is specified in the deployment manifest at spec.template.annotations. The enabled flag specifies whether a sidecar should be injected or not, while the app-id flag assigns a unique identifier to the pod.

Example annotation of dapr

If you are not familiar with Kubernetes deployments or annotations, we recommend exploring the relevant chapter at the Kubernetes documentation on kubernetes.io.

Integrate complex, distributed applications

As promised at the beginning, we will now go a little deeper into a few selected building blocks of Dapr, their functionalities and capabilities. But we also want to encourage everyone to try out Dapr for themselves. The following sections are intended to provide an introduction and overview of the most commonly seen use cases.

Service-to-Service Invocation

The foundation for the functionality of distributed applications is the interaction between their services. In recent years, several interface patterns have been established that map this communication in a standardized way. For direct and coupled calls, the REST and gRPC patterns have become established. Due to its performance advantages, there has been a strong trend toward gRPC lately.

Various requirements arise for distributed applications from different perspectives:

  • Where is the target service located? And how is it addressed?
  • What transmission errors can occur and how should they be handled?
  • Which protocol is used for the transmission or expected by the recipient?
  • How is it ensured that the calls are encrypted?
  • How can access policies and authorization be implemented?
  • How can errors in the transmission be made visible?
  • How can interfaces and protocols be standardised and independent of software frameworks?

Dapr uses a reverse proxy pattern to answer these questions: Communication between components does not occur directly but through sidecar provided by Dapr as a container or process. Here, the Dapr sidecar acts as a „messenger“ for the defined requirements.

Source: https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/

The application passes the call to Dapr via gRPC or HTTP. This is done by a call to localhost:{dapr-port}/v1.0/invoke/{app-id}/method/{method-name} or by using the Dapr client SDK. Due to its language-agnostic implementation, Dapr allows for flexible use of frameworks. For most widely used languages (e.g. Java, .NET, Python, PHP, …) there are already SDKs that encapsulate this functionality and the other building blocks in language-specific clients available.

As soon as the call arrives at the Dapr sidecar, the correct recipient is determined via mdns (local) or kubedns (Kubernetes) and passed to the corresponding Dapr sidecar of the recipient via gRPC. The recipient is specified by its name and can be used with Kubernetes service and its load balancing mechanisms. The communication between the sidecars is secured by mTLS and consequently encrypted. At this point, it should be mentioned that traffic from containerized services such as nginx or traefik can also be transferred to the pods via Dapr. This means that traffic can be transmitted encrypted from the cluster ingress to the actual service without using service meshes or overlay networks. This topic, in particular, can be found in many security requirements lists.

The receiver sidecar forwards the call according to the methods registered by the receiver app. In case of errors within the communication or at the receiver, Dapr will forward this request protocol-agnostically back to the sender. If no suitable receiver can be determined by Dapr, this information is also returned to the sender application and can be handled accordingly. Dapr tries to reach the recipient several times to rule out unavailability introduced by e.g. a temporarily unavailable pod. This procedure, therefore, does not need to be handled in the application.

Due to the abstraction of the communication layer, Dapr also allows communication across multiple protocols (HTTP/gRPC). Thus, it is possible to migrate the existing application to gRPC step by step.

Tracing and Observation

By passing the communication to Dapr, a central, application-independent way of monitoring the calls is created. Dapr uses the standardized „W3C Trace Context“ headers (http: traceparent, gRPC: grpc-trace-bin) and supplements this information as needed. The information is collected in Zipkin format and can be forwarded to external monitoring systems directly or via the OpenTelemetry Collector. The resulting independence from specific implementations allows teams of any size with different programming languages to have a unified collection of logs and metrics.

Publish and Subscribe

The Publish and Subscribe pattern is an alternative to the direct invocation of a service method. Above all, the advantage of (temporal) decoupling makes it possible to efficiently implement resilient and asynchronous applications. Often, external messaging services are used that are either provided themselves (RabbitMQ, Kafka, NATS, Redis…) or are available as PaaS components (Google Cloud Platform Pub/Sub, Azure Eventhub/ServiceBus, AWS SNS/SWS). The multitude of possibilities and formats led to Cloud Events establishing itself as an overarching format standard in recent years. However, the protocols and interfaces themselves have remained unaffected and vary between each service.

Especially for the operation of several applications developed in different departments, this diversity can be a great challenge since the operation and testing knowledge must be distinct depending on the component.

dapr also tries to offer a simplification for developers and testers and operations at this point through the abstraction approach.

As with the direct invocation of a service method, publishing or subscribing is also done via the Dapr sidecar. The application itself communicates with the sidecar via generic HTTP/gRPC calls and is used independently of the message broker. The resulting flexibility allows developers to use different brokers depending on the environment (local, on-premises, cloud) and to avoid complex mocks for testing. On the other hand, it gives operations the chance to reduce the number of different technologies without restricting development. This also provides Dapr more independence in terms of vendor lock-in.

For message transmission, Dapr guarantees several features without any implementation effort:

  • Guaranteed delivery of a message (Quality-of-Service (QoS) Level 1: At-least-once)
  • Distribution of messages to multiple subscribers (Competing Consumers Pattern)
  • Restricting access (Topic Scoping)
  • Restricting message lifetime (Message TTL)
  • Error handling (Retry/Drop)

The connection of the respective Message- or Event Broker is made as a Dapr component. The configuration contains component-specific parameters. The configuration of an Azure EventHub as a Publish & Subscribe component is used in the following example:

Pub/Sub Azure Event Hub configuration

The specified connection data of an Azure storage account allows Dapr to store checkpoints that enable the „Competing Consumers Pattern“.

Within the application, a message can be published by calling localhost:{dapr-port}/v1.0/publish/{component-name}/{topic-name}.

Subscriptions can be made either from within the application (programmatically) or via a central configuration (declaratively). Incoming messages are passed to the application by Dapr via the specified route. The following figure shows the connection of a Redis schematically.

This functionality is also prepared language-specifically in the available SDKs. At this point in particular, decoupling is helpful for simplifying tests and testing the interfaces to them independently of the selected PubSub component. This allows for cross-application test components to be created that offer generic message broker mocks based on Dapr. Thus, when using Dapr, the standardization of the interfaces can also reduce the effort required for testing.

Resource Bindings and Triggers

The resource bindings and triggers allow to connect arbitrary services to Dapr and to implement event-driven architectures. This means that no dependencies on service-specific SDKs or libraries are created. Practically, there are no limits to the services that can be connected via them. The bindings currently available can be found in Dapr’s GitHub repository and include various cloud provider-managed services, databases, and platforms such as Twitter or Twillo. These can be supplemented with your own bindings or existing bindings can be built upon.

For Dapr to respond to a service and address an application when the published interface is called, an input binding must be created first. For this purpose, a new resource is created in Kubernetes. This will be configured with a corresponding binding type and other parameters (left figure). The application then is also able to listen locally for the service and react when an event occurs (right figure).

The procedure for creating an output binding is essentially the same. The corresponding component must be configured and can then be called within the application. This can be done with a simple curl command. Output bindings are triggered by sending a HTTP POST request to the endpoint /v1.0/bindings/<binding-name> on the Dapr sidecar. Of course, this call is also possible via gRPC.

As you have probably already noticed, the binding and addressing of external components is done in the same way in all building blocks. Also, the handling is always done with the same scheme.

Light and shadow of Dapr

What does Dapr do for me?

  • The direct addressing of communication partners
  • Securing Sercie-to-Service-Invocation (S2SI) communication
  • The decoupling of PubSub services
  • Monitoring of traffic
  • … as well as the convenience of the not further specified Building Blocks

Dapr offers an easy-to-handle toolbox, especially for application developers. A not to be neglected advantage is the standardization in the integration tools. Especially when using multiple building blocks, complexity is quickly reduced, and knowledge is bundled. The multitude of options developers have for integration is minimized by Dapr. This can also be an advantage for operations teams, as reduced variance also reduces effort.

The bottom line is that utilizing Dapr makes it easier for development teams to get started with distributed application development and take over essential tasks without much additional effort.

What Dapr doesn’t solve?

  • Messaging only works with publish & subscribe but not with queues.
  • E.g. when using Kafka in Pub/Sub no Kafka Streams can be used, so scenarios like Machine Learning are not possible.
  • If I want to have a new component, I must develop it myself or suggest it. However, many companies are not yet ready to enter the field of open-source development.

There is also the question of who runs Dapr and all connected components. Through the strong simplification, specific dependencies are dissolved. Nevertheless, a tuning between enterprise and development must occur. For operation, this initially means more complexity.

Alternatives

An alternative to Dapr is the Go Micro Framework, which provides a similar scope and approach to integrations. However, as the name suggests, it is only available for the Go programming language. Attempts to compare Dapr with tools such as Istio, Knative or Keptn are in vain as these have other application areas and can be combined much more with Dapr. The closest comparison would be with SDKs for public cloud providers. Here, the focus is more on the use and provisioning of resources.

A look into the crystal ball

In the last few months, Dapr has experienced strong growth, has already gained nearly 15.000 stars on GitHub and is backed by Microsoft. The latter see Dapr as a strategically important topic and perfectly fits the overall direction to support developers. There are also some rumors in the community about Dapr soon becoming a native feature on Microsoft’s Azure Kubernetes Service (AKS, the managed Kubernetes service). In addition, using Dapr as an integration layer between various PaaS components is, of course, an interesting consideration – i.e., between Kubernetes, Serverless Functions (e.g. Azure Functions) or other hosting options.

We are looking forward to the further development of Dapr. For many development projects, applying Dapr can take the complexity out of building microservices and solve stringent dependencies between technologies. Of course, it is not (yet) applicable to every scenario, but it addresses many of the common problems and provides a web-scale-ready answer.

Originally authored by Florian Eidner from Cluster Reply & Max Körbächer from Liquid Reply