How to scale out web app base on custom metrics

Scenario: How to scale out web app base on custom metrics. For example: request, response time and etc.

Currently, Web App Linux only CPU and memory are available.

Here is the end to end steps/screenshots:

**Step 1. Use the portal to create an Azure AD application and service principal that can access resources

  1. Click “App registration”

  2. “New registration”

  3. Enter a Name and click Register

  4. In Overview page, you can find the Application Id and Tenant Id.

  5. Create a client secret.

  1. Copy the client secret in Azure Portal

For more information: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal  

**Step 2. Add app roles in your application and receive them in the token

  1. Go to your resource group and “Access Control” and “Add a role assignment”

  2. Select “Contributor” and select your app (Registered in Step1)

  3. Go to your App Service Plan

  4. Select “Reader” Role

  5. Publish your function app.

  6. Test Your Function App in Azure Portal

  7. Verify the Instance Count in Azure Portal

  8. Get the Function URI:

  1. Test it in browser.

  2. Verify it in Azure Portal

For more information: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps

**Step 3. Setup an alert to change capacity

  1. Create an alert and choose a custom metric

  2. Create a action group and action type “Webhook”, the function URI is from step2.8

  3. Save alert

  4. Simulate a User Load and verify the Instance Count:

  1. Instance Count changed to 5
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Rest;
using System.Linq;
 
namespace ScaleWebAppFunction
{
    public static class Function1
    {
        [FunctionName("ScaleWebApp")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
 
            int capacity;
 
            if (!string.IsNullOrEmpty(req.Query["capacity"]) && int.TryParse(req.Query["capacity"], out capacity))
            {
                log.LogInformation("You want to chagne the capacity to "+ capacity.ToString());
                // Step1. How to: Use the portal to create an Azure AD application and service principal that can access resources
                //      https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal
 
                // Step2. How to: Add app roles in your application and receive them in the token
                //      https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps
                // For security concerns, I only give the resource group => contributor
                //                          and the service plan read permission
 
                var subscriptionId = "Your subscription id";
                var appId = "e6f6d231-Your-App_id";
                var secretKey = "2z]pj0vy2JRapjo:R@TJSoW1AmOCC=o8";
                var tenantId = "72f988bf-Your-Tenant-Id";
                var resourceGroup = "jackywebl1rg";
                var servicePlanName = "ASP-jackywebl1rg-81ee";
 
 
                // Step 3. change capacity
                var context = new AuthenticationContext("https://login.windows.net/" + tenantId);
                ClientCredential clientCredential = new ClientCredential(appId, secretKey);
                var tokenResponse = context.AcquireTokenAsync("https://management.azure.com/", clientCredential).Result;
                var accessToken = tokenResponse.AccessToken;
                TokenCredentials credential = new TokenCredentials(accessToken);
                var webSiteManagementClient = new Microsoft.Azure.Management.WebSites.WebSiteManagementClient(credential);
                webSiteManagementClient.SubscriptionId = subscriptionId;
                var servicePlan = webSiteManagementClient.AppServicePlans.ListByResourceGroupWithHttpMessagesAsync(resourceGroup).Result.Body.Where(x => x.Name.Equals(servicePlanName)).FirstOrDefault();
 
                var appServicePlanRequest = await webSiteManagementClient.AppServicePlans.ListByResourceGroupWithHttpMessagesAsync(resourceGroup);
                appServicePlanRequest.Body.ToList().ForEach(x => log.LogInformation($">>>{x.Name}"));
                var appServicePlan = appServicePlanRequest.Body.Where(x => x.Name.Equals(servicePlanName)).FirstOrDefault();
                if (appServicePlan == null)
                {
                    log.LogError("Could not find app service plan.");
                }
 
                //scale up/down
                //servicePlan.Sku.Family = "P";
                //servicePlan.Sku.Name = "P1v2";
                //servicePlan.Sku.Size = "P1v2";
                //servicePlan.Sku.Tier = "PremiumV2";
                servicePlan.Sku.Capacity = capacity; // scale out: number of instances
                var updateResult = webSiteManagementClient.AppServicePlans.CreateOrUpdateWithHttpMessagesAsync(resourceGroup, servicePlanName, servicePlan).Result;
                log.LogInformation("Completed!!");
                return (ActionResult)new OkObjectResult($"Hello, {capacity} {updateResult.Response.StatusCode}");
            }
            else
            {
                return new BadRequestObjectResult("Please pass a capacity on the query string or in the request body");
            }
           
        }
    }
}

HTH. 2019-11-18 By Jacky