Sultan Dzhumaev, 10 August 2024

Dapr Introduction and Service-to-Service Invocation Part I

When working with distributed systems, there are many factors to consider, including the environment you are operating in, ensuring the relevant configurations are up and running, and maintaining communication between the services and components. One common pitfall in developing a distributed system is not having a smart way to manage all the key-value pair configurations used for service-to-service communication, database connections, and other integrations. This challenge is more severe when you need to differentiate between various environment configurations and maintain each one of them.

Another difficulty in working with such systems arises when using different vendors' SDKs to integrate with their resources. This approach often results in a strong coupling to that specific infrastructure within your code, which might not be desirable, particularly when moving between local development environments and staging environments in the cloud.

This is where Dapr comes into play, addressing these problems and more.

Dapr is a runtime application designed to be a handy tool for distributed systems, helping achieve quality attributes like resilience, maintainability, and scalability. It accomplishes this by offering "building blocks," each a coherent set of APIs tailored to specific feature.

Dapr is fully supported with the latest .NET and C# with SDK library, and even more the Dapr itself tries to support variety of different programming languages. Dapr is an open source project written in GO language.

Sidecar Pattern

A significant feature of the Dapr runtime is its implementation of the sidecar pattern. In this pattern, every microservice has its own dedicated sidecar application running alongside it. The core principle here is that your main service contains the essential business logic and features, while cross-cutting concerns are offloaded to its sidecar.

In the context of Dapr, these cross-cutting concerns are particularly interesting because by offloading responsibilities to Dapr for communicating with different outbound sources, your microservice is relieved from integrating directly with vendor SDKs. Instead, it interacts with a consistent set of Dapr APIs, independent of the underlying infrastructure.

Each Dapr sidecar exposes APIs for metadata, health checks, and building blocks. The metadata and health APIs are used for load balancing and discovering the appropriate destinations for communication.

Cluster with services and sidecars
Amount of service instances will always match with amount of spawned sidecars in a Kubernetes based cluster

Dapr Components

Dapr components are pluggable units that provide integration with various external systems and resources. These components enable the system to interact with different resources such as state stores, message brokers, bindings, and observability tools. By leveraging Dapr components, developers can switch between different infrastructure providers without changing application code, enhancing the portability and flexibility of the distributed system.

different components depending on your environment
The ability to swap between different components depending on your environment for the same building block

Dapr components are declared in YAML configuration files. These declarations specify how Dapr should interact with external resources and define the metadata and configuration details necessary for integration. Components can be used by Dapr's building blocks to perform various operations like state management, publish/subscribe messaging, and resource binding.

Example Component Declaration Below is an example of a YAML file that configures a Redis state store component for use by the Dapr state management building block:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
  namespace: default
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: "localhost:6379"
  - name: redisPassword
    value: ""

In this example, the component is given a name (statestore) and specifies the type (state.redis) and version (v1). The metadata section contains key-value pairs needed to configure the Redis connection.

Dapr Building Blocks

Dapr building blocks are modular APIs that provide essential functionality for building distributed applications. They abstract away the complexities of working with various infrastructure components, allowing developers to focus on business logic. Dapr building blocks utilize Dapr Component declarations to provide various capabilities to your applications. A service depends on building block API and the building block depends on Component declarations that determines it behavior. Summary of building blocks shown below:

Dapr Building Blocks
Dapr Building Blocks Source link: https://docs.dapr.io/images/building_blocks.png

Azure Support

In Azure, there is a Kubernetes-based serverless hosting platform called Container Apps, which natively integrates with Dapr. By enabling this feature, the cloud provider manages and provisions the necessary resources for Dapr to operate and allocates the sidecar containers. Both from the Azure portal GUI and with Infrastructure as Code (IaC) tools such as Bicep, you can configure all of the required Dapr settings within this resource platform.

Container Apps is a good candidate for deploying your microservices and provides layers of abstraction that make it easier to provision your containers, as the underlying Kubernetes infrastructure is maintained by the cloud provider.

One limitation is that any Dapr building block that is either in the Alpha or Beta stage will not be supported in this hosting platform until it reaches a stable stage version.

>Dapr Container apps deployed in runtime with Dapr
Dapr Container apps deployed in runtime with Dapr Source link: https://learn.microsoft.com/en-gb/azure/container-apps/media/microservices-dapr/azure-container-apps-microservices-dapr.png

Service-to-Service Invocation

The service-to-service invocation building block is a fundamental part of Dapr, enabling microservices to communicate with each other efficiently and reliably. This building block abstracts the complexities of service discovery and routing, allowing developers to focus on their business logic without worrying about the network communication or base URL of the service to be invoked.

  • Service Discovery: Dapr automatically handles service discovery, allowing services to locate and communicate with each other without hard-coded addresses.

  • Load Balancing: The Dapr runtime includes load balancing mechanisms, ensuring requests are distributed evenly across service instances.

  • Protocol Agnostic: Supports both HTTP and gRPC, allowing developers to choose the communication protocol that best suits their needs.

  • Security: Dapr supports secure communication between services using mutual TLS, ensuring data privacy and integrity.

By using Dapr's service-to-service invocation building block, developers can bridge http/grpc communication without the overhead of managing it.

Example Scenario: ServiceA invocating ServiceB for fetching WeatherForecast

We start the dapr sidecars in self-hosted mode, and relative to where the csprojects are located in the file system, we create two ps.1 scripts which will tell the dapr CLI to start both the dotnet application, sidecar and bind it to services. Pay attention to --resources-path parameter, which tells where the dapr components are stored.

dapr run --app-id serviceA --app-port 5203 --resources-path ..\dapr.components\ -- dotnet run

dapr run --app-id serviceB --app-port 5204 --resources-path ..\dapr.components\ -- dotnet run

You need to install nuget package Dapr.AspNetCore.

This library will provide an extension method to IServiceCollection, you will need to configure Dapr Dependency Injection as following:

builder.Services.AddDaprClient();

The code below for ServiceA showcases multiple ways to archieve the same service invocation capability.

using Dapr.Client;
using Microsoft.AspNetCore.Mvc;

namespace dapr.serviceA;

[Route("/")]
public class ServiceAController(
    DaprClient client) : ControllerBase
{

    [HttpGet("/forecast1")]
    public async Task<IActionResult> GetForecast()
    {
        // Several overloads of InvokeMethodAsync, and ability to strongly bind contracts.
        //Response WITHOUT request payload
        await client.InvokeMethodAsync<WeatherForecast[]>(httpMethod: HttpMethod.Get, appId: "serviceB", methodName: "forecast");

        //Response with request payload
        await client.InvokeMethodAsync<int, WeatherForecast[]>(httpMethod: HttpMethod.Post, appId: "serviceB", data: 100, methodName: "forecast");

        //Either response contract will be returned in case of valid HTTP status, or an exception will be raised in above methods
        //Alternative is to use InvokeMethodWithResponseAsync, granular control in case of failure
        HttpRequestMessage request = client.CreateInvokeMethodRequest(appId: "serviceB", methodName: "forecast2");

        HttpResponseMessage response = await client.InvokeMethodWithResponseAsync(request);
    }
}

The dapr sidecar will discover the appropriate endpoint to call in ServiceB as long as the relative route path of the endpoint matches with the methodName parameter passed above.

using Dapr.Client;
using Microsoft.AspNetCore.Mvc;

namespace dapr.serviceB;

[Route("/")]
public class ServiceBController(
    DaprClient client) : ControllerBase
{
    private string[] summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild"
        };

    [HttpGet("/forecast2")]
    public async Task<IActionResult> GetForecast()
    {
        return Ok(Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray());
    }
}

Please refer to the official Dapr documentation on how to start using it with different hosting models:

Documentation

Conclusion

Dapr is a powerful tool that makes it easier to develop and distribute microservices due to its powerful building blocks. We only demonstrated the service invocation capability of Dapr in this post. In future posts, we will explore other capabilities. A real constraint of Dapr is that not all of its budling block APIs are in a stable version. Depending on your needs, infrastructure, and platform you are hosting your system on, this will dictate which features are production-ready and which are not. Reading the documentation and tracking GitHub issues may save you the trouble of reproducing the same error in your execution environment.

Comments