diff --git a/TasksTracker.TasksManager.Backend.Api/Services/TasksStoreManager.cs b/TasksTracker.TasksManager.Backend.Api/Services/TasksStoreManager.cs index 21e3938f..5fa59107 100644 --- a/TasksTracker.TasksManager.Backend.Api/Services/TasksStoreManager.cs +++ b/TasksTracker.TasksManager.Backend.Api/Services/TasksStoreManager.cs @@ -103,7 +103,7 @@ public async Task UpdateTask(Guid taskId, string taskName, string assigned private async Task PublishTaskSavedEvent(TaskModel taskModel) { - _logger.LogInformation("Publish Task Saved event for task with Id: '{0}' and Name: '{1}' for Assigne: '{2}'", + _logger.LogInformation("Publish Task Saved event for task with Id: '{0}' and Name: '{1}' for Assignee: '{2}'", taskModel.TaskId, taskModel.TaskName, taskModel.TaskAssignedTo); await _daprClient.PublishEventAsync("dapr-pubsub-servicebus", "tasksavedtopic", taskModel); } diff --git a/docs/aca/01-deploy-api-to-aca/Program.cs b/docs/aca/01-deploy-api-to-aca/Program.cs index 8a447bbd..c007ab18 100644 --- a/docs/aca/01-deploy-api-to-aca/Program.cs +++ b/docs/aca/01-deploy-api-to-aca/Program.cs @@ -3,6 +3,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. + builder.Services.AddSingleton(); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/docs/aca/01-deploy-api-to-aca/index.md b/docs/aca/01-deploy-api-to-aca/index.md index 0c978997..9deb8d16 100644 --- a/docs/aca/01-deploy-api-to-aca/index.md +++ b/docs/aca/01-deploy-api-to-aca/index.md @@ -68,7 +68,7 @@ In this module, we will accomplish three objectives: - Now we need to register `FakeTasksManager` on project startup. Open file `#!csharp Program.cs` and register the newly created service by adding the highlighted lines from below snippet. Don't forget to include the required `using` statement for the task interface and class. === "Program.cs" - ```csharp hl_lines="1 6" + ```csharp hl_lines="1 7" --8<-- "docs/aca/01-deploy-api-to-aca/Program.cs" ``` diff --git a/docs/aca/03-aca-dapr-integration/index.md b/docs/aca/03-aca-dapr-integration/index.md index 147e76f4..47f50cec 100644 --- a/docs/aca/03-aca-dapr-integration/index.md +++ b/docs/aca/03-aca-dapr-integration/index.md @@ -68,33 +68,11 @@ You are now ready to run the applications locally using the Dapr sidecar in a se === ".NET 6 or below" - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - ``` - - ```shell - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:basic-dotnet6" === ".NET 7 or above" - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - ``` - - ```shell - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-backend-api.md:basic" ![app-port](../../assets/images/03-aca-dapr-integration/self_hosted_dapr_app-port.png) @@ -247,50 +225,15 @@ We are ready now to verify the changes on the Frontend Web App and test locally. === ".NET 6 or below" - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui - - dapr run ` - --app-id tasksmanager-frontend-webapp ` - --app-port $UI_APP_PORT ` - --dapr-http-port 3501 ` - --app-ssl ` - -- dotnet run - ``` - - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run - ``` + --8<-- "snippets/dapr-run-frontend-webapp.md:basic-dotnet6" + + --8<-- "snippets/dapr-run-backend-api.md:basic-dotnet6" === ".NET 7 or above" - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui - - dapr run ` - --app-id tasksmanager-frontend-webapp ` - --app-port $UI_APP_PORT ` - --dapr-http-port 3501 ` - --app-ssl ` - -- dotnet run --launch-profile https - ``` - - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-frontend-webapp.md:basic" + + --8<-- "snippets/dapr-run-backend-api.md:basic" Notice how we assigned the Dapr App Id “tasksmanager-frontend-webapp” to the Frontend WebApp. diff --git a/docs/aca/04-aca-dapr-stateapi/Program.cs b/docs/aca/04-aca-dapr-stateapi/Program.cs index fac7a877..d1059f6a 100644 --- a/docs/aca/04-aca-dapr-stateapi/Program.cs +++ b/docs/aca/04-aca-dapr-stateapi/Program.cs @@ -3,6 +3,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. + builder.Services.AddDaprClient(); builder.Services.AddSingleton(); //builder.Services.AddSingleton(); diff --git a/docs/aca/04-aca-dapr-stateapi/containerapps-statestore-cosmos.yaml b/docs/aca/04-aca-dapr-stateapi/containerapps-statestore-cosmos.yaml index 6abab967..55cba8ac 100644 --- a/docs/aca/04-aca-dapr-stateapi/containerapps-statestore-cosmos.yaml +++ b/docs/aca/04-aca-dapr-stateapi/containerapps-statestore-cosmos.yaml @@ -2,7 +2,7 @@ componentType: state.azure.cosmosdb version: v1 metadata: - name: url - value: .documents.azure.com:443/> + value: .documents.azure.com:443/> - name: database value: tasksmanagerdb - name: collection diff --git a/docs/aca/04-aca-dapr-stateapi/dapr-statestore-cosmos.yaml b/docs/aca/04-aca-dapr-stateapi/dapr-statestore-cosmos.yaml index f39fe068..782050a6 100644 --- a/docs/aca/04-aca-dapr-stateapi/dapr-statestore-cosmos.yaml +++ b/docs/aca/04-aca-dapr-stateapi/dapr-statestore-cosmos.yaml @@ -7,7 +7,7 @@ spec: version: v1 metadata: - name: url - value: .documents.azure.com:443/> + value: https://cosmos-tasks-tracker-state-store-<$RANDOM_STRING>.documents.azure.com:443/ - name: masterKey value: "" - name: database diff --git a/docs/aca/04-aca-dapr-stateapi/index.md b/docs/aca/04-aca-dapr-stateapi/index.md index 6cfa2560..26fb8243 100644 --- a/docs/aca/04-aca-dapr-stateapi/index.md +++ b/docs/aca/04-aca-dapr-stateapi/index.md @@ -34,30 +34,11 @@ To try out the State Management APIs, run the Backend API from VS Code by runnin === ".NET 6 or below" - ```shell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:basic-dotnet6" === ".NET 7 or above" - ```shell - - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-backend-api.md:basic" Now from any rest client, invoke the below **POST** request to the endpoint: [http://localhost:3500/v1.0/state/statestore](http://localhost:3500/v1.0/state/statestore){target=_blank} @@ -179,7 +160,7 @@ Now we need to register the new service named `TasksStoreManager` and `DaprClien === "Program.cs" - ```csharp hl_lines="6-8" + ```csharp hl_lines="7-9" --8<-- "docs/aca/04-aca-dapr-stateapi/Program.cs" ``` @@ -319,26 +300,11 @@ If you have been using the dapr cli commands instead of the aforementioned debug === ".NET 6 or below" - ```shell - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components-dotnet6" + === ".NET 7 or above" - ```shell - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components" !!! note "Deprecation Warning" components-path is being deprecated in favor of --resources-path. At the time of producing this workshop the --resources-path was not supported yet by the VS code extension. Hence, you will notice the use of the property "componentsPath": "./components" in the tasks.json file. Check the extension documentation in case that has changed. diff --git a/docs/aca/05-aca-dapr-pubsubapi/Backend.Svc.csproj b/docs/aca/05-aca-dapr-pubsubapi/Backend.Svc.csproj new file mode 100644 index 00000000..9917185b --- /dev/null +++ b/docs/aca/05-aca-dapr-pubsubapi/Backend.Svc.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + \ No newline at end of file diff --git a/docs/aca/05-aca-dapr-pubsubapi/Program-dotnet6.cs b/docs/aca/05-aca-dapr-pubsubapi/Program-dotnet6.cs new file mode 100644 index 00000000..fcadf6ab --- /dev/null +++ b/docs/aca/05-aca-dapr-pubsubapi/Program-dotnet6.cs @@ -0,0 +1,19 @@ +namespace TasksTracker.Processor.Backend.Svc +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + // Add services to the container. + builder.Services.AddControllers().AddDapr(); + var app = builder.Build(); + app.UseHttpsRedirection(); + app.UseAuthorization(); + app.UseCloudEvents(); + app.MapControllers(); + app.MapSubscribeHandler(); + app.Run(); + } + } +} \ No newline at end of file diff --git a/docs/aca/05-aca-dapr-pubsubapi/Program.cs b/docs/aca/05-aca-dapr-pubsubapi/Program.cs index c49803d1..3af46196 100644 --- a/docs/aca/05-aca-dapr-pubsubapi/Program.cs +++ b/docs/aca/05-aca-dapr-pubsubapi/Program.cs @@ -1,19 +1,29 @@ -namespace TasksTracker.Processor.Backend.Svc - { - public class Program - { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - // Add services to the container. - builder.Services.AddControllers().AddDapr(); - var app = builder.Build(); - app.UseHttpsRedirection(); - app.UseAuthorization(); - app.UseCloudEvents(); - app.MapControllers(); - app.MapSubscribeHandler(); - app.Run(); - } - } - } \ No newline at end of file +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers().AddDapr(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseCloudEvents(); + +app.MapControllers(); + +app.MapSubscribeHandler(); + +app.Run(); \ No newline at end of file diff --git a/docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController.cs b/docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController.cs index 477cb115..1781c404 100644 --- a/docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController.cs +++ b/docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController.cs @@ -20,8 +20,8 @@ public TasksNotifierController(IConfiguration config, ILogger _logger; + + public TasksStoreManager(DaprClient daprClient, IConfiguration config, ILogger logger) + { + _daprClient = daprClient; + _config = config; + _logger = logger; + } + public async Task CreateNewTask(string taskName, string createdBy, string assignedTo, DateTime dueDate) + { + var taskModel = new TaskModel() + { + TaskId = Guid.NewGuid(), + TaskName = taskName, + TaskCreatedBy = createdBy, + TaskCreatedOn = DateTime.UtcNow, + TaskDueDate = dueDate, + TaskAssignedTo = assignedTo, + }; + + _logger.LogInformation("Save a new task with name: '{0}' to state store", taskModel.TaskName); + await _daprClient.SaveStateAsync(STORE_NAME, taskModel.TaskId.ToString(), taskModel); + await PublishTaskSavedEvent(taskModel); + return taskModel.TaskId; + } + + public async Task DeleteTask(Guid taskId) + { + _logger.LogInformation("Delete task with Id: '{0}'", taskId); + await _daprClient.DeleteStateAsync(STORE_NAME, taskId.ToString()); + return true; + } + + public async Task GetTaskById(Guid taskId) + { + _logger.LogInformation("Getting task with Id: '{0}'", taskId); + var taskModel = await _daprClient.GetStateAsync(STORE_NAME, taskId.ToString()); + return taskModel; + } + + public async Task> GetTasksByCreator(string createdBy) + { + var query = "{" + + "\"filter\": {" + + "\"EQ\": { \"taskCreatedBy\": \"" + createdBy + "\" }" + + "}}"; + + var queryResponse = await _daprClient.QueryStateAsync(STORE_NAME, query); + + var tasksList = queryResponse.Results.Select(q => q.Data).OrderByDescending(o=>o.TaskCreatedOn); + return tasksList.ToList(); + } + + public async Task MarkTaskCompleted(Guid taskId) + { + _logger.LogInformation("Mark task with Id: '{0}' as completed", taskId); + var taskModel = await _daprClient.GetStateAsync(STORE_NAME, taskId.ToString()); + if (taskModel != null) + { + taskModel.IsCompleted = true; + await _daprClient.SaveStateAsync(STORE_NAME, taskModel.TaskId.ToString(), taskModel); + return true; + } + return false; + } + + public async Task UpdateTask(Guid taskId, string taskName, string assignedTo, DateTime dueDate) + { + _logger.LogInformation("Update task with Id: '{0}'", taskId); + var taskModel = await _daprClient.GetStateAsync(STORE_NAME, taskId.ToString()); + var currentAssignee = taskModel.TaskAssignedTo; + if (taskModel != null) + { + taskModel.TaskName = taskName; + taskModel.TaskAssignedTo = assignedTo; + taskModel.TaskDueDate = dueDate; + await _daprClient.SaveStateAsync(STORE_NAME, taskModel.TaskId.ToString(), taskModel); + if (!taskModel.TaskAssignedTo.Equals(currentAssignee, StringComparison.OrdinalIgnoreCase)) + { + await PublishTaskSavedEvent(taskModel); + } + return true; + } + return false; + } + + private async Task PublishTaskSavedEvent(TaskModel taskModel) + { + _logger.LogInformation("Publish Task Saved event for task with Id: '{0}' and Name: '{1}' for Assignee: '{2}'", + taskModel.TaskId, taskModel.TaskName, taskModel.TaskAssignedTo); + await _daprClient.PublishEventAsync("dapr-pubsub-servicebus", "tasksavedtopic", taskModel); + } + } +} diff --git a/docs/aca/05-aca-dapr-pubsubapi/containerapps-pubsub-svcbus.yaml b/docs/aca/05-aca-dapr-pubsubapi/containerapps-pubsub-svcbus.yaml index 6b3885fd..c8814749 100644 --- a/docs/aca/05-aca-dapr-pubsubapi/containerapps-pubsub-svcbus.yaml +++ b/docs/aca/05-aca-dapr-pubsubapi/containerapps-pubsub-svcbus.yaml @@ -2,7 +2,7 @@ componentType: pubsub.azure.servicebus version: v1 metadata: - name: namespaceName - value: "sbns-taskstracker-.servicebus.windows.net" + value: "sbns-taskstracker-<$RANDOM_STRING>.servicebus.windows.net" - name: consumerID value: "sbts-tasks-processor" # Application scopes diff --git a/docs/aca/05-aca-dapr-pubsubapi/index.md b/docs/aca/05-aca-dapr-pubsubapi/index.md index 2c825672..0d4b1e02 100644 --- a/docs/aca/05-aca-dapr-pubsubapi/index.md +++ b/docs/aca/05-aca-dapr-pubsubapi/index.md @@ -7,30 +7,40 @@ canonical_url: https://bitoftech.net/2022/09/02/azure-container-apps-async-commu !!! info "Module Duration" 90 minutes -In this module, we will introduce a new background service which is named `ACA-Processor Backend` according to our [architecture diagram](../../assets/images/00-workshop-intro/ACA-Architecture-workshop.jpg). This new service will be responsible for sending notification emails (simulated) to the task owners to notify them that a new task has been assigned to them. We can do this in the Backend API and send the email right after saving the task, but we want to offload this process to another service and keep the Backend API service responsible for managing tasks data only. +## Objective -As a best practice, it is recommended that we decouple the two services from each other. So this means we are going to rely on the Publisher-Subscriber pattern (Pub/Sub Pattern). -The main advantage of this pattern is that it offers loose coupling between services, where the sender/publisher of the message doesn't know anything about the receiver/consumers. -You can even have multiple consumers consuming a copy of the message in a totally different way. You can imagine adding another consumer which is responsible to send push notification for the task owner (e.g. if we had a mobile app channel). +In this module, we will accomplish five objectives: -The publisher/subscriber pattern relies on a message broker which is responsible for receiving the message from the publisher, storing the message to ensure durability, and delivering this message to the interested -consumer(s) to process it. There is no need for the consumers to be available when the message is stored in the message broker. Consumers can process the messages at a later time in an async fashion. -The below diagram gives a high-level overview of how the pub/sub pattern works: +1. Learn how Azure Container Apps uses the Publisher-Subscriber (Pub/Sub) pattern with Dapr. +1. Introduce a new background service, `{{ apps.backendsvc }}` configured for Dapr. +1. Use Azure Service Bus as a Service Broker for Dapr Pub/Sub API. +1. Deploy the Backend Background Processor and the updated Backend API Projects to Azure Container Apps. +1. Configure Managed Identities for the Backend Background Processor and the Backend API Azure Container Apps. + +## Module Sections + +--8<-- "snippets/restore-variables.md" + +### 1. Pub/Sub Pattern with Dapr + +As a best practice, it is recommended that we decouple services from each other. A conventional way to do so is by employing the Publisher-Subscriber (Pub/Sub) pattern. The primary advantage of this pattern is that it offers loose coupling between services where the sender/publisher of the message doesn't know anything about the receivers/consumers. This can be done in a 1-1 or 1-many constellation in which multiple consumers each receive a copy of the message in a totally different way. For example, imagine adding another consumer which is responsible for sending push notifications to the task owner (e.g. if we had a mobile app channel). + +In module 3 we introduced you to decoupling `{{ apps.frontend }}` from `{{ apps.backend }}` through asynchronous HTTP calls via Dapr. And in module 4 we added integrations with Redis Cache locally and Azure Cosmos DB in the cloud. In this module we will configure such a Pub/Sub pattern to faciliate asynchronous messaging between the microservices. Specifically, the publisher/subscriber pattern relies on a message broker which is responsible for receiving the message from the publisher, storing the message to ensure durability, and delivering this message to the interested consumer(s) to process it. There is no need for the consumers to be available when the message is stored in the message broker. Consumers can process the messages at a later time in an async fashion. +The below diagram gives a high-level overview of how the Pub/Sub pattern works: ![pubsub-arch](../../assets/images/05-aca-dapr-pubsubapi/tutorial-pubsub-arch.jpg) -If you implemented the Pub/Sub pattern before, you already know that there is a lot of plumbing needed on the publisher and subscriber components in order to publish and consume messages. -In addition, each message broker has its own SDK and implementation. So you need to write your code in an abstracted way to hide the specific implementation details for each message broker SDK and make it easier for the publisher and consumers to re-use this functionality. What Dapr offers here is a building block that significantly simplifies implementing pub/sub functionality. +If you implemented the Pub/Sub pattern before, you already know that there is a lot of plumbing needed on the publisher and subscriber components in order to publish and consume messages. In addition, each message broker has its own SDK and implementation. So you need to write your code in an abstracted way to hide the specific implementation details for each message broker SDK and make it easier for the publisher and consumers to re-use this functionality. What Dapr offers here is a building block that significantly simplifies implementing Pub/Sub functionality by abstracting the implementation of the provider from the usage of the pattern in the container. Differently, the container does not know who it is interacting with - and this is entirely intentional and favorable for container portability and immutability. -Put simply, the Dapr pub/sub building block provides a platform-agnostic API framework to send and receive messages. Your producer/publisher services publish messages to a named topic. Your consumer services subscribe to a topic to consume messages. +Put simply, the Dapr Pub/Sub building block provides a platform-agnostic API framework to send and receive messages. Your producer/publisher services publish messages to a named topic. Your consumer services subscribe to a topic to consume messages. -To try this out we can directly invoke the Pub/Sub API and publish a message to Redis locally. If you remember from [module 3](../../aca/03-aca-dapr-integration/index.md) once we initialized Dapr in a local development environment, it installed Redis container instance locally. So we can use Redis locally to publish and subscribe to a message. +#### 1.1 Testing Pub/Sub Locally + +To try this out we can directly invoke the Pub/Sub API and publish a message to Redis locally. If you remember from [module 3](../../aca/03-aca-dapr-integration/index.md), when we initialized Dapr in a local development environment, it installed Redis container instance locally. Therefore, we can use Redis locally to publish and subscribe to a message. If you navigate to the path `%USERPROFILE%\.dapr\components (assuming you are using windows)` you will find a file named `pubsub.yaml`. Inside this file, you will see the properties needed to access the local Redis instance. The publisher/subscriber brokers template component file structure can be found [here](https://docs.dapr.io/operations/components/setup-pubsub/){target=_blank}. ---8<-- "snippets/restore-variables.md" - -We want to have more control and provide our own component file, so let's create pub/sub component file in our **components** folder as shown below: +However, we want to have more control and provide our own component file, so let's create Pub/Sub component file in our **components** folder as shown below: === "dapr-pubsub-redis.yaml" @@ -40,38 +50,15 @@ We want to have more control and provide our own component file, so let's create To try out the Pub/Sub API, run the Backend API from VS Code by running the below command or using the Run and Debug tasks we have created in the [appendix](../13-appendix/01-run-debug-dapr-app-vscode.md). -!!! note - Don't forget to include the property `--resources-path`. Remember to replace the placeholders with your own values - === ".NET 6 or below" - ```powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components-dotnet6" === ".NET 7 or above" - ```powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components" -Now let's try to publish a message by sending a **POST** request to [http://localhost:3500/v1.0/publish/taskspubsub/tasksavedtopic](http://localhost:3500/v1.0/publish/taskspubsub/tasksavedtopic) with the below request body, don't forget to set the `Content-Type` header to `application/json` +Let's try to publish a message by sending a **POST** request to [http://localhost:3500/v1.0/publish/taskspubsub/tasksavedtopic](http://localhost:3500/v1.0/publish/taskspubsub/tasksavedtopic) with the below request body, don't forget to set the `Content-Type` header to `application/json` ```json { @@ -84,49 +71,42 @@ Now let's try to publish a message by sending a **POST** request to [http://loca } ``` -??? tip "Curious about the details of the endpoint" - We can break endpoint into the following: +??? tip "Curious about the details of the endpoint?" + We can break the endpoint into the following: - The value `3500`: is the Dapr app listing port, it is the port number upon which the Dapr sidecar is listening. - - The value `taskspubsub`: is the name of the selected Dapr pub/sub-component. + - The value `taskspubsub`: is the name of the selected Dapr Pub/Sub-component. - The value `tasksavedtopic`: is the name of the topic to which the message is published. -If all is configured correctly, you should receive HTTP response 204 from this endpoint which indicates that the message is published successfully by the service broker (Redis) into the topic named `tasksavedtopic`. +If all is configured correctly, you should see an *HTTP 204 No Content* response from this endpoint which indicates that the message was published successfully by the service broker (Redis) into the topic named `tasksavedtopic`. You can also check that topic is created successfully by using the [Redis Xplorer extension](https://marketplace.visualstudio.com/items?itemName=davidsekar.redis-xplorer){target=_blank} in VS Code which should look like this: ![redis-xplorer](../../assets/images/05-aca-dapr-pubsubapi/task-saved-topic-redis-xplorer.png) -Right now those published messages are stored in the message broker topic doing nothing as we don't have any subscribers bound to the service broker on the topic `tasksavedtopic` which are interested in consuming and processing those messages. So let`s add a consumer to consume the message. +Right now those published messages are just hanging out in the message broker topic. We don't yet have any subscribers bound to the service broker on the topic `tasksavedtopic`, which are interested in consuming and processing those messages. We will set up such a consumer in the next section. !!! note Some Service Brokers allow the creation of topics automatically when sending a message to a topic which has not been created before. That's the reason why the topic `tasksavedtopic` was created automatically here for us. -### Setting up the Backend Background Processor Project +### 2. Setting up the Backend Background Processor Project + +#### 2.1 Create the Backend Service Project -#### 1. Create the Backend Service Project +In this module, we will also introduce a new background service which is named `{{ apps.backendsvc }}` according to our [architecture diagram](../../assets/images/00-workshop-intro/ACA-Architecture-workshop.jpg). This new service will be responsible for sending notification emails (simulated) to the task owners to notify them that a new task has been assigned to them. We can do this in the Backend API and send the email right after saving the task, but we want to offload this process to another service and keep the Backend API service responsible for managing tasks data only. -Now we will add a new ASP.NET Core Web API project named **TasksTracker.Processor.Backend.Svc**. Open a command-line terminal and navigate to **root folder** of your project. +Now we will add a new ASP.NET Core Web API project named **TasksTracker.Processor.Backend.Svc**. Open a command-line terminal and navigate to the workshop's root. -```powershell +```shell dotnet new webapi -o TasksTracker.Processor.Backend.Svc ``` - Delete the boilerplate `WeatherForecast.cs` and `Controllers\WeatherForecastController.cs` files from the new `TasksTracker.Processor.Backend.Svc` project folder. -We need to containerize this application, so we can push it to Azure Container Registry as a docker image then deploy it to ACA. -To do so Open the VS Code Command Palette (++ctrl+shift+p++) and select **Docker: Add Docker Files to Workspace...** - -- Use `.NET: ASP.NET Core` when prompted for application platform. -- Choose `Linux` when prompted to choose the operating system. - - You will be asked if you want to add Docker Compose files. Select `No`. - - Take a note of the provided **application port** as we will be using later on. You can always find it again inside the designated `DockerFile` inside the newly created project's directory. - - `Dockerfile` and `.dockerignore` files are added to the workspace. +--8<-- "snippets/containerize-app.md" -- Open `Dockerfile` and replace `FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0 AS build` with `FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build`. +#### 2.2 Add Models -#### 2. Add Models - -Now we will add the model which will be used to deserialize the published message. Add below file under new folder named **Models**. +Now we will add the model which will be used to deserialize the published message. Create a **Models** folder and add this file: === "TaskModel.cs" @@ -137,9 +117,9 @@ Now we will add the model which will be used to deserialize the published messag !!! tip For sake of simplicity we are recreating the same model `TaskModel.cs` under each project. For production purposes it is recommended to place the `TaskModel.cs` in a common project that can be referenced by all the projects and thus avoid code repetition which increases the maintenance cost. -#### 3.Install Dapr SDK Client NuGet package +#### 2.3 Install Dapr SDK Client NuGet package -Now we will install Dapr SDK to be able to subscribe to the service broker topic in a programmatic way. To do so, add the highlighted NuGet package to the file shown below: +Now we will install Dapr SDK to be able to subscribe to the service broker topic in a programmatic way. Add the highlighted NuGet package to the file shown below: === ".NET 6" @@ -156,27 +136,12 @@ Now we will install Dapr SDK to be able to subscribe to the service broker topic === "TasksTracker.Processor.Backend.Svc.csproj" ```xml hl_lines="10" - - - - net7.0 - enable - enable - - - - - - - - - + --8<-- "docs/aca/05-aca-dapr-pubsubapi/Backend.Svc.csproj" ``` -#### 4. Create an API Endpoint for the Consumer to Subscribe to the Topic +#### 2.4 Create an API Endpoint for the Consumer to Subscribe to the Topic -Now we will add an endpoint that will be responsible to subscribe to the topic in the message broker we are interested in. This endpoint will start receiving the message published from the Backend API producer. -To do so, add a new controller under **Controllers** folder. +Now we will add an endpoint that will be responsible to subscribe to the topic in the message broker we are interested in. This endpoint will start receiving the message published from the Backend API producer. Add a new controller under **Controllers** folder. === "TasksNotifierController.cs" @@ -184,10 +149,10 @@ To do so, add a new controller under **Controllers** folder. --8<-- "docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController.cs" ``` -??? tip "Curious about what we have done so far" +??? tip "Curious about what we have done so far?" - We have added an action method named `TaskSaved` which can be accessed on the route `api/tasksnotifier/tasksaved` - - We have attributed this action method with the attribute `Dapr.Topic` which accepts the Dapr pub/sub component to target as the first argument, + - We have attributed this action method with the attribute `Dapr.Topic` which accepts the Dapr Pub/Sub component to target as the first argument, and the second argument is the topic to subscribe to, which in our case is `tasksavedtopic`. - The action method expects to receive a `TaskModel` object. - Now once the message is received by this endpoint, we can start out the business logic to trigger sending an email (more about this next) and then return `200 OK` response to indicate that the consumer @@ -197,8 +162,8 @@ To do so, add a new controller under **Controllers** folder. - If we need to drop the message as we are aware it will not be processed even after retries (i.e Email to is not formatted correctly) we return a `404 Not Found` response. This will tell the message broker to drop the message and move it to dead-letter or poison queue. -Now you are probably wondering how the consumer was able to identify what are the subscriptions available and on which route they can be found at. -The answer for this is that at startup on the consumer service (more on that below after we add app.MapSubscribeHandler()), the Dapr runtime will call the application on a well-known endpoint to identify and create the required subscriptions. +You may be wondering how the consumer was able to identify what are the subscriptions available and on which route they can be found at. +The answer for this is that at startup on the consumer service (more on that below after we add `app.MapSubscribeHandler())`, the Dapr runtime will call the application on a well-known endpoint to identify and create the required subscriptions. The well-known endpoint can be reached on this endpoint: `http://localhost:/dapr/subscribe`. When you invoke this endpoint, the response will contain an array of all available topics for which the applications will subscribe. Each includes a route to call when the topic receives a message. This was generated as we used the attribute `Dapr.Topic` on the action method `api/tasksnotifier/tasksaved`. @@ -220,7 +185,7 @@ In our case, a sample response will be as follows: Follow this [link](https://learn.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/publish-subscribe#how-it-works){target=_blank} to find a detailed diagram of how the consumers will discover and subscribe to those endpoints. -#### 5. Register Dapr and Subscribe Handler at the Consumer Startup +#### 2.5 Register Dapr and Subscribe Handler at the Consumer Startup Update below file in **TasksTracker.Processor.Backend.Svc** project. @@ -229,60 +194,15 @@ Update below file in **TasksTracker.Processor.Backend.Svc** project. === "Program.cs" ```csharp hl_lines="9 13 15" - namespace TasksTracker.Processor.Backend.Svc - { - public class Program - { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - // Add services to the container. - builder.Services.AddControllers().AddDapr(); - var app = builder.Build(); - app.UseHttpsRedirection(); - app.UseAuthorization(); - app.UseCloudEvents(); - app.MapControllers(); - app.MapSubscribeHandler(); - app.Run(); - } - } - } + --8<-- "docs/aca/05-aca-dapr-pubsubapi/Program-dotnet6.cs" ``` === ".NET 7" === "Program.cs" - - ```csharp hl_lines="4 22 26" - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container. - builder.Services.AddControllers().AddDapr(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - var app = builder.Build(); - - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } - - app.UseHttpsRedirection(); - - app.UseAuthorization(); - - app.UseCloudEvents(); - - app.MapControllers(); - - app.MapSubscribeHandler(); - - app.Run(); + + ```csharp hl_lines="5 23 27" + --8<-- "docs/aca/05-aca-dapr-pubsubapi/Program.cs" ``` - Let's verify that the Dapr dependency is restored properly and that the project compiles. From VS Code Terminal tab, open developer command prompt or PowerShell terminal and navigate to the parent directory which hosts the `.csproj` project folder and build the project. @@ -303,65 +223,27 @@ Update below file in **TasksTracker.Processor.Backend.Svc** project. - On line `app.MapSubscribeHandler();`, we make the endpoint `http://localhost:/dapr/subscribe` available for the consumer so it responds and returns available subscriptions. When this endpoint is called, it will automatically find all WebAPI action methods decorated with the `Dapr.Topic` attribute and instruct Dapr to create subscriptions for them. -With all those bits in place, we are ready to run the publisher service `Backend API` and the consumer service `Backend Background Service` and test pub/sub pattern end to end. +With all those bits in place, we are ready to run the publisher service `Backend API` and the consumer service `Backend Background Service` and test Pub/Sub pattern end to end. -```powershell -$BACKEND_SERVICE_APP_PORT=launchSettings.json. e.g. 7051> +```shell +$BACKEND_SERVICE_APP_PORT=launchSettings.json (e.g. 7051)> ``` --8<-- "snippets/update-variables.md::5" To do so, run the below commands in two separate PowerShell console, ensure you are on the right root folder of each respective project. ---8<-- "snippets/restore-variables.md:9:14" +--8<-- "snippets/restore-variables.md:7:11" === ".NET 6 or below" - ```powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - dotnet run - - cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc - - dapr run ` - --app-id tasksmanager-backend-processor ` - --app-port $BACKEND_SERVICE_APP_PORT ` - --dapr-http-port 3502 ` - --app-ssl ` - --resources-path "../components" ` - dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components-dotnet6" + --8<-- "snippets/dapr-run-backend-service.md:dapr-components-dotnet6" === ".NET 7 or above" - ```powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - - cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc - - dapr run ` - --app-id tasksmanager-backend-processor ` - --app-port $BACKEND_SERVICE_APP_PORT ` - --dapr-http-port 3502 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components" + --8<-- "snippets/dapr-run-backend-service.md:dapr-components" !!! note Notice that we gave the new Backend background service a Dapr App Id with the name `tasksmanager-backend-processor` and a Dapr HTTP port with the value `3502`. @@ -390,7 +272,9 @@ Keep an eye on the terminal logs of the Backend background processor as you will ![dapr-pub-sub-code-extension](../../assets/images/05-aca-dapr-pubsubapi/dapr-pub-sub-code-extension.jpg) -#### 6. Optional: Update VS Code Tasks and Launch Configuration Files +Shut down the sessions. + +#### 2.6 Optional: Update VS Code Tasks and Launch Configuration Files If you have followed the steps in the [appendix](../13-appendix/01-run-debug-dapr-app-vscode.md) so far in order to be able to run the three services together (frontend, backend api, and backend processor) and debug them in VS Code, we need to update the files `tasks.json` and `launch.json` to include the new service we have added. @@ -417,9 +301,7 @@ This is a good opportunity to save intermediately: *** -### Use the Dapr .NET Client SDK to Publish Messages - -#### 1. Update Backend API to Publish a Message When a Task Is Saved +#### 2.7 Update Backend API to Publish a Message When a Task Is Saved Now we need to update our Backend API to publish a message to the message broker when a task is saved (either due to a new task being added or an existing task assignee being updated). @@ -427,54 +309,8 @@ To do this, update below file under the project **TasksTracker.TasksManager.Back === "TasksStoreManager.cs" - ```csharp hl_lines="1-7 24 40-43" - //Add new private method - private async Task PublishTaskSavedEvent(TaskModel taskModel) - { - _logger.LogInformation("Publish Task Saved event for task with Id: '{0}' and Name: '{1}' for Assigne: '{2}'", - taskModel.TaskId, taskModel.TaskName, taskModel.TaskAssignedTo); - await _daprClient.PublishEventAsync("dapr-pubsub-servicebus", "tasksavedtopic", taskModel); - } - - //Update the below method: - public async Task CreateNewTask(string taskName, string createdBy, string assignedTo, DateTime dueDate) - { - var taskModel = new TaskModel() - { - TaskId = Guid.NewGuid(), - TaskName = taskName, - TaskCreatedBy = createdBy, - TaskCreatedOn = DateTime.UtcNow, - TaskDueDate = dueDate, - TaskAssignedTo = assignedTo, - }; - - _logger.LogInformation("Save a new task with name: '{0}' to state store", taskModel.TaskName); - await _daprClient.SaveStateAsync(STORE_NAME, taskModel.TaskId.ToString(), taskModel); - await PublishTaskSavedEvent(taskModel); - return taskModel.TaskId; - } - - //Update the below method: - public async Task UpdateTask(Guid taskId, string taskName, string assignedTo, DateTime dueDate) - { - _logger.LogInformation("Update task with Id: '{0}'", taskId); - var taskModel = await _daprClient.GetStateAsync(STORE_NAME, taskId.ToString()); - var currentAssignee = taskModel.TaskAssignedTo; - if (taskModel != null) - { - taskModel.TaskName = taskName; - taskModel.TaskAssignedTo = assignedTo; - taskModel.TaskDueDate = dueDate; - await _daprClient.SaveStateAsync(STORE_NAME, taskModel.TaskId.ToString(), taskModel); - if (!taskModel.TaskAssignedTo.Equals(currentAssignee, StringComparison.OrdinalIgnoreCase)) - { - await PublishTaskSavedEvent(taskModel); - } - return true; - } - return false; - } + ```csharp hl_lines="33 88-91 97-102" + --8<-- "docs/aca/05-aca-dapr-pubsubapi/TasksStoreManager.cs" ``` !!! tip @@ -482,88 +318,16 @@ To do this, update below file under the project **TasksTracker.TasksManager.Back The second parameter `tasksavedtopic` is the topic name the publisher going to send the task model to. That's all the changes required to start publishing async messages from the Backend API. -#### 2. Update Backend Background Processor to Consume Messages and Simulate Sending Emails - -Update the files below under the project **TasksTracker.Processor.Backend.Svc**. We are installing the NuGet package named `SendGrid` to the Backend processor project which will allow us to send emails. - -=== "TasksNotifierController.cs" - - ```csharp - --8<-- "docs/aca/05-aca-dapr-pubsubapi/TasksNotifierController-SendGrid.cs" - ``` - -=== "TasksTracker.Processor.Backend.Svc.csproj" - - ```xml hl_lines="2" - - - - ``` - -=== "appsettings.json" - - ```json hl_lines="8-12" - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "SendGrid": { - "ApiKey": "", - "IntegrationEnabled":false - } - } - ``` - -!!! tip - Follow this [link](https://signup.sendgrid.com/){target=_blank} to set up a SendGrid account. Also follow [these](https://docs.sendgrid.com/ui/account-and-settings/api-keys#creating-an-api-key){target=_blank} instructions to fetch your SendGrid ApiKey. - - Please note that the SendGrid API KEY is generated and displayed to you just once. So be sure to copy and save it somewhere. After that only the subset key is displayed. - -!!! tip - In the [next module](../06-aca-dapr-bindingsapi/index.md) we will be using a new type of Dapr components to send emails using SendGrid. - - If you don't want to bother with signing up for a SendGrid account to send emails, you can just simulate sending emails by returning always `ok` from the `TaskSaved` method as shown below. - - ```csharp hl_lines="4 7-14" - // If you prefer not to deal with setting up a SendGrid account then simply always return OK from the TaskSaved method - [Dapr.Topic("dapr-pubsub-servicebus", "tasksavedtopic")] - [HttpPost("tasksaved")] - public IActionResult TaskSaved([FromBody] TaskModel taskModel) - { - _logger.LogInformation("Started processing message with Task Name '{0}'", taskModel.TaskName); - // I decided to simulate a call - //var sendGridResponse = await SendEmail(taskModel); - - //if (sendGridResponse.Item1) - //{ - return Ok($"SendGrid response status code: 200"); - //} - //return BadRequest($"Failed to send email, SendGrid response status code: {sendGridResponse.Item1}"); - } - ``` - -??? info "Curious to learn what's happening in the code above?" - - - We've updated the attribute `Dapr.Topic` to use the same Pub/Sub component name used in the publisher `dapr-pubsub-servicebus`. Then we added a new method that is responsible to consume the received message, - taking the assignee email and trying to send an email using SendGrid API. - - We are returning `200 Ok` if the SendGrid was able to send the email successfully, otherwise we are returning `400 Bad Request` if the SendGrid failed to send the email. - This will allow the consumer service to re-try processing the message again on failure. - - We are reading the `SendGrid:ApiKey` from AppSettings and later we will read this value from environment variables once we deploy this service to Azure Container Apps. - -### Use Azure Service Bus as a Service Broker for Dapr Pub/Sub API +### 3. Use Azure Service Bus as a Service Broker for Dapr Pub/Sub API Now we will switch our implementation to use Azure Service Bus as a message broker. Redis worked perfectly for local development and testing, but we need to prepare ourselves for the cloud deployment. To do so we need to create Service Bus Namespace followed by a Topic. A namespace provides a scoping container for Service Bus resources within your application. -#### 1. Create Azure Service Bus Namespace and a Topic +#### 3.1 Create Azure Service Bus Namespace and a Topic You can do this from [Azure portal](https://portal.azure.com){target=_blank} or use the below PowerShell command to create the services. We will assume you are using the same PowerShell session from the previous module so variables still hold the right values. You need to change the namespace variable as this one should be unique globally across all Azure subscriptions. Also, you will notice that we are opting for standard sku (default if not passed) as topics only available on the standard tier not and not on the basic tier. More details can be found [here](https://learn.microsoft.com/en-us/cli/azure/servicebus/namespace?view=azure-cli-latest#az-servicebus-namespace-create-optional-parameters){target=_blank}. -```powershell +```shell $SERVICE_BUS_NAMESPACE_NAME="sbns-taskstracker-$RANDOM_STRING" $SERVICE_BUS_TOPIC_NAME="tasksavedtopic" $SERVICE_BUS_TOPIC_SUBSCRIPTION="sbts-tasks-processor" @@ -593,7 +357,7 @@ az servicebus namespace authorization-rule keys list ` !!! note Primary connection string is only needed for local dev testing. We will be using Managed Identities when publishing container apps to ACA. -#### 2. Create a local Dapr Component file for Pub/Sub API Using Azure Service Bus +#### 3.2 Create a local Dapr Component file for Pub/Sub API Using Azure Service Bus Add a new files **components** as shown below: @@ -609,7 +373,7 @@ Add a new files **components** as shown below: We have set the scopes section to include the `tasksmanager-backend-api` and `tasksmanager-backend-processor` app ids, as those will be the Dapr apps that need access to Azure Service Bus for publishing and consuming the messages. -#### 3. Create an ACA Dapr Component file for Pub/Sub API Using Azure Service Bus +#### 3.3 Create an ACA Dapr Component file for Pub/Sub API Using Azure Service Bus Add a new files **aca-components** as shown below: @@ -627,58 +391,20 @@ Add a new files **aca-components** as shown below: - The metadata `namespaceName` value is set to the address of the Service Bus namespace as a fully qualified domain name. The `namespaceName` key is mandatory when using Managed Identities for authentication. - We are setting the metadata `consumerID` value to match the topic subscription name `sbts-tasks-processor`. If you didn't set this metadata, dapr runtime will try to create a subscription using the dapr application ID. -With all those bits in place, we are ready to run the publisher service `Backend API` and the consumer service `Backend Background Service` and test pub/sub pattern end to end. +With all those bits in place, we are ready to run the publisher service `Backend API` and the consumer service `Backend Background Service` and test Pub/Sub pattern end to end. !!! note Ensure you are on the right root folder of each respective project. Remember to replace the placeholders with your own values. === ".NET 6 or below" - ```powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - dotnet run - - cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc - - dapr run ` - --app-id tasksmanager-backend-processor ` - --app-port $BACKEND_SERVICE_APP_PORT ` - --dapr-http-port 3502 ` - --app-ssl ` - --resources-path "../components" ` - dotnet run - ``` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components-dotnet6" + --8<-- "snippets/dapr-run-backend-service.md:dapr-components-dotnet6" === ".NET 7 or above" - ````powershell - cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api - - dapr run ` - --app-id tasksmanager-backend-api ` - --app-port $API_APP_PORT ` - --dapr-http-port 3500 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - - cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc - - dapr run ` - --app-id tasksmanager-backend-processor ` - --app-port $BACKEND_SERVICE_APP_PORT ` - --dapr-http-port 3502 ` - --app-ssl ` - --resources-path "../components" ` - -- dotnet run --launch-profile https - ```` + --8<-- "snippets/dapr-run-backend-api.md:dapr-components" + --8<-- "snippets/dapr-run-backend-service.md:dapr-components" !!! note We gave the new Backend background service a Dapr App Id with the name `tasksmanager-backend-processor` and a Dapr HTTP port with the value **3502**. @@ -702,16 +428,16 @@ Content-Type: application/json You should see console messages from APP in the backend service console as you send requests. -### Deploy the Backend Background Processor and the Backend API Projects to Azure Container Apps +### 4. Deploy the Backend Background Processor and the Backend API Projects to Azure Container Apps -#### 1. Build the Backend Background Processor and the Backend API App Images and Push Them to ACR +#### 4.1 Build the Backend Background Processor and the Backend API App Images and Push Them to ACR As we have done previously we need to build and deploy both app images to ACR, so they are ready to be deployed to Azure Container Apps. !!! note Make sure you are in root directory of the project, i.e. **TasksTracker.ContainerApps** -```powershell +```shell $BACKEND_SERVICE_NAME="tasksmanager-backend-processor" az acr build ` @@ -725,42 +451,19 @@ az acr build ` --file 'TasksTracker.Processor.Backend.Svc/Dockerfile' . ``` -#### 2. Create a new Azure Container App to host the new Backend Background Processor +#### 4.2 Create a new Azure Container App to host the new Backend Background Processor Now we need to create a new Azure Container App. We need to have this new container app with those capabilities in place: - Ingress for this container app should be disabled (no access via HTTP at all as this is a background processor responsible to process published messages). - Dapr needs to be enabled. -- **Optional** (only if you activated your SendGrid account and received an api key. Otherwise, remove the `--secrets` and `--env-var` from the powershell command below). -Setting the value of SendGrid API in the secrets store and referencing it in the environment variables, as well setting the flag `IntegrationEnabled` to `true` so it will send actual emails. To achieve the above, run the PowerShell script below. !!! note Notice how we removed the Ingress property totally which disables the Ingress for this Container App. Remember to replace the placeholders with your own values: -=== "Using SendGrid" - - ```powershell - az containerapp create ` - --name "$BACKEND_SERVICE_NAME" ` - --resource-group $RESOURCE_GROUP ` - --environment $ENVIRONMENT ` - --image "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io/tasksmanager/$BACKEND_SERVICE_NAME" ` - --registry-server "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io" ` - --min-replicas 1 ` - --max-replicas 1 ` - --cpu 0.25 --memory 0.5Gi ` - --enable-dapr ` - --dapr-app-id $BACKEND_SERVICE_NAME ` - --dapr-app-port $TARGET_PORT ` - --secrets "sendgrid-apikey=" ` - --env-vars "SendGrid__ApiKey=secretref:sendgrid-apikey" "SendGrid__IntegrationEnabled=true" - ``` - -=== "Not Using SendGrid" - - ```powershell + ```shell az containerapp create ` --name "$BACKEND_SERVICE_NAME" ` --resource-group $RESOURCE_GROUP ` @@ -775,11 +478,11 @@ To achieve the above, run the PowerShell script below. --dapr-app-port $TARGET_PORT ``` -#### 3. Deploy New Revisions of the Backend API to Azure Container Apps +#### 4.3 Deploy New Revisions of the Backend API to Azure Container Apps We need to update the Azure Container App hosting the Backend API with a new revision so our code changes for publishing messages after a task is saved is available for users. -```powershell +```shell # Update Backend API App container app and create a new revision az containerapp update ` --name $BACKEND_API_NAME ` @@ -787,11 +490,11 @@ az containerapp update ` --revision-suffix v$TODAY-2 ``` -#### 4. Add Azure Service Bus Dapr Pub/Sub Component to Azure Container Apps Environment +#### 4.4 Add Azure Service Bus Dapr Pub/Sub Component to Azure Container Apps Environment Deploy the Dapr Pub/Sub Component to the Azure Container Apps Environment using the following command: -```powershell +```shell az containerapp env dapr-component set ` --name $ENVIRONMENT ` --resource-group $RESOURCE_GROUP ` @@ -802,15 +505,15 @@ az containerapp env dapr-component set ` !!! note Notice that we set the component name `dapr-pubsub-servicebus` when we added it to the Container Apps Environment. -### Configure Managed Identities for Both Container Apps +### 5. Configure Managed Identities for Both Container Apps In the previous module we have [already configured](../04-aca-dapr-stateapi/index.md#configure-managed-identities-in-container-app) and used system-assigned identity for the Backend API container app. We follow the same steps here to create an association between the backend processor container app and Azure Service Bus. -#### 1. Create system-assigned identity for Backend Processor App +#### 5.1 Create system-assigned identity for Backend Processor App Run the command below to create `system-assigned` identity for our Backend Processor App: -```powershell +```shell az containerapp identity assign ` --resource-group $RESOURCE_GROUP ` --name $BACKEND_SERVICE_NAME ` @@ -833,7 +536,7 @@ This command will create an Enterprise Application (basically a Service Principa } ``` -#### 2. Grant Backend Processor App the Azure Service Bus Data Receiver Role +#### 5.2 Grant Backend Processor App the Azure Service Bus Data Receiver Role We will be using a `system-assigned` managed identity with a role assignments to grant our Backend Processor App the `Azure Service Bus Data Receiver` role which will allow it to receive messages from Service Bus queues and subscriptions. @@ -841,7 +544,7 @@ You can read more about `Azure built-in roles for Azure Service Bus` [here](http Run the command below to associate the `system-assigned` identity with the access-control role `Azure Service Bus Data Receiver`: -```powershell +```shell $SVC_BUS_DATA_RECEIVER_ROLE = "Azure Service Bus Data Receiver" # Built in role name az role assignment create ` @@ -850,11 +553,11 @@ az role assignment create ` --scope /subscriptions/$AZURE_SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.ServiceBus/namespaces/$SERVICE_BUS_NAMESPACE_NAME ``` -#### 3. Grant Backend API App the Azure Service Bus Data Sender Role +#### 5.3 Grant Backend API App the Azure Service Bus Data Sender Role We'll do the same with Backend API container app, but we will use a different Azure built-in roles for Azure Service Bus which is the role `Azure Service Bus Data Sender` as the Backend API is a publisher of the messages. Run the command below to associate the `system-assigned` with access-control role `Azure Service Bus Data Sender`: -```powershell +```shell $SVC_BUS_DATA_SENDER_ROLE = "Azure Service Bus Data Sender" # Built in role name az role assignment create ` @@ -863,11 +566,11 @@ az role assignment create ` --scope /subscriptions/$AZURE_SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.ServiceBus/namespaces/$SERVICE_BUS_NAMESPACE_NAME ``` -#### 4. Restart Container Apps +#### 5.4 Restart Container Apps Lastly, we need to restart both container apps revisions to pick up the role assignment. -```powershell +```shell # Get revision name and assign it to a variable $REVISION_NAME = (az containerapp revision list ` --name $BACKEND_SERVICE_NAME ` @@ -893,16 +596,14 @@ az containerapp revision restart ` ``` !!! Success - With this in place, you should be able to test the 3 services end to end and should receive a notification email to the task assignee email, the email will look like the below. - - ![email-log](../../assets/images/05-aca-dapr-pubsubapi/email-log.jpg) + With this in place, you should be able to test the 3 services end to end. !!! note - If you opted not to activate the SendGrid then you won't receive the email. In this case you can get the backend processor logs using the command below. Start by running the command below and then launch the + Start by running the command below and then launch the application and start creating new tasks. You should start seeing logs similar to the ones shown in the image below. The command will stop executing after 60 seconds of inactivity. - ```powershell + ```shell az containerapp logs show --follow ` -n $BACKEND_SERVICE_NAME ` -g $RESOURCE_GROUP @@ -918,4 +619,14 @@ az containerapp revision restart ` --8<-- "snippets/update-variables.md" --8<-- "snippets/persist-state.md:module52" +## Review + +In this module, we have accomplished five objectives: + +1. Learned how Azure Container Apps uses the Publisher-Subscriber (Pub/Sub) pattern with Dapr. +1. Introduced a new background service, `{{ apps.backendsvc }}` configured for Dapr. +1. Used Azure Service Bus as a Service Broker for Dapr Pub/Sub API. +1. Deployed the Backend Background Processor and the updated Backend API Projects to Azure Container Apps. +1. Configured Managed Identities for the Backend Background Processor and the Backend API Azure Container Apps. + The next module will delve into the implementation of Dapr bindings with ACA. diff --git a/docs/aca/13-appendix/Set-Variables.ps1 b/docs/aca/13-appendix/Set-Variables.ps1 index c3cc3f95..b5abb4b3 100644 --- a/docs/aca/13-appendix/Set-Variables.ps1 +++ b/docs/aca/13-appendix/Set-Variables.ps1 @@ -14,6 +14,7 @@ $vars = @( "BACKEND_API_EXTERNAL_BASE_URL", "BACKEND_API_INTERNAL_BASE_URL", "BACKEND_API_NAME", + "BACKEND_API_PRINCIPAL_ID", "BACKEND_SERVICE_APP_PORT", "BACKEND_SERVICE_NAME", "BACKEND_SERVICE_PRINCIPAL_ID", diff --git a/mkdocs.yml b/mkdocs.yml index dd0cf0c1..122dd5d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ plugins: extra: apps: backend: "ACA API - Backend" + backendsvc: "ACA Processor - Backend" frontend: "ACA Web - Frontend" dapr: version: 1.9.0 diff --git a/snippets/dapr-run-backend-api.md b/snippets/dapr-run-backend-api.md new file mode 100644 index 00000000..b2888fcd --- /dev/null +++ b/snippets/dapr-run-backend-api.md @@ -0,0 +1,59 @@ + +--8<-- [start:basic] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api + +dapr run ` +--app-id tasksmanager-backend-api ` +--app-port $API_APP_PORT ` +--dapr-http-port 3500 ` +--app-ssl ` +-- dotnet run --launch-profile https +``` +--8<-- [end:basic] + + +--8<-- [start:dapr-components] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api + +dapr run ` +--app-id tasksmanager-backend-api ` +--app-port $API_APP_PORT ` +--dapr-http-port 3500 ` +--app-ssl ` +--resources-path "../components" ` +-- dotnet run --launch-profile https +``` +--8<-- [end:dapr-components] + + + + +--8<-- [start:basic-dotnet6] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api + +dapr run ` +--app-id tasksmanager-backend-api ` +--app-port $API_APP_PORT ` +--dapr-http-port 3500 ` +--app-ssl ` +-- dotnet run +``` +--8<-- [end:basic-dotnet6] + + +--8<-- [start:dapr-components-dotnet6] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api + +dapr run ` +--app-id tasksmanager-backend-api ` +--app-port $API_APP_PORT ` +--dapr-http-port 3500 ` +--app-ssl ` +--resources-path "../components" ` +-- dotnet run +``` +--8<-- [end:dapr-components-dotnet6] diff --git a/snippets/dapr-run-backend-service.md b/snippets/dapr-run-backend-service.md new file mode 100644 index 00000000..de92fb07 --- /dev/null +++ b/snippets/dapr-run-backend-service.md @@ -0,0 +1,31 @@ + +--8<-- [start:dapr-components] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc + +dapr run ` +--app-id tasksmanager-backend-processor ` +--app-port $BACKEND_SERVICE_APP_PORT ` +--dapr-http-port 3502 ` +--app-ssl ` +--resources-path "../components" ` +-- dotnet run --launch-profile https +``` +--8<-- [end:dapr-components] + + + + +--8<-- [start:dapr-components-dotnet6] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.Processor.Backend.Svc + +dapr run ` +--app-id tasksmanager-backend-processor ` +--app-port $BACKEND_SERVICE_APP_PORT ` +--dapr-http-port 3502 ` +--app-ssl ` +--resources-path "../components" ` +-- dotnet run +``` +--8<-- [end:dapr-components-dotnet6] diff --git a/snippets/dapr-run-frontend-webapp.md b/snippets/dapr-run-frontend-webapp.md new file mode 100644 index 00000000..b07c2583 --- /dev/null +++ b/snippets/dapr-run-frontend-webapp.md @@ -0,0 +1,29 @@ + +--8<-- [start:basic] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui + +dapr run ` +--app-id tasksmanager-frontend-webapp ` +--app-port $UI_APP_PORT ` +--dapr-http-port 3501 ` +--app-ssl ` +-- dotnet run --launch-profile https +``` +--8<-- [end:basic] + + + + +--8<-- [start:basic-dotnet6] +```shell +cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui + +dapr run ` +--app-id tasksmanager-frontend-webapp ` +--app-port $UI_APP_PORT ` +--dapr-http-port 3501 ` +--app-ssl ` +-- dotnet run +``` +--8<-- [end:basic-dotnet6] diff --git a/snippets/update-variables.md b/snippets/update-variables.md index 72464cd3..b3af0e9f 100644 --- a/snippets/update-variables.md +++ b/snippets/update-variables.md @@ -4,7 +4,7 @@ .\Set-Variables.ps1 ``` -- Persist a list of all current variables. +- From the root, persist a list of all current variables. ```shell git add .\Variables.ps1