Skip to content

Module 1 - Deploy Backend API to ACA

In this module, we will start by creating the first microservice named ACA Web API – Backend as illustrated in the architecture diagram. Followed by that we will provision the Azure resources needed to deploy the service to Azure Container Apps using the Azure CLI.

1. Create the backend API project (Web API)

  • Open a command-line terminal and create a folder for your project. Use the code command to launch Visual Studio Code from that directory as shown:

    mkdir TasksTracker.ContainerApps
    cd TasksTracker.ContainerApps
    code .
    

  • From VS Code Terminal tab, open developer command prompt or PowerShell terminal in the project folder TasksTracker.ContainerApps and initialize the project. This will create and ASP.NET Web API project scaffolded with a single controller.

    dotnet new webapi -o TasksManager.Backend.Api
    

  • Next we need to containerize this application, so we can push it to Azure Container Registry as a docker image then deploy it to Azure Container Apps. Start by opening 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 pass it later on as the --target-port for the az containerapp create command.
    • Dockerfile and .dockerignore files are added to the workspace.
  • 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;
        }
    }
  • Create new folder named Services (make sure it is created at the same level as the models folder and not inside the models folder itself) and add new files as shown below. Add the Fake Tasks Manager service (In-memory), this will be the interface of Tasks Manager service. We will work initially with data in memory to keep things simple with very limited dependency on any other components or data store and focus on the deployment of the backend API to ACA. In the upcoming modules we will switch this implementation with a concrete data store where we are going to store data in Redis and Azure Cosmos DB using Dapr State Store building block
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
        {
            private 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 is self-explanatory, it generates 10 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 statements 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>();
// Code removed for brevity
app.Run();
  • Add a new controller under the Controllers folder with name below. 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);
                if (task != null)
                {
                    return Ok(task);
                }
                return 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);
                if (updated)
                {
                    return Ok();
                }
                return BadRequest();
            }

            [HttpPut("{taskId}/markcomplete")]
            public async Task<IActionResult> MarkComplete(Guid taskId)
            {
                var updated = await _tasksManager.MarkTaskCompleted(taskId);
                if (updated)
                {
                    return Ok();
                }
                return BadRequest();
            }

            [HttpDelete("{taskId}")]
            public async Task<IActionResult> Delete(Guid taskId)
            {
                var deleted = await _tasksManager.DeleteTask(taskId);
                if (deleted)
                {
                    return Ok();
                }
                return 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 {YourLocalPath}\TasksTracker.TasksManager.Backend.Api
    dotnet build
    
    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.

2. Deploy Web API Backend Project to ACA

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

  • We will start with Installing/Upgrading the Azure Container Apps Extension.

    # Upgrade Azure CLI
    az upgrade
    # Login to Azure
    az login 
    # Only required if you have multiple subscriptions
    az account set --subscription <name or id>
    # Install/Upgrade Azure Container Apps Extension
    az extension add --name containerapp --upgrade
    

  • Define the variables below in the PowerShell console to use them across the different modules in the workshop. You should change the values of those variables to be able to create the resources successfully. Some of those variables should be unique across all Azure subscriptions such as Azure Container Registry name. Remember to replace the place holders with your own values:

    $RESOURCE_GROUP="tasks-tracker-rg"
    $LOCATION="eastus"
    $ENVIRONMENT="tasks-tracker-containerapps-env"
    $WORKSPACE_NAME="<replace this with your unique app log analytics workspace name>"
    $APPINSIGHTS_NAME="<replace this with your unique app insights name>"
    $BACKEND_API_NAME="tasksmanager-backend-api"
    $ACR_NAME="<replace this with your unique acr name>"
    

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

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

  • 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 `
    --resource-group $RESOURCE_GROUP `
    --name $ACR_NAME `
    --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.

  • 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. To create it, run the command below:
    # create the log analytics workspace
    az monitor log-analytics workspace create `
    --resource-group $RESOURCE_GROUP `
    --workspace-name $WORKSPACE_NAME
    
    # retrieve workspace ID
    $WORKSPACE_ID=az monitor log-analytics workspace show --query customerId `
    -g $RESOURCE_GROUP `
    -n $WORKSPACE_NAME -o tsv
    
    # retrieve workspace secret
    $WORKSPACE_SECRET=az monitor log-analytics workspace get-shared-keys --query primarySharedKey `
    -g $RESOURCE_GROUP `
    -n $WORKSPACE_NAME -o 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:

    # Install the application-insights extension for the CLI
    az extension add -n application-insights
    
    # Create application-insights instance
    az monitor app-insights component create `
    -g $RESOURCE_GROUP `
    -l $LOCATION `
    --app $APPINSIGHTS_NAME `
    --workspace $WORKSPACE_NAME
    
    # Get Application Insights Instrumentation Key
    $APPINSIGHTS_INSTRUMENTATIONKEY=($(az monitor app-insights component show `
    --app $APPINSIGHTS_NAME `
    -g $RESOURCE_GROUP)  | ConvertFrom-Json).instrumentationKey
    

  • Now we will create an Azure Container Apps Environment. As a reminder of the different ACA component check 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. To create it, run the below command:

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

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.

  • 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.

    cd {YourLocalPath}\TasksTracker.ContainerApps
    az acr build --registry $ACR_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 there is a new docker image with a latest tag is created.

  • The last step here is to create and deploy the Web API to ACA following the below command. Remember to replace the place holders with your own values:

    az containerapp create `
    --name $BACKEND_API_NAME  `
    --resource-group $RESOURCE_GROUP `
    --environment $ENVIRONMENT `
    --image "$ACR_NAME.azurecr.io/tasksmanager/$BACKEND_API_NAME" `
    --registry-server "$ACR_NAME.azurecr.io" `
    --target-port [port number that was generated when you created your docker file in vs code] `
    --ingress 'external' `
    --min-replicas 1 `
    --max-replicas 1 `
    --cpu 0.25 --memory 0.5Gi `
    --query configuration.ingress.fqdn
    
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 Azure Portal and selecting the resource group named tasks-tracker-rg that you created earlier. You should see the 5 recourses created below. Azure Resources

Success

To test the backend api service, copy the FQDN (Application URL) of the Azure container app named tasksmanager-backend-api. 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.

Tip

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

Web API Response

In the next module, we will see how we will add a new Frontend Web App as a microservice and how it will communicate with the backend API.


Last update: 2023-03-26