This is the fourth part of Building Microservice Applications with Azure Container Apps and Dapr. The topics we’ll cover are:
- Tutorial for building Microservice Applications with Azure Container Apps and Dapr – Part 1
- Deploy backend API Microservice to Azure Container Apps – Part 2
- Communication between Microservices in Azure Container Apps – Part 3
- Dapr Integration with Azure Container Apps – (This Post)
- Azure Container Apps State Store With Dapr State Management API – Part 5
- Azure Container Apps Async Communication with Dapr Pub/Sub API – Part 6
- Azure Container Apps with Dapr Bindings Building Block – Part 7
- Azure Container Apps Monitoring and Observability with Application Insights – Part 8
- Continuous Deployment for Azure Container Apps using GitHub Actions – Part 9
- Use Bicep to Deploy Dapr Microservices Apps to Azure Container Apps – Part 10
- Azure Container Apps Auto Scaling with KEDA – Part 11
- Azure Container Apps Volume Mounts using Azure Files – Part 12
- Integrate Health probes in Azure Container Apps – Part 13
Dapr Integration with Azure Container Apps
In this post, we will start integrating Dapr into both services and see how Dapr with Azure Container Apps will simplify complex microservices scenarios such as service discovery, service-to-service invocation, calling services asynchronously via pub/sub patterns, auto-scaling for overloaded services, etc..
The source code for this tutorial is available on GitHub. You can check the demo application too.
Benefits of integrating Dapr in Azure Container Apps
Our Tasks Tracker microservice application is composed of multiple microservices (2 microservices so far), and function calls are spread across the network. To support the distributed nature of microservices, we need to account for failures, retries, and timeouts. While Container Apps features the building blocks for running microservices, the use of Dapr provides an even richer microservices programming model. Dapr includes features like service discovery, pub/sub, service-to-service invocation with mutual TLS, retries, state store management, and more.
Enable Dapr on a local development machine
Moving forward I will be using VS Code for local development and debugging, the extensions provided in VS code to work with Dapr, and the local development experience is better than Visual Studio 2022.
Step 1: Install the Dapr CLI
Run PowerShell console as an administrator and run the below command:
1 |
powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex" |
This command will install the Dapr CLI as the main tool for running Dapr-related tasks.
Note: You might need to execute the below PowerShell command before installing the Dapr CLI, this command is to allow local PowerShell scripts to run regardless of signature, and requires trusted digital signatures only for remote scripts.
1 |
Set-ExecutionPolicy RemoteSigned -scope CurrentUser |
After you run the command, verify the CLI is installed by restarting your PowerShell console and running the following command:
1 |
dapr |
If you want to install Dap CLI on a different OS other than windows, you can use the following link.
Step 2: Initialize Dapr in a local development environment
Now we have installed the Dapr CLI, we will use the Dapr CLI to initialize Dapr on our local machine.
Dapr runs as a sidecar alongside our application. In self-hosted mode, this means it is a process on our local machine. By initializing Dapr, we will fetch and install the Dapr sidecar binaries locally, and we will create a development environment that streamlines application development with Dapr.
Once Dapr initialization completes it will include the below on our local machine:
- Running a Redis container instance to be used as a local state store and message broker.
- Running a Zipkin container instance for observability.
- Creating a default components folder with component definitions for the above.
- Running a Dapr placement service container instance for local actor support.
To do so open the PowerShell console as an administrator and run the below command
1 |
dapr init |
To verify the deployment check Dapr version by running the below command:
1 |
dapr --version |
Now we need to verify that Dapr global components directory has been initialized, so open this path by running explorer "%USERPROFILE%\.dapr\"
And you should see the below components folder which contains several YAML files with definitions for a state store, Pub/sub, and Zipkin. The Dapr sidecar will read these components and use:
- The Redis container for state management and async messaging.
- The Zipkin container for collecting traces.
Run Backend API and Frontend WebApp locally using Dapr
Now open VS Code and open the root folder named “TasksTracker.ContainerApps” in VS code which contains the Web API Project folder “TasksTracker.TasksManager.Backend.Api” and Web App project folder “TasksTracker.WebPortal.Frontend.Ui”
Your VS code explorer should look like the below
Step 1: Install VS Code Dapr extension
This extension will help us to run, debug, and interact with Dapr-enabled applications in VS Code. Go to the extension tab in VS Code and search for “Dapr” and install the extension.
Step 2: Running Backend Web API using Dapr
The first thing to try out here is to run the Backend Web API using Dapr, from VS Code open a new PowerShell terminal, change the directory in the terminal to folder “TasksTracker.TasksManager.Backend.Api” and run the below command in PS terminal:
1 |
dapr run --app-id tasksmanager-backend-api --app-port 7088 --dapr-http-port 3500 --app-ssl dotnet run |
When using Dapr run command we are running a dapr process as a sidecar next to the Web API application, the properties we have configured as the following:
- app-id: The unique identifier of the application. Used for service discovery, state encapsulation, and the pub/sub consumer identifier.
- app-port: This parameter tells Dapr which port your application is listening on, you can get the app port from “launchSettings.json” file in the Web API Project.
- dapr-http-port: The HTTP port for Dapr to listen on.
- app-ssl: Sets the URI scheme of the app to https and attempts an SSL connection.
For a full list of properties, you can check this link
If all is working as expected, you should receive an output similar to the below where your app logs and dapr logs will appear on the same PowerShell terminal:
Now to test invoking the Web API using Dapr sidecar, you can issue HTTP GET request to the following URL: http://localhost:3500/v1.0/invoke/tasksmanager-backend-api/method/api/tasks?createdBy=tjoudeh@bitoftech.net
What happened here is that Dapr exposes its HTTP and gRPC APIs as a sidecar process, as a process that can access our Backend Web API, we didn’t do any changes to the application code to include any Dapr runtime code as well providing separation of the application logic for improved supportability.
I’m borrowing the below picture from Dapr website to illustrate how this call works
Looking back at the HTTP GET request http://localhost:3500/v1.0/invoke/tasksmanager-backend-api/method/api/tasks?createdBy=tjoudeh@bitoftech.net we can break it into the following:
- /v1.0/invoke Endpoint: is the Dapr feature identifier for the “Service to Service invocation” building block. This building block enables applications to communicate with each other through well-known endpoints in the form of http or gRPC messages. Dapr provides an endpoint that acts as a combination of a reverse proxy with built-in service discovery while leveraging built-in distributed tracing and error handling.
- 3500: the HTTP port that Dapr is listening on.
- tasksmanager-backend-api: is the dapr application unique identifier.
- method: reserved word when using invoke endpoint.
- api/tasks?createdBy=tjoudeh@bitoftech.net: the path of the action method that needs to be invoked in the remote service.
Another example is that we want to create a new task by invoking the POST operation, we need to issue the below POST request:
1 2 3 4 5 6 7 8 9 10 11 |
POST /v1.0/invoke/tasksmanager-backend-api/method/api/tasks/ HTTP/1.1 Host: localhost:3500 Content-Type: application/json Content-Length: 203 { "taskName": "Task number: 51", "taskCreatedBy": "tjoudeh@mail.com", "taskDueDate": "2022-08-31T09:33:35.9256429Z", "taskAssignedTo": "taiseer@mail.com" } |
Step 3: Use Dapr SDK in Frontend Web App to invoke Backend API services
The Dapr .NET SDK provides .NET developers with an intuitive and language-specific way to interact with Dapr. The SDK offers developers three ways of making remote service invocation calls:
- Invoke HTTP services using HttpClient
- Invoke HTTP services using DaprClient
- Invoke gRPC services using DaprClient
In the implementation shared in GitHub, I’m using the second approach (HTTP services using DaprClient), but it is worth describing the first approach (Invoke HTTP services using HttpClient) in the post here. So I will go over the first approach briefly and then discuss the second one too.
Now we will install Dark SDK for .NET Core in the Frontend Web APP so use the service discovery and service invocation offered by Dapr Sidecar, to do so, open the .csproj file of the project “TasksTracker.WebPortal.Frontend.Ui.csproj” and add the below NuGet package
1 2 3 |
<ItemGroup> <PackageReference Include="Dapr.AspNetCore" Version="1.8.0" /> </ItemGroup> |
Next, open the file “Programs.cs” of the Frontend Web App and register the DaprClient as the code below, The DaprClient object is intended to be long-lived. A single DaprClient instance can be reused for the lifetime of the application
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TasksTracker.WebPortal.Frontend.Ui { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); // Code removed for brevity builder.Services.AddSingleton<DaprClient>(_ => new DaprClientBuilder().Build()); var app = builder.Build(); // Code removed for brevity } } } |
Now, we will inject the DaprClient into the Index.cshtml page to use the method “InvokeMethodAsync” (second approach), to do so, open the page named “Index.cshtml.cs” under folder “Pages\Tasks” and use the code below
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks { public class IndexModel : PageModel { private readonly IHttpClientFactory _httpClientFactory; private readonly DaprClient _daprClient; public List<TaskModel>? TasksList { get; set; } [BindProperty] public string? TasksCreatedBy { get; set; } public IndexModel(IHttpClientFactory httpClientFactory, DaprClient daprClient) { _httpClientFactory = httpClientFactory; _daprClient = daprClient; } public async Task OnGetAsync() { TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"]; //Invoke via internal URL (Not Dapr) //var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); //TasksList = await httpClient.GetFromJsonAsync<List<TaskModel>>($"api/tasks?createdBy={TasksCreatedBy}"); // Invoke via Dapr SideCar URL //var port = 3500;//Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); //HttpClient client = new HttpClient(); //var result = await client.GetFromJsonAsync<List<TaskModel>>($"http://localhost:{port}/v1.0/invoke/tasksmanager-backend-api/method/api/tasks?createdBy={TasksCreatedBy}"); //TasksList = result; // Invoke via DaprSDK (Invoke HTTP services using HttpClient) --> Use Dapr Desitination App ID (Option 1) //var daprHttpClient = DaprClient.CreateInvokeHttpClient(appId: "tasksmanager-backend-api"); //TasksList = await daprHttpClient.GetFromJsonAsync<List<TaskModel>>($"api/tasks?createdBy={TasksCreatedBy}"); // Invoke via DaprSDK (Invoke HTTP services using HttpClient) --> Specify Port (Option 2) //var daprHttpClient = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); //TasksList = await daprHttpClient.GetFromJsonAsync<List<TaskModel>>($"http://tasksmanager-backend-api/api/tasks?createdBy={TasksCreatedBy}"); // Invoke via DaprSDK (Invoke HTTP services using DaprClient) TasksList = await _daprClient.InvokeMethodAsync<List<TaskModel>>(HttpMethod.Get, "tasksmanager-backend-api", $"api/tasks?createdBy={TasksCreatedBy}"); } public async Task<IActionResult> OnPostDeleteAsync(Guid id) { // direct svc to svc http request // var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); // var result = await httpClient.DeleteAsync($"api/tasks/{id}"); //Dapr SideCar Invocation await _daprClient.InvokeMethodAsync(HttpMethod.Delete, "tasksmanager-backend-api", $"api/tasks/{id}"); return RedirectToPage(); } public async Task<IActionResult> OnPostCompleteAsync(Guid id) { // direct svc to svc http request // var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); // var result = await httpClient.PutAsync($"api/tasks/{id}/markcomplete", null); //Dapr SideCar Invocation await _daprClient.InvokeMethodAsync(HttpMethod.Put, "tasksmanager-backend-api", $"api/tasks/{id}/markcomplete"); return RedirectToPage(); } } } |
Notice how we are not using the HttpClientFactory anymore and how we were able from the Frontend Dapr Sidesar to invoke backend API Sidesar using the method “InvokeMethodAsync” which accepts the Dapr remote App ID for the Backend API( “tasksmanager-backend-api”) and it will be able to discover the URL and invoke the method based on the specified input params.
In addition to this, notice how in POST and PUT operations, the third argument is a “TaskAdd” or “TaskUpdate” Model, those objects will be serialized internally (using System.Text.JsonSerializer) and sent as the request payload. The .NET SDK takes care of the call to the Sidecar. It also deserializes the response in case of the GET operations to a “List<TaskModel>” object.
Note: All calls between Dapr sidecars go over gRPC for performance. Only calls between services and Dapr sidecars can be either HTTP or gRPC. I highly recommend looking into the Service invocation diagram here to understand fully how the name resolution of services happening and the flow of calls.
Looking at the first option of invoking the remote service “Invoke HTTP services using HttpClient”, you can see that we can create an HttpClient by invoking “DaprClient.CreateInvokeHttpClient” and specify the remote service app id, custom port if needed and then use the HTTP methods such as “GetFromJsonAsync”, this is a good approach as well at it gives you full support of advanced scenarios, such as custom headers, and full control over request and response messages.
In both options, the final request will be rewritten by the Dapr .NET SDK before it gets executed, in our case and for the GET operation it will be written to this request: http://127.0.0.1:3500/v1/invoke/tasksmanager-backend-api/method/api/tasks?createdBy=tjoudeh@gmail.com
We need now to update the Create.cshtml.cs and Edit.cshtml.cs by injecting the DaprClient. I will not copy the code here, but you can take a look at how the files will look after the update using the previous links.
Step 4: Verify changes on Frontend Web App
Now we need to run the Frintend Web App along with the Backend Web API and test locally that changes using the .NET SDK and invoking services via Dapr Sidecar are working as expected, to do so run the below 2 commands (Ensure that you are on the right project directory when running each command):
1 2 3 |
~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui> dapr run --app-id tasksmanager-frontend-webapp --app-port 7208 --dapr-http-port 3501 --app-ssl dotnet run ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api> dapr run --app-id tasksmanager-backend-api --app-port 7088 --dapr-http-port 3500 --app-ssl dotnet run |
Notice how we assigned the Dapr App Id “tasksmanager-frontend-webapp” to the Frontend WebApp.
Now both Applications are running using Dapr sidecar, open your browser and browser for “https://localhost:{localwebappport}”, in my case it will be “https://localhost:7208” and provide an email to load the tasks for the user, if all is good you should see tasks list results.
Step 5: Debug and launch Dapr applications in VSCode
Now if we need to run both microservices together, we need to keep calling dapr run manually each time in the terminal, and when we have multiple microservices talking to each other and need to run at the same time to debug the solution, this will be a very annoying process.
We need to update VS code “tasks.json” and “launch.json” configuration to make this automated and you only need to click on Run and Debug button in VS Code to launch all services be able to debug them locally.
Firstly we need to add a new launch configuration for the Backend Web API and Frontend Web App projects, to do so, open file launch.json and add the below 2 configurations. For the complete launch.json file, you can check the file here.
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 41 42 43 44 45 46 |
{ "configurations": [ { "name": "Launch (backend api) with Dapr", "type": "coreclr", "request": "launch", "preLaunchTask": "backend-api-dapr-debug", "program": "${workspaceFolder}/TasksTracker.TasksManager.Backend.Api/bin/Debug/net6.0/TasksTracker.TasksManager.Backend.Api.dll", "args": [], "cwd": "${workspaceFolder}/TasksTracker.TasksManager.Backend.Api", "stopAtEntry": false, "serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" }, "postDebugTask": "daprd-down-backend-api" }, { "name": "Launch (web app) with Dapr", "type": "coreclr", "request": "launch", "preLaunchTask": "webapp-ui-dapr-debug", "program": "${workspaceFolder}/TasksTracker.WebPortal.Frontend.Ui/bin/Debug/net6.0/TasksTracker.WebPortal.Frontend.Ui.dll", "args": [], "cwd": "${workspaceFolder}/TasksTracker.WebPortal.Frontend.Ui", "stopAtEntry": false, "serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" }, "postDebugTask": "webapp-ui-daprd-down" } ] } |
Notice that we have a “preLaunchTask” and a “postDebugTask” which we need to define right now, those tasks are Dapr tasks.
The Dapr VSCode extension we have previously installed helps us to define those pre and post debug tasks, to do so, open the file tasks.json and click ctrl + shift + p , and type Dapr: Scaffold Dapr Tasks the Dapr VS Code extension will allow us to manage Dapr application and test it out in an easier way, the below image shows a full list of helper commands.
Now we will add 4 tasks, for each application, there will be a task to support the “preLaunch” activity and the “postDebug” activity (Terminate/Exit Dapr Sidecar process), so open file tasks.json and add the tasks below:
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 |
{ "tasks": [ { "appId": "tasksmanager-backend-api", "appPort": 7088, "httpPort": 3500, "grpcPort": 50001, "appSsl": true, "label": "backend-api-dapr-debug", "type": "dapr", "dependsOn": "build-backend-api", "componentsPath": "./components" }, { "appId": "tasksmanager-backend-api", "label": "daprd-down-backend-api", "type": "daprd-down" }, { "appId": "tasksmanager-frontend-webapp", "appPort": 7208, "httpPort": 3501, "grpcPort": 50002, "appSsl": true, "label": "webapp-ui-dapr-debug", "type": "dapr", "dependsOn": "build-webapp-ui" }, { "appId": "tasksmanager-frontend-webapp", "label": "webapp-ui-daprd-down", "type": "daprd-down" } ] } |
What we have added right now is the following:
- The tasks with the label “backend-api-dapr-debug” will invoke the “daprd” task, this task is similar to calling dapr run from CLI
- We are setting the appPort, httpPort, and grpcPort properties (grpcPort is needed in future posts when we staring using the state manager building block. If you didn’t set it, you might face a similar issue)
- We are setting the “componentsPath” property, this is needed when start working with the state manager, pub/sub, etc.. you can remove it for now.
- We are setting the dependsOn property, so this means this task will fire after the dependsOn tasks complete successfully, we need to add those dependsOn tasks.
- The tasks with the label “daprd-down-backend-api” will terminate the Dapr Sidecar process, this will be used for the postDebug activity in configuration.json.
- For a complete list of available properties please check this link
So let’s add the dependsOn tasks, so open tasks.json and add the tasks below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "label": "build-backend-api", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/TasksTracker.TasksManager.Backend.Api/TasksTracker.TasksManager.Backend.Api.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "build-webapp-ui", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/TasksTracker.WebPortal.Frontend.Ui/TasksTracker.WebPortal.Frontend.Ui.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" } |
For a complete reference of tasks.json file, you can check this link
Lastly, we need to add a “compound launch” property so we launch and debug both applications together, to do so, open the file launch.json again and add the below array after the “configuration” array.
1 2 3 4 5 6 7 8 9 10 |
"compounds": [ { "name": "RunAll with Dapr", "configurations": [ "Launch (backend api) with Dapr", "Launch (web app) with Dapr" ], "stopAll": true } ] |
If all is done correctly, you should be able to see a debug configuration named “RunAll with Dapr” and you should be able to just hit F5, sit breakpoints and debug both applications locally in VS Code
In the next post, we will integrate the Dapr state store building block by saving tasks to Redis locally, and then to Azure Cosmos DB, and deploy the updated applications to Azure Container Apps.
Leave a Reply