This is the third 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 – (This Post)
- Dapr Integration with Azure Container Apps – Part 4
- 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
Communication between Microservices in Azure Container Apps
In this post, we will add a new microservice named “ACA WebApp-Frontend” as illustrated in the architecture diagram.
The source code for this tutorial is available on GitHub. You can check the demo application too.
Setting up the Web App Frontend Project
Step 1: Create the Web App Project
Now we will add a new ASP.NET Core Web App project named “TasksTracker.WebPortal.Frontend.Ui”, Configuration will be as the image below, check “Enable Docker” as we are going to containerize the application and deploy it to ACR, make sure “Linux” is selected for the Docker OS setting.
Step 2: Add Models (DTO)
Add a new folder named “Tasks” under the existing folder named “Pages” and then add a new folder named “Models”, then create a new file named “TaskModel.cs” and paste 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 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models { public class TaskModel { public Guid TaskId { get; set; } public string TaskName { get; set; } = string.Empty; public string TaskCreatedBy { get; set; } = string.Empty; public DateTime TaskCreatedOn { get; set; } public DateTime TaskDueDate { get; set; } public string TaskAssignedTo { get; set; } = string.Empty; public bool IsCompleted { get; set; } public bool IsOverDue { get; set; } } public class TaskAddModel { [Display(Name = "Task Name")] [Required] public string TaskName { get; set; } = string.Empty; [Display(Name = "Task DueDate")] [Required] public DateTime TaskDueDate { get; set; } [Display(Name = "Assigned To")] [Required] public string TaskAssignedTo { get; set; } = string.Empty; public string TaskCreatedBy { get; set; } = string.Empty; } public class TaskUpdateModel { public Guid TaskId { get; set; } [Display(Name = "Task Name")] [Required] public string TaskName { get; set; } = string.Empty; [Display(Name = "Task DueDate")] [Required] public DateTime TaskDueDate { get; set; } [Display(Name = "Assigned To")] [Required] public string TaskAssignedTo { get; set; } = string.Empty; } } |
Step 3: Add Razor pages for CRUD operations
Next, we will add 3 pages responsible for listing all the tasks, creating a new task, and updating existing tasks.
So add a new empty Razor Page named “Index.cshtml” under the “Tasks” folder and copy the cshtml content from this link.
By looking at the cshtml content notice that the page is expecting a query string named “createdBy” which will be used to group tasks for application users, this technique is subject to change once we enable Authentication in future posts, we will get the user email from the claims identity of the authenticated users.
Now we will add the code behind the “Index.cshtml” file, so open the file named “Index.cshtml.cs” and paste 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 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks { public class IndexModel : PageModel { private readonly IHttpClientFactory _httpClientFactory; public List<TaskModel>? TasksList { get; set; } [BindProperty] public string? TasksCreatedBy { get; set; } public IndexModel(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task OnGetAsync() { TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"]; // direct svc to svc http request var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); TasksList = await httpClient.GetFromJsonAsync<List<TaskModel>>($"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}"); 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); return RedirectToPage(); } } } |
What we have done is straightforward, we’ve injected named HttpClientFactory which is responsible to call the Backend API service as HTTP request. The index page supports deleting and marking tasks as completed along with listing tasks for certain users based on the “createdBy” property stored in a cookie named “TasksCreatedByCookie”, more about we are filling this property later in the post.
Now we will add a new Razor page named “Create.cshtml” under the “Tasks” folder and copy the cshtml content from this link.
Next, we will add the code behind the “Create.cshtml” file, so open the file named “Create.cshtml.cs” and paste 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 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks { public class CreateModel : PageModel { private readonly IHttpClientFactory _httpClientFactory; public CreateModel(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public IActionResult OnGet() { return Page(); } [BindProperty] public TaskAddModel TaskAdd { get; set; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } if (TaskAdd != null) { var createdBy = Request.Cookies["TasksCreatedByCookie"]; TaskAdd.TaskCreatedBy = createdBy; // direct svc to svc http request var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); var result = await httpClient.PostAsJsonAsync("api/tasks/", TaskAdd); } return RedirectToPage("./Index"); } } } |
The code is self-explanatory here, we just injected the type HttpClientFactory in order to issue a POST request and create a new task.
Lastly, we will add a new Razor page named “Edit.cshtml” under the “Tasks” folder and copy the cshtml content from this link.
Add the code behind to the “Edit.cshtml” file, so open the file named “Edit.cshtml.cs” and paste 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 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks { public class EditModel : PageModel { private readonly IHttpClientFactory _httpClientFactory; [BindProperty] public TaskUpdateModel? TaskUpdate { get; set; } public EditModel(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<IActionResult> OnGetAsync(Guid? id) { if (id == null) { return NotFound(); } // direct svc to svc http request var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); var Task = await httpClient.GetFromJsonAsync<TaskModel>($"api/tasks/{id}"); if (Task == null) { return NotFound(); } TaskUpdate = new TaskUpdateModel() { TaskId = Task.TaskId, TaskName = Task.TaskName, TaskAssignedTo = Task.TaskAssignedTo, TaskDueDate = Task.TaskDueDate, }; return Page(); } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } if (TaskUpdate != null) { // direct svc to svc http request var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal"); var result = await httpClient.PutAsJsonAsync($"api/tasks/{TaskUpdate.TaskId}", TaskUpdate); } return RedirectToPage("./Index"); } } } |
The code added is similar to the create operation, the Edit page accepts the TaskId as a Guid, loads the task, then updates the task by sending HTTP PUT operation.
Step 4: Inject HTTP Client Factory and Define Environment Variables
Now we will register the HttpClientFactory named “BackEndApiExternal” to make it available for injection in controllers, so open file “Program.cs” and paste 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 |
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(); builder.Services.AddHttpClient("BackEndApiExternal", httpClient => { httpClient.BaseAddress = new Uri(builder.Configuration.GetValue<string>("BackendApiConfig:BaseUrlExternalHttp")); }); var app = builder.Build(); // Code removed for brevity } } } |
Next, we will add a new environment variable named “BackendApiConfig:BaseUrlExternalHttp” into “appsettings.json” file, this variable will contain the Base URL for the backend API. Later on in this post, we will see how we can set the environment variable once we deploy it to Azure Container App
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "BackendApiConfig": { "BaseUrlExternalHttp": "https://tasksmanager-backend-api.agreeablestone-8c14c04c.eastus.azurecontainerapps.io" } } |
Step 5: Update the Web App landing page
Lastly, we will update the page “Index.html” to capture the email of the tasks owner user and assign this email to a cookie named “TasksCreatedByCookie”, Navigate to the page named “Pages\Index.csthml” and replace the HTML with the one in this link, then update code behind of “Index.csthml.cs” by pasting 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 |
namespace TasksTracker.WebPortal.Frontend.Ui.Pages { [IgnoreAntiforgeryToken(Order = 1001)] public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; [BindProperty] public string TasksCreatedBy { get; set; } public IndexModel(ILogger<IndexModel> logger) { _logger = logger; } public void OnGet() { } public IActionResult OnPost() { if (!string.IsNullOrEmpty(TasksCreatedBy)) { Response.Cookies.Append("TasksCreatedByCookie", TasksCreatedBy); } return RedirectToPage("./Tasks/Index"); } } } |
Step 6: Add Dockerfile to the Frontend Web App project
In case you didn’t check the “Enable Docker” checkbox when creating the project we need to add a Dockerfile to the Web App project, so ensure you are on the root of the project and add file named “Docekrfile” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["TasksTracker.WebPortal.Frontend.Ui/TasksTracker.WebPortal.Frontend.Ui.csproj", "TasksTracker.WebPortal.Frontend.Ui/"] RUN dotnet restore "TasksTracker.WebPortal.Frontend.Ui/TasksTracker.WebPortal.Frontend.Ui.csproj" COPY . . WORKDIR "/src/TasksTracker.WebPortal.Frontend.Ui" RUN dotnet build "TasksTracker.WebPortal.Frontend.Ui.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "TasksTracker.WebPortal.Frontend.Ui.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "TasksTracker.WebPortal.Frontend.Ui.dll"] |
With this last bit of code added to the Frontend Web App, you can run the Web app locally and try the different tasks management operations, if all is working fine you should be able to list all tasks created under the email you provided, update them, delete them, and mark them as completed. The web App will look similar to the below image
Deploy the Web App Frontend Project to Azure Container Apps
Step 1: Build and Push Web App Frontend Docker image to ACR
I will assume that you still have the same PowerShell console session opened from the last post, we need to add the below PS variables:
1 |
$FRONTEND_WEBAPP_NAME="tasksmanager-frontend-webapp" |
Now we will build the Web App project on ACR and push the docker image to ACR. Use the below command to initiate the image build and push process using ACR. The “.” at the end of the command represents the docker build context, in our case, we need to be on the parent directory which hosts the .csproject.
1 2 |
cd {YourLocalPath}\TasksTracker.ContainerApps az acr build --registry $ACR_NAME --image "tasksmanager/$FRONTEND_WEBAPP_NAME" --file 'TasksTracker.WebPortal.Frontend.Ui/Dockerfile' . |
Once this step is completed you can verify the results by going to the Azure portal and checking that a new repository named “tasksmanager/tasksmanager-frontend-webapp” has been created and there is a new docker image with a “latest” tag is created, it should look like the below
Step 3: Provision Azure Container App to host Web App
Now we will create and deploy the Web App to Azure Container App following the below command:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
az containerapp create ` --name "$FRONTEND_WEBAPP_NAME" ` --resource-group $RESOURCE_GROUP ` --environment $ENVIRONMENT ` --image "$ACR_NAME.azurecr.io/tasksmanager/$FRONTEND_WEBAPP_NAME" ` --registry-server "$ACR_NAME.azurecr.io" ` --env-vars BackendApiConfig_BaseUrlExternalHttp=https://tasksmanager-backend-api.agreeablestone-8c14c04c.eastus.azurecontainerapps.io/ ` --target-port 80 ` --ingress 'external' ` --min-replicas 1 ` --max-replicas 2 ` --cpu 0.25 --memory 0.5Gi ` --query configuration.ingress.fqdn |
Notice how we used the property “env-vars” to set the value of the environment variable named “BackendApiConfig_BaseUrlExternalHttp” which we added in the AppSettings.json file. You can set multiple environment variables at the same time by using a space between each variable
The ‘ingress’ property is set to external as the Web frontend App will be exposed to the public internet for users.
Now copy the FQDN (Application URL) of the Azure container app named “tasksmanager-frontend-webapp” and open it in your browser and you should be able to browse the frontend web app and manage your tasks.
Step 4: Update Backend Web API Container App Ingress property
So far the Frontend App is sending HTTP requests to publically exposed Web API, any REST client can invoke this API, we need to change the Web API ingress settings and make it only accessible for applications deployed within our Azure Container Environment only, any application outside the Azure Container Environment will not be able to access the Web API. You can check the ingress settings of the Backend API from the Azure portal, it will look like the below image
To change the settings of the Backend API, fire the below CLI command
1 2 3 4 5 |
az containerapp ingress enable ` --name $BACKEND_API_NAME ` --resource-group $RESOURCE_GROUP ` --target-port 80 ` --type "internal" |
After you run this command you can check ingress settings in Portal and “Limited to Container Apps Environment” will be selected.
When you do this change, the FQDN (Application URL) will change and it will be similar to the below, notice how there is an “Internal” part of the URL.
https://tasksmanager-backend-api.internal.agreeablestone-8c14c04c.eastus.azurecontainerapps.io/api/tasks/?createdby=tjoudeh@bitoftech.net
If you try to invoke the URL from the browser directly it will return 404 as this Internal Url can only be accessed from container apps within the container environment.
The FQDN consists of multiple parts, for example, all our Container Apps we are going to create will be under the Environment unique identifier “agreeablestone-8c14c04c” and the Container App will vary based on the name provided, check the image below for a better explanation.
Step 5: Update the Frontend Web App environment variable to point to the internal Web API FQDN
The last thing we need to do here is to update the Frontend WebApp environment variable named “BackendApiConfig_BaseUrlExternalHttp” with the new value of the internal Backend Web API base URL, to do so we need to update the Web App container app and it will create a new revision implicitly (More about revisions in the upcoming posts) to do so, execute the below command:
1 2 3 4 |
az containerapp update ` --name "$FRONTEND_WEBAPP_NAME" ` --resource-group $RESOURCE_GROUP ` --set-env-vars BackendApiConfig__BaseUrlExternalHttp=https://tasksmanager-backend-api.internal.agreeablestone-8c14c04c.eastus.azurecontainerapps.io ` |
Browse the web app again and you should be able to see the same results and access the backend API endpoints from the Web App
In the next post, we will start integrating Dapr and use the service to service Building block for services discovery and invocation.
Hi!
Amazing article, just this link is wrong “So add a new empty Razor Page named “Index.cshtml” under the “Tasks” folder and copy the cshtml content from this link.”
Thanks Rodrigo for your feedback, link is updated now