Skip to content

Module 1 - Deploy Backend API to ACA

Module Duration

60 minutes

Prerequisities

Please ensure that all prerequisites have been taken care of prior to continuing.

Objective

In this module, we will accomplish three objectives:

  1. Create the first microservice, ACA API - Backend, which serves as the API for our tasks.
  2. Create the initial Azure infrastructure that we will need throughout this workshop.
  3. Deploy the `ACA API - Backend container app to Azure.

Module Sections

1. Create the backend API project (Web API)

  • From VS Code's Terminal tab, select New Terminal to open a (PowerShell) terminal in the project folder TasksTracker.ContainerApps (also referred to as root).

  • We need to define the .NET version we will use throughout this workshop. In the terminal execute dotnet --info. Take note of the intalled .NET SDK versions and select the one with which you wish to proceed.

  • In the root folder create a new file and set the .NET SDK version from the above command:

    {
        "sdk": {
            "version": "8.0.100",
            "rollForward": "latestFeature"
        }
    }
    
  • Now we can initialize the backend API project. This will create and ASP.NET Web API project scaffolded with a single controller.

    Controller-Based vs. Minimal APIs

    APIs can be created via the traditional, expanded controller-based structure with Controllers and Models folders, etc. or via the newer minimal APIs approach where controller actions are written inside Program.cs. The latter approach is preferential in a microservices project where the endpoints are overseeable and may easily be represented by a more compact view.

    As our workshop takes advantage of microservices, the use case for minimal APIs is given. However, in order to make the workshop a bit more demonstrable, we will, for now, stick with controller-based APIs.

    dotnet new webapi -o TasksTracker.TasksManager.Backend.Api
    
    dotnet new webapi --use-controllers -o TasksTracker.TasksManager.Backend.Api
    
  • Delete the boilerplate WeatherForecast.cs and Controllers\WeatherForecastController.cs files from the new TasksTracker.TasksManager.Backend.Api project folder.

  • We need to containerize this application, so we can push it to the Azure Container Registry before we deploy it to Azure Container Apps:

    • 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 the application platform.
    • Choose the newly-created project, if prompted.
    • Choose Linux when prompted to choose the operating system.
    • Set the application port to 5000. This is arbitrary but memorable for this workshop.
    • You will be asked if you want to add Docker Compose files. Select No.
    • Dockerfile and .dockerignore files are added to the workspace.
    • Open Dockerfile and replace
      FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build with
      FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

      Dockerfile Build Platform

      Azure Container Registry does not set $BUILDPLATFORM presently when building containers. This consequently causes the build to fail. See this issue for details. Therefore, we remove it from the file for the time being. We expect this to be corrected in the future.

  • In the project root add a new folder named Models and create a new file with name below. These are the DTOs that will be used across the projects.

    namespace TasksTracker.TasksManager.Backend.Api.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
        {
            public string TaskName { get; set; } = string.Empty;
            public string TaskCreatedBy { get; set; } = string.Empty;
            public DateTime TaskDueDate { get; set; }
            public string TaskAssignedTo { get; set; } = string.Empty;
        }
    
        public class TaskUpdateModel
        {
            public Guid TaskId { get; set; }
            public string TaskName { get; set; } = string.Empty;
            public DateTime TaskDueDate { get; set; }
            public string TaskAssignedTo { get; set; } = string.Empty;
        }
    }
    
  • In the project root create a new folder named Services and add the two files below. Ensure to create it as a sibling to the Models folder. Add the Fake Tasks Manager service. This will be the interface of Tasks Manager service. In this module we will work with data in memory. Later on, we will implement a data store.

    using TasksTracker.TasksManager.Backend.Api.Models;
    
    namespace TasksTracker.TasksManager.Backend.Api.Services
    {
        public interface ITasksManager
        {
            Task<List<TaskModel>> GetTasksByCreator(string createdBy);
            Task<TaskModel?> GetTaskById(Guid taskId);
            Task<Guid> CreateNewTask(string taskName, string createdBy, string assignedTo, DateTime dueDate);
            Task<bool> UpdateTask(Guid taskId, string taskName, string assignedTo, DateTime dueDate);
            Task<bool> MarkTaskCompleted(Guid taskId);
            Task<bool> DeleteTask(Guid taskId);
        }
    }
    
    using TasksTracker.TasksManager.Backend.Api.Models;
    
    namespace TasksTracker.TasksManager.Backend.Api.Services
    {
        public class FakeTasksManager : ITasksManager
        {
            List<TaskModel> _tasksList = new List<TaskModel>();
            Random rnd = new Random();
    
            private void GenerateRandomTasks()
            {
                for (int i = 0; i < 10; i++)
                {
                    var task = new TaskModel()
                    {
                        TaskId = Guid.NewGuid(),
                        TaskName = $"Task number: {i}",
                        TaskCreatedBy = "tjoudeh@bitoftech.net",
                        TaskCreatedOn = DateTime.UtcNow.AddMinutes(i),
                        TaskDueDate = DateTime.UtcNow.AddDays(i),
                        TaskAssignedTo = $"assignee{rnd.Next(50)}@mail.com",
                    };
                    _tasksList.Add(task);
                }
            }
    
            public FakeTasksManager()
            {
                GenerateRandomTasks();
            }
    
            public Task<Guid> CreateNewTask(string taskName, string createdBy, string assignedTo, DateTime dueDate)
            {
                var task = new TaskModel()
                {
                    TaskId = Guid.NewGuid(),
                    TaskName = taskName,
                    TaskCreatedBy = createdBy,
                    TaskCreatedOn = DateTime.UtcNow,
                    TaskDueDate = dueDate,
                    TaskAssignedTo = assignedTo,
                };
    
                _tasksList.Add(task);
    
                return Task.FromResult(task.TaskId);
            }
    
            public Task<bool> DeleteTask(Guid taskId)
            {
                var task = _tasksList.FirstOrDefault(t => t.TaskId.Equals(taskId));
    
                if (task != null)
                {
                    _tasksList.Remove(task);
                    return Task.FromResult(true);
                }
    
                return Task.FromResult(false);
            }
    
            public Task<TaskModel?> GetTaskById(Guid taskId)
            {
                var taskModel = _tasksList.FirstOrDefault(t => t.TaskId.Equals(taskId));
                return Task.FromResult(taskModel);
            }
    
            public Task<List<TaskModel>> GetTasksByCreator(string createdBy)
            {
                var tasksList = _tasksList.Where(t => t.TaskCreatedBy.Equals(createdBy)).OrderByDescending(o => o.TaskCreatedOn).ToList();
                return Task.FromResult(tasksList);
            }
    
            public Task<bool> MarkTaskCompleted(Guid taskId)
            {
                var task = _tasksList.FirstOrDefault(t => t.TaskId.Equals(taskId));
    
                if (task != null)
                {
                    task.IsCompleted = true;
                    return Task.FromResult(true);
                }
    
                return Task.FromResult(false);
            }
    
            public Task<bool> UpdateTask(Guid taskId, string taskName, string assignedTo, DateTime dueDate)
            {
                var task = _tasksList.FirstOrDefault(t => t.TaskId.Equals(taskId));
    
                if (task != null)
                {
                    task.TaskName = taskName;
                    task.TaskAssignedTo = assignedTo;
                    task.TaskDueDate = dueDate;
                    return Task.FromResult(true);
                }
    
                return Task.FromResult(false);
            }
        }
    }
    
  • The code above generates ten tasks and stores them in a list in memory. It also has some operations to add/remove/update those tasks.

  • Now we need to register FakeTasksManager on project startup. Open file 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.

    using TasksTracker.TasksManager.Backend.Api.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddSingleton<ITasksManager, FakeTasksManager>();
    builder.Services.AddControllers();
    // 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.MapControllers();
    
    app.Run();
    
  • Inside the Controllers folder create a new controller with the below filename. We need to create API endpoints to manage tasks.

    using Microsoft.AspNetCore.Mvc;
    using TasksTracker.TasksManager.Backend.Api.Models;
    using TasksTracker.TasksManager.Backend.Api.Services;
    
    namespace TasksTracker.TasksManager.Backend.Api.Controllers
    {
        [Route("api/tasks")]
        [ApiController]
        public class TasksController : ControllerBase
        {
            private readonly ILogger<TasksController> _logger;
            private readonly ITasksManager _tasksManager;
    
            public TasksController(ILogger<TasksController> logger, ITasksManager tasksManager)
            {
                _logger = logger;
                _tasksManager = tasksManager;
            }
    
            [HttpGet]
            public async Task<IEnumerable<TaskModel>> Get(string createdBy)
            {
                return await _tasksManager.GetTasksByCreator(createdBy);
            }
    
            [HttpGet("{taskId}")]
            public async Task<IActionResult> GetTask(Guid taskId)
            {
                var task = await _tasksManager.GetTaskById(taskId);
    
                return (task != null) ? Ok(task) : NotFound();
            }
    
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] TaskAddModel taskAddModel)
            {
                var taskId = await _tasksManager.CreateNewTask(
                    taskAddModel.TaskName,
                    taskAddModel.TaskCreatedBy,
                    taskAddModel.TaskAssignedTo,
                    taskAddModel.TaskDueDate
                );
    
                return Created($"/api/tasks/{taskId}", null);
    
            }
    
            [HttpPut("{taskId}")]
            public async Task<IActionResult> Put(Guid taskId, [FromBody] TaskUpdateModel taskUpdateModel)
            {
                var updated = await _tasksManager.UpdateTask(
                    taskId,
                    taskUpdateModel.TaskName,
                    taskUpdateModel.TaskAssignedTo,
                    taskUpdateModel.TaskDueDate
                );
    
                return updated ? Ok() : BadRequest();
            }
    
            [HttpPut("{taskId}/markcomplete")]
            public async Task<IActionResult> MarkComplete(Guid taskId)
            {
                var updated = await _tasksManager.MarkTaskCompleted(taskId);
    
                return updated ? Ok() : BadRequest();
            }
    
            [HttpDelete("{taskId}")]
            public async Task<IActionResult> Delete(Guid taskId)
            {
                var deleted = await _tasksManager.DeleteTask(taskId);
    
                return deleted ? Ok() : NotFound();
            }
        }
    }
    
  • 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.

    cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api
    dotnet build
    

Note

Throughout the documentation, we will use the the tilde character [~] to represent the base / parent folder where you chose to install the workshop assets.

Make sure that the build is successful and that there are no build errors. Usually you should see a "Build succeeded" message in the terminal upon a successful build.

  • Navigate to the root and persist the module to Git.

    git add .
    git commit -m "Add Module 1"
    

2. Create Azure Infrastructure

2.1 Define the Basics

We will be using Azure CLI to deploy the Web API Backend to ACA as shown in the following steps:

  • First, we need to ensure that our CLI is updated. Then we log in to Azure.

    # Upgrade the Azure CLI
    az upgrade
    
    # Install/upgrade the Azure Container Apps & Application Insights extensions
    az extension add --upgrade --name containerapp
    az extension add --upgrade --name application-insights
    
    # Log in to Azure
    az login 
    
  • You may be able to use the queried Azure subscription ID or you may need to set it manually depending on your setup.

    # Retrieve the currently active Azure subscription ID
    $AZURE_SUBSCRIPTION_ID = az account show --query id --output tsv
    
    # Set a specific Azure Subscription ID (if you have multiple subscriptions)
    # $AZURE_SUBSCRIPTION_ID = "<Your Azure Subscription ID>" # Your Azure Subscription id which you can find on the Azure portal
    # az account set --subscription $AZURE_SUBSCRIPTION_ID
    
    echo $AZURE_SUBSCRIPTION_ID
    
  • Execute the variables below in the PowerShell console to use them across the different modules in the workshop. Some of these variables must be globally unique, which we attempt by using $RANDOM_STRING:

    # Create a random, 6-digit, Azure safe string
    $RANDOM_STRING=-join ((97..122) + (48..57) | Get-Random -Count 6 | ForEach-Object { [char]$_})
    $RESOURCE_GROUP="rg-tasks-tracker-$RANDOM_STRING"
    $LOCATION="eastus"
    $ENVIRONMENT="cae-tasks-tracker"
    $WORKSPACE_NAME="log-tasks-tracker-$RANDOM_STRING"
    $APPINSIGHTS_NAME="appi-tasks-tracker-$RANDOM_STRING"
    $BACKEND_API_NAME="tasksmanager-backend-api"
    $AZURE_CONTAINER_REGISTRY_NAME="crtaskstracker$RANDOM_STRING"
    $VNET_NAME="vnet-tasks-tracker"
    $TARGET_PORT=5000
    
Cloud Adoption Framework Abbreviations

Unless you have your own naming convention, we suggest to use Cloud Adoption Framework (CAF) abbreviations for resource prefixes.

  • Create a resource group to organize the services related to the application, run the below command:

    az group create `
    --name $RESOURCE_GROUP `
    --location "$LOCATION"
    

2.2 Create Network Infrastructure

Note

We are keeping this implementation simple. A production workload should have Network Security Groups and a firewall.

  • We need to create a virtual network (VNet) to secure our container apps. Note that while the VNet size with /16 CIDR is arbitrary, the container app subnet must have at least a /27 CIDR.

    az network vnet create `
    --name $VNET_NAME `
    --resource-group $RESOURCE_GROUP `
    --address-prefix 10.0.0.0/16 `
    --subnet-name ContainerAppSubnet `
    --subnet-prefix 10.0.0.0/27
    
  • Azure Container Apps requires management of the subnet, so we must delegate exclusive control.

    az network vnet subnet update `
    --name ContainerAppSubnet `
    --resource-group $RESOURCE_GROUP `
    --vnet-name $VNET_NAME `
    --delegations Microsoft.App/environments
    
  • Retrieve the Azure Container App subnet resource ID as it will be referenced when the Azure Container App Environment is created later.

    $ACA_ENVIRONMENT_SUBNET_ID=$(az network vnet subnet show `
    --name ContainerAppSubnet `
    --resource-group $RESOURCE_GROUP `
    --vnet-name $VNET_NAME `
    --query id `
    --output tsv)
    

2.3 Create Log Analytics workspace & Application Insights

  • Create an Azure Log Analytics workspace which will provide a common place to store the system and application log data from all container apps running in the environment. Each environment should have its own Log Analytics workspace.

    # Create the Log Analytics workspace
    az monitor log-analytics workspace create `
    --resource-group $RESOURCE_GROUP `
    --workspace-name $WORKSPACE_NAME
    
    # Retrieve the Log Analytics workspace ID
    $WORKSPACE_ID=az monitor log-analytics workspace show `
    --resource-group $RESOURCE_GROUP `
    --workspace-name $WORKSPACE_NAME `
    --query customerId `
    --output tsv
    
    # Retrieve the Log Analytics workspace secret
    $WORKSPACE_SECRET=az monitor log-analytics workspace get-shared-keys `
    --resource-group $RESOURCE_GROUP `
    --workspace-name $WORKSPACE_NAME `
    --query primarySharedKey `
    --output tsv
    
  • Create an Application Insights instance which will be used mainly for distributed tracing between different container apps within the ACA environment to provide searching for and visualizing an end-to-end flow of a given execution or transaction. To create it, run the command below:

    # Create Application Insights instance
    az monitor app-insights component create `
    --resource-group $RESOURCE_GROUP `
    --location $LOCATION `
    --app $APPINSIGHTS_NAME `
    --workspace $WORKSPACE_NAME
    
    # Get Application Insights Instrumentation Key
    $APPINSIGHTS_INSTRUMENTATIONKEY=($(az monitor app-insights component show `
    --resource-group $RESOURCE_GROUP `
    --app $APPINSIGHTS_NAME ) | ConvertFrom-Json).instrumentationKey
    

2.4 Azure Container Infrastructure

  • Create an Azure Container Registry (ACR) instance in the resource group to store images of all Microservices we are going to build during this workshop. Make sure that you set the admin-enabled flag to true in order to seamlessly authenticate the Azure container app when trying to create the container app using the image stored in ACR.

    az acr create `
    --name $AZURE_CONTAINER_REGISTRY_NAME `
    --resource-group $RESOURCE_GROUP `
    --sku Basic `
    --admin-enabled true
    

Note

Notice that we create the registry with admin rights --admin-enabled flag set to true which is not suited for real production, but good for our workshop.

  • Now we will create an Azure Container Apps Environment. As a reminder of the different ACA components, see this link in the workshop introduction. The ACA environment acts as a secure boundary around a group of container apps that we are going to provision during this workshop.

    # Create the ACA environment
    az containerapp env create `
    --name $ENVIRONMENT `
    --resource-group $RESOURCE_GROUP `
    --location $LOCATION `
    --logs-workspace-id $WORKSPACE_ID `
    --logs-workspace-key $WORKSPACE_SECRET `
    --dapr-instrumentation-key $APPINSIGHTS_INSTRUMENTATIONKEY `
    --enable-workload-profiles `
    --infrastructure-subnet-resource-id $ACA_ENVIRONMENT_SUBNET_ID
    

Note

We are not creating an internal-only Azure Container App Environment. This means that the static IP will be a public IP, and container apps, by default, will be publicly available on the internet.
While this is not advised in a production workload, it is suitable for the workshop to keep the architecture confined to Azure Container Apps.

Want to learn what above command does?
  • It creates an ACA environment and associates it with the Log Analytics workspace created in the previous step.
  • We are setting the --dapr-instrumentation-key value to the instrumentation key of the Application Insights instance. This will come handy when we introduce Dapr in later modules and show how the distributed tracing between microservices/container apps are captured and visualized in Application Insights.

    NOTE: You can set the --dapr-instrumentation-key after you create the ACA environment but this is not possible via the AZ CLI right now. There is an open issue which is being tracked by the product group.

3. Deploy Web API Backend Project to ACA

  • Build the Web API 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 .csproj.

    az acr build `
    --registry $AZURE_CONTAINER_REGISTRY_NAME `
    --image "tasksmanager/$BACKEND_API_NAME" `
    --file 'TasksTracker.TasksManager.Backend.Api/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-backend-api has been created, and that there is a new Docker image with a latest tag.

  • The last step here is to create and deploy the Web API to ACA following the below command:

    $fqdn=(az containerapp create `
    --name $BACKEND_API_NAME `
    --resource-group $RESOURCE_GROUP `
    --environment $ENVIRONMENT `
    --image "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io/tasksmanager/$BACKEND_API_NAME" `
    --registry-server "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io" `
    --target-port $TARGET_PORT `
    --ingress 'external' `
    --min-replicas 1 `
    --max-replicas 1 `
    --cpu 0.25 `
    --memory 0.5Gi `
    --query properties.configuration.ingress.fqdn `
    --output tsv)
    
    $BACKEND_API_EXTERNAL_BASE_URL="https://$fqdn"
    
    echo "See a listing of tasks created by the author at this URL:"
    echo "https://$fqdn/api/tasks/?createdby=tjoudeh@bitoftech.net"
    
Want to learn what above command does?
  • Ingress param is set to external which means that this container app (Web API) project will be accessible from the public internet. When Ingress is set to Internal or External it will be assigned a fully qualified domain name (FQDN). Important notes about IP addresses and domain names can be found here.
  • The target port param is set to 80, this is the port our Web API container listens to for incoming requests.
  • We didn't specify the ACR registry username and password, az containerapp create command was able to look up ACR username and password and add them as a secret under the created Azure container app for future container updates.
  • The minimum and the maximum number of replicas are set. More about this when we cover Autoscaling in later modules. For the time being, only a single instance of this container app will be provisioned as Auto scale is not configured.
  • We set the size of the Container App. The total amount of CPUs and memory requested for the container app must add up to certain combinations, for full details check the link here.
  • The query property will filter the response coming from the command and just return the FQDN. Take note of this FQDN as you will need it for the next step.

For full details on all available parameters for this command, please visit this page.

  • You can now verify the deployment of the first ACA by navigating to the link at the end of the above script or to the Azure portal and selecting the resource group named tasks-tracker-rg that you created earlier. You should see the 5 resourses created below. Azure Resources

Success

To test the backend api service, either click on the URL output by the last command or copy the FQDN (Application URL) of the Azure container app named tasksmanager-backend-api, then issue a GET request similar to this one: https://tasksmanager-backend-api.<your-aca-env-unique-id>.eastus.azurecontainerapps.io/api/tasks/?createdby=tjoudeh@bitoftech.net and you should receive an array of the 10 tasks similar to the below image.

Note that the specific query string matters as you may otherwise get an empty result back.

Tip

You can find your Azure container app application url on the Azure portal overview tab.

Web API Response

  • Execute the Set-Variables.ps1 in the root to update the variables.ps1 file with all current variables. The output of the script will inform you how many variables are written out.

    .\Set-Variables.ps1
    
  • From the root, persist a list of all current variables.

    git add .\Variables.ps1
    git commit -m "Update Variables.ps1"
    

Review

In this module, we have accomplished three objectives:

  1. Created the first microservice, ACA API - Backend, which serves as the API for our tasks.
  2. Created the initial Azure infrastructure that we will need throughout this workshop.
  3. Deployed the ACA API - Backend microservice to Azure.

In the next module, we will add a new frontend web app as a microservice to communicate with the backend API.