Skip to content

Module 2 - Communication Between Microservices in ACA

Module Duration

60 minutes

Objective

In this module, we will accomplish three objectives:

  1. Create a web app named ACA Web - Frontend, which is the UI to interact with ACA API - Backend.
  2. Deploy the ACA Web - Frontend container app to Azure.
  3. Shield ACA API - Backend from external access.

Module Sections

  • From the VS Code Terminal tab, open developer command prompt or PowerShell terminal in the project folder TasksTracker.ContainerApps (root):

    cd ~\TasksTracker.ContainerApps
    
  • Restore the previously-stored variables by executing the local script. The output informs you how many variables have been set.

    .\Variables.ps1
    

1. Create the Frontend Web App project

  • Initialize the web project. This will create and ASP.NET Razor Pages web app project.

    dotnet new webapp -o TasksTracker.WebPortal.Frontend.Ui
    
  • 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 8080, which is the default non-privileged port since .NET 8.
    • You will be asked if you want to add Docker Compose files. Select No.
    • Dockerfile and .dockerignore files are added to the project workspace.
    • Open Dockerfile and remove --platform=$BUILDPLATFORM from the FROM instruction.

      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.

  • From inside the Pages folder, add a new folder named Tasks. Within that folder, add a new folder named Models, then create file as shown below.

    using Microsoft.AspNetCore.Mvc;
    using System.ComponentModel.DataAnnotations;
    
    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;
        }
    }
    
  • Now, in the Tasks folder, we will add 3 Razor pages for CRUD operations which will be responsible for listing tasks, creating a new task, and updating existing tasks. 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.

    Note

    We are following this approach here to keep the workshop simple, but for production applications, authentication should be applied and the user email should be retrieved from the claims identity of the authenticated users.

    @page 
    @model TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.IndexModel
    @{
    }
    
    <h1>Tasks Manager</h1>
    @if (!String.IsNullOrEmpty(Model.TasksCreatedBy)) {
        <h4>Tasks for (@Model.TasksCreatedBy)</h4>
    }
    <form method="post">
        <table class="table">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Due Date</th>
                    <th>Assigned To</th>
                    <th>Completed</th>
                    <th>Overdue</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @if (Model.TasksList != null)
                {
                    foreach (var task in Model.TasksList)
                    {
                        <tr>
                            <td><a asp-page="./Edit" asp-route-id="@task.TaskId">@task.TaskName</a></td>
                            <td>@task.TaskDueDate.Date.ToString("dd-MM-yyyy")</td>
                            <td>@task.TaskAssignedTo</td>
                            <td>@Html.CheckBox("IsCompleted",@task.IsCompleted)</td>
                            <td>@Html.CheckBox("IsOverDue",@task.IsOverDue)</td>
                            <td>
                                <button type="submit" asp-page-handler="complete" asp-route-id="@task.TaskId">Complete</button>
                                <button type="submit" asp-page-handler="delete" asp-route-id="@task.TaskId">Delete</button>
                            </td>
                        </tr>
                    }
                }
            </tbody>
        </table>
        <a asp-page="Create">Create New</a>
    </form>
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    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<IActionResult> OnGetAsync()
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                if (!String.IsNullOrEmpty(TasksCreatedBy)) {
                    // direct svc to svc http request
                    var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal");
                    TasksList = await httpClient.GetFromJsonAsync<List<TaskModel>>($"api/tasks?createdBy={TasksCreatedBy}");
                    return Page();
                } else {
                    return RedirectToPage("../Index");
                }
            }
    
            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 does this code do?

    In the code above 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 populating this property later in the workshop.

    @page
    @model TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.CreateModel
    @{
    }
    
    <h1>Create Task</h1>
    
    <h4>Task</h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form method="post">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="TaskAdd!.TaskName" class="control-label"></label>
                    <input asp-for="TaskAdd!.TaskName" class="form-control" />
                    <span asp-validation-for="TaskAdd!.TaskName" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <label asp-for="TaskAdd!.TaskDueDate" class="control-label"></label>
                    <input asp-for="TaskAdd!.TaskDueDate" class="form-control" type="date" />
                    <span asp-validation-for="TaskAdd!.TaskDueDate" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <label asp-for="TaskAdd!.TaskAssignedTo" class="control-label"></label>
                    <input asp-for="TaskAdd!.TaskAssignedTo" class="form-control" type="email" />
                    <span asp-validation-for="TaskAdd!.TaskAssignedTo" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <input type="submit" value="Save" class="btn btn-primary" />
                </div>
            </form>
        </div>
    </div>
    
    <div>
        <a asp-page="./Index">Back to List</a>
    </div>
    
    @section Scripts {
        @{
            await Html.RenderPartialAsync("_ValidationScriptsPartial");
        }
    }
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks
    {
        public class CreateModel : PageModel
        {
            private readonly IHttpClientFactory _httpClientFactory;
            public CreateModel(IHttpClientFactory httpClientFactory)
            {
                _httpClientFactory = httpClientFactory;
            }
            public string? TasksCreatedBy { get; set; }
    
            public IActionResult OnGet()
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                return (!String.IsNullOrEmpty(TasksCreatedBy)) ? Page() : RedirectToPage("../Index");
            }
    
            [BindProperty]
            public TaskAddModel? TaskAdd { get; set; }
    
            public async Task<IActionResult> OnPostAsync()
            {
                if (!ModelState.IsValid)
                {
                    return Page();
                }
    
                if (TaskAdd != null)
                {
                    var createdBy = Request.Cookies["TasksCreatedByCookie"];
    
                    if (!string.IsNullOrEmpty(createdBy))
                    {
                        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");
            }
        }
    }
    

    What does this code do?

    The code is self-explanatory here. We just injected the type HttpClientFactory in order to issue a POST request and create a new task.

    @page "{id:guid}"
    @model TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.EditModel
    @{
        ViewData["Title"] = "Edit";
    }
    
    
    <h1>Edit Task</h1>
    
    <h4>Task</h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form method="post">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <input type="hidden" asp-for="TaskUpdate!.TaskId" />
                <div class="form-group">
                    <label asp-for="TaskUpdate!.TaskName" class="control-label"></label>
                    <input asp-for="TaskUpdate!.TaskName" class="form-control" />
                    <span asp-validation-for="TaskUpdate!.TaskName" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <label asp-for="TaskUpdate!.TaskDueDate" class="control-label"></label>
                    <input asp-for="TaskUpdate!.TaskDueDate" class="form-control" type="date" />
                    <span asp-validation-for="TaskUpdate!.TaskDueDate" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <label asp-for="TaskUpdate!.TaskAssignedTo" class="control-label"></label>
                    <input asp-for="TaskUpdate!.TaskAssignedTo" class="form-control" />
                    <span asp-validation-for="TaskUpdate!.TaskAssignedTo" class="text-danger"></span>
                </div>
    
                <div class="form-group">
                    <input type="submit" value="Save" class="btn btn-primary" />
                </div>
            </form>
        </div>
    </div>
    
    <div>
        <a asp-page="./Index">Back to List</a>
    </div>
    
    @section Scripts {
        @{
            await Html.RenderPartialAsync("_ValidationScriptsPartial");
        }
    }
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks
    {
        public class EditModel : PageModel
        {
            private readonly IHttpClientFactory _httpClientFactory;
    
            [BindProperty]
            public TaskUpdateModel? TaskUpdate { get; set; }
            public string? TasksCreatedBy { get; set; }
    
            public EditModel(IHttpClientFactory httpClientFactory)
            {
                _httpClientFactory = httpClientFactory;
            }
    
            public async Task<IActionResult> OnGetAsync(Guid? id)
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                if (String.IsNullOrEmpty(TasksCreatedBy)) {
                    return RedirectToPage("../Index");
                }
    
                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");
            }
        }
    }
    

    What does this code do?

    The code added is similar to the create operation. The Edit page accepts the TaskId as a Guid, loads the task, and then updates the task by sending an HTTP PUT operation.

  • Now we will inject an HTTP client factory and define environment variables. To do so, we will register the HttpClientFactory named BackEndApiExternal to make it available for injection in controllers. Open the Program.cs file and update it with highlighted code below. Your file may be flattened rather than indented and not contain some of the below elements. Don't worry. Just place the highlighted lines in the right spot:

    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
    builder.Services.AddHttpClient("BackEndApiExternal", httpClient =>
    {
        var backendApiBaseUrlExternalHttp = builder.Configuration.GetValue<string>("BackendApiConfig:BaseUrlExternalHttp");
    
        if (!string.IsNullOrEmpty(backendApiBaseUrlExternalHttp)) {
            httpClient.BaseAddress = new Uri(backendApiBaseUrlExternalHttp);
        } else {
            throw new("BackendApiConfig:BaseUrlExternalHttp is not defined in App Settings.");
        }
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
    builder.Services.AddHttpClient("BackEndApiExternal", httpClient =>
    {
        var backendApiBaseUrlExternalHttp = builder.Configuration.GetValue<string>("BackendApiConfig:BaseUrlExternalHttp");
    
        if (!string.IsNullOrEmpty(backendApiBaseUrlExternalHttp)) {
            httpClient.BaseAddress = new Uri(backendApiBaseUrlExternalHttp);
        } else {
            throw new("BackendApiConfig:BaseUrlExternalHttp is not defined in App Settings.");
        }
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapStaticAssets();
    app.MapRazorPages()
       .WithStaticAssets();
    
    app.Run();
    
  • 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 deployed in the previous module to ACA. Later on in the workshop, we will see how we can set the environment variable once we deploy it to ACA. Use the output from this script as the BaseUrlExternalHttp value.

    $BACKEND_API_EXTERNAL_BASE_URL
    
        {
          "Logging": {
            "LogLevel": {
              "Default": "Information",
              "Microsoft.AspNetCore": "Warning"
            }
          },
          "AllowedHosts": "*",
          "BackendApiConfig": {
            "BaseUrlExternalHttp": "url to your backend api goes here. You can find this on the Azure portal overview tab. Look for the Application url property there."
          }
        }
    
  • Lastly, we will update the web app landing page Index.html and Index.cshtml.cs inside Pages folder to capture the email of the tasks owner user and assign this email to a cookie named TasksCreatedByCookie.

    @page
    @model IndexModel
    @{
        ViewData["Title"] = "Tasks Tracker";
    }
    
    <form method="post">
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                    <div class="card border-0 shadow rounded-3 my-5">
                        <div class="card-body p-4 p-sm-5">
                            <h5 class="card-title text-center mb-5 fw-light fs-5">Welcome to Tasks Tracker!</h5>
                            <form>
                                <div class="form-floating mb-3">
                                    <input type="email" class="form-control" id="floatingInput"
                                        placeholder="name@example.com" asp-for="TasksCreatedBy" required>
                                    <label for="floatingInput">Email address</label>
                                </div>
    
                                <div class="d-grid">
                                    <input type="submit" class="btn btn-primary btn-login text-uppercase fw-bold"
                                        value="Load My Tasks" />
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </form>
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    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");
            }
        }
    }
    
  • From VS Code Terminal tab, open developer command prompt or PowerShell terminal and navigate to the frontend directory which hosts the .csproj project folder and build the project.

    cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui
    dotnet build
    

Note

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 Razor Pages Web App Frontend Project to ACA

  • We need to add the below PS variables:

    $FRONTEND_WEBAPP_NAME="tasksmanager-frontend-webapp"
    
  • Now we will build and push the Web App project 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.

    cd ~\TasksTracker.ContainerApps
    
    1
    2
    3
    4
    az acr build `
    --registry $AZURE_CONTAINER_REGISTRY_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 that a new docker image with a latest tag has been created.

  • Next, we will create and deploy the Web App to ACA using the following command:

    $fqdn=(az containerapp create `
    --name "$FRONTEND_WEBAPP_NAME"  `
    --resource-group $RESOURCE_GROUP `
    --environment $ENVIRONMENT `
    --image "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io/tasksmanager/$FRONTEND_WEBAPP_NAME" `
    --registry-server "$AZURE_CONTAINER_REGISTRY_NAME.azurecr.io" `
    --env-vars "BackendApiConfig__BaseUrlExternalHttp=$BACKEND_API_EXTERNAL_BASE_URL/" `
    --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)
    
    $FRONTEND_UI_BASE_URL="https://$fqdn"
    
    echo "See the frontend web app at this URL:"
    echo $FRONTEND_UI_BASE_URL
    

Tip

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.

After you run the command, 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.

3. Update Backend Web API Container App Ingress property

So far the Frontend App is sending HTTP requests to the publicly exposed Web API. This means that any REST client can invoke the Web API. We need to change the Web API ingress settings and make it accessible only by applications deployed within our Azure Container Environment. Any application outside the Azure Container Environment should not be able to access the Web API.

  • To change the settings of the Backend API, execute the following command:

    $fqdn=(az containerapp ingress enable `
    --name $BACKEND_API_NAME  `
    --resource-group $RESOURCE_GROUP `
    --target-port $TARGET_PORT `
    --type "internal" `
    --query fqdn `
    --output tsv)
    
    $BACKEND_API_INTERNAL_BASE_URL="https://$fqdn"
    
    echo "The internal backend API URL:"
    echo $BACKEND_API_INTERNAL_BASE_URL
    
Want to know more about the command?

When you do this change, the FQDN (Application URL) will change, and it will be similar to the one shown below. Notice how there is an Internal part of the URL. https://tasksmanager-backend-api.internal.[Environment unique identifier].eastus.azurecontainerapps.io/api/tasks/

If you try to invoke the URL directly it will return 403 - Forbidden as this internal Url can only be accessed successfully from container apps within the container environment. This means that while the API is not accessible, it still provides a clue that something exists at that URL. Ideally, we would want to see a 404 - Not Found. However, recall from module 1 that we did not set internal-only for simplicity's sake of the workshop. In a production scenario, this should be done with completely private networking to not reveal anything.

The FQDN consists of multiple parts. For example, all our Container Apps will be under a specific Environment unique identifier (e.g. agreeablestone-8c14c04c) and the Container App will vary based on the name provided, check the image below for a better explanation. Container Apps FQDN

  • Now we will need to update the Frontend Web App environment variable to point to the internal backend 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 modules). The following command will update the container app with the changes:

    1
    2
    3
    4
    az containerapp update `
    --name "$FRONTEND_WEBAPP_NAME" `
    --resource-group $RESOURCE_GROUP `
    --set-env-vars "BackendApiConfig__BaseUrlExternalHttp=$BACKEND_API_INTERNAL_BASE_URL/"
    

Success

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. You can obtain the frontend URL from executing this variable.

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

    git add .
    git commit -m "Add Module 2"
    
  • 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 a web app named ACA Web - Frontend, which is the UI to interact with ACA API - Backend.
  2. Deployed the ACA Web - Frontend container app to Azure.
  3. Shielded ACA API - Backend from external access.

In the next module, we will start integrating Dapr and use the service to service Building block for services discovery and invocation.