Automatically Scale Up an Azure App Service Plan

Azure App Service is a hugely popular cloud service used to host, scale and manage web applications. An Azure App Service Plan is the component which defines the infrastructure used to host those web applications. It specifies the number of instances used to host the web applications as well as the size of the those instances. There’s two ways to scale an Azure App Service Plan: scaling out and scaling up.

Scaling out is also known as horizontal scaling. It involves adding more instances to the Azure App Service Plan. We can manually change the number of instances by using the Azure Portal or running an ad-hoc script with the Azure CLI or Azure PowerShell. However, we can also set rules based on metrics to automatically scale out the number of instances using an out-of-the-box feature of Azure App service.

Scaling up is also known as vertical scaling. It involves upgrading the instances to a larger size which can provide more compute and memory. We can scale up manually just like we can scale out manually. However, there is no native Azure App Service feature at this time which allows us to automatically scale up based on metrics. When workloads must be autoscaled, but they don’t or can’t benefit from horizontal scaling, we need to build our own solution.

We’re going to demonstrate such a solution which automatically scales up an Azure App Service Plan from a Standard S1 size to a Standard S2 size when the CPU usage goes above 75%.

Prerequisites

We’ll need the following to implement the solution presented:

Solution Overview

Before we start with our implementation, we’ve presented the solution overview to give us some context.

  1. Azure Monitor fires an alert when the CPU usage of the Azure App Service Plan goes above 75%.
  2. The action group associated with the alert calls an Azure Function.
  3. The Azure Function runs an Azure PowerShell command to scale up the app service plan.

Create and Deploy the Azure Function

Looking at our solution overview above, we can see that the Azure Function is the component which actually scales up the Azure App Service Plan. Let’s build that function first. To follow this tutorial, we’ll need the Azure Functions Core Tools version 3.X and Azure PowerShell installed. The completed source code for the Azure Function App can also be found here.

Open a PowerShell prompt and run the following commands to generate the initial source code for the PowerShell function:

mkdir ScaleFunction
cd ScaleFunction
#Generate the code for a PowerShell function app
func init ScaleFunction --powershell
cd ScaleFunction
#Generate the code for an Http-Triggered function called ScaleUp
func new --template "Http Trigger" --name ScaleUp --language PowerShell

As part of our generated code, we should see a file called requirements.psd1. Open it and replace the contents with the following. This will allow us to load the Az.Websites Azure PowerShell module which contains the command required to scale up an Azure App Service Plan.

@{
    'Az' = "6.*"
    'Az.Websites' = "2.*"
}

As part of our generated code, we should also see a file called profile.ps1. Open it and replace the contents with the following. This will allow us to use the Azure Function’s managed identity to perform the scale up operation on the Azure App Service Plan.

#Authenticate with Azure PowerShell using the function's managed identity
if ($env:MSI_SECRET) {
    Disable-AzContextAutosave -Scope Process | Out-Null
    Connect-AzAccount -Identity
}

As part of our generated code, we should also see a file called run.ps1. This file holds the code for the actual function. Replace the current contents of the file with the following code. Make sure to replace the placeholder <appServicePlanName> with the name of the Azure App Service Plan which we want to scale up. Replace the placeholder <appServicePlanResourceGroupName> with the name of the resource group which contains the Azure App Service Plan.

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
# Scale up the app service plan to a S2 (Medium) Standard instance.
Set-AzAppServicePlan `
    -Name '<appServicePlanName>' `
    -ResourceGroupName '<appServicePlanResourceGroupName>' `
    -Tier Standard -WorkerSize Medium

# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = [HttpStatusCode]::OK
})

We’re done with writing our source code. Now we just need to deploy the code to an Azure Function. Before we proceed, we may need to run the following command in the PowerShell prompt to authenticate against the Azure subscription.

Connect-AzAccount

We’re now going to run an Azure PowerShell script which will do the following:

  1. Create an Azure Storage Account
  2. Create an Azure Function App with a system-assigned managed identity.
  3. Assign the function app’s managed identity a role of Web Plan Contributor with our app service plan resource as the scope. This will give our function app the permission to scale up our app service plan.
  4. Publish our source code to the function app resource we created.

For the script to work, make sure to replace the placeholders mentioned below with the correct values.

  • Replace <location> with the region we want to deploy the Azure Function in, for example EastUs. It would make sense to use the same location as the app service plan.
  • Replace <uniqueFunctionAppName> with a globally unique name for our Azure Function App. This name will also be used for the storage account.
  • Replace <appServicePlanResourceGroup> with the name of the resource group which contains the app service plan.
  • Replace <appServicePlanName> with the name of the app service plan which we want to scale up.

The script is below:

#Replace placeholders <> below 
$location ="<location>"
$uniqueFunctionAppName= "<uniqueFunctionAppName>"
$appServicePlanResourceGroup="<appServicePlanResourceGroup>"
$appServicePlanName ="<appServicePlanName>"

#Create Storage Account
New-AzStorageAccount -ResourceGroupName $appServicePlanResourceGroup `
  -Name $uniqueFunctionAppName `
  -Location $location `
  -SkuName Standard_LRS `
  -Kind StorageV2

#Create Azure Function App
New-AzFunctionApp -Name $uniqueFunctionAppName `
    -ResourceGroupName $appServicePlanResourceGroup `
    -StorageAccount $uniqueFunctionAppName `
    -Runtime PowerShell `
    -FunctionsVersion 3 `
    -Location $location `
    -IdentityType SystemAssigned

#Get Azure Function App details
$funcApp = Get-AzFunctionApp `
    -Name $uniqueFunctionAppName `
    -ResourceGroupName $appServicePlanResourceGroup

#Get Azure App Service Plan which we want to scale up
$appServicePlan = Get-AzAppServicePlan `
    -Name $appServicePlanName `
    -ResourceGroupName $appServicePlanResourceGroup

#Assign the function app the Web Plan Contributor role for the app service plan
New-AzRoleAssignment -ObjectId $funcApp.IdentityPrincipalId `
    -RoleDefinitionName "Web Plan Contributor" `
    -Scope $appServicePlan.Id

start-sleep -Seconds 10
#Publish the function source code to the azure function resource
func azure functionapp publish $uniqueFunctionAppName

After running the above script successfully, if we were to manually trigger the Azure Function, our app service plan should scale up to the Standard S2 tier. However, we want to automatically scale up our app service plan when its CPU usage exceeds 75%. To do that let’s move on to the next part: creating an alert with an action group on the app service plan.

Create the Alert and Action Group

Azure Monitor gives us the ability to fire an alert when certain configurable conditions are met. These conditions can be based on log queries, activity logs or metrics. In our example, we’ll be using a metric-based condition for our alert, because we want to take an action when the CPU usage is greater than 75%.

An alert on its own will not do much. To be notified of an alert or to take an automatic action in response to an alert, we have to associate it with an action group. For our case, we will create an action group which triggers the Azure Function we created in the previous section.

We have provided an Azure PowerShell script which will create both the action group and the alert. Note that this PowerShell script actually includes commands which use the Azure CLI, so make sure it is installed! We can go ahead and run the following script in the PowerShell prompt:

$actionGroupReceiverName = "ScaleUpFunctionReceiver"
$actionGroupName = "ScaleUpActionGroup"
$actionshortName = "ScaleUp"
$alertRuleName = "CPU High"

#Get the base URL of the HTTP-triggered function
$functionBaseUrl = az functionapp function show `
    --function-name ScaleUp `
    --name $uniqueFunctionAppName `
    --resource-group $appServicePlanResourceGroup `
    --query "invokeUrlTemplate" `
    --output tsv

#Get the authorization code for the HTTP-triggered function
$functionKey = az functionapp function keys list `
    --function-name ScaleUp `
    --name $uniqueFunctionAppName `
    --resource-group $appServicePlanResourceGroup `
    --query "default" `
    --output tsv

#Combine the auth code and base URL into one request URL     
$functionUrl =$functionBaseUrl + "?code=" + $functionKey

#Create a receiver for the action group which calls the function
$functionReceiver = New-AzActionGroupReceiver `
    -Name $actionGroupReceiverName `
    -FunctionAppResourceId $funcApp.Id `
    -HttpTriggerUrl $functionUrl `
    -FunctionName $uniqueFunctionAppName

#Create the action group to be used with the alert
$actionGroup = Set-AzActionGroup -Name $actionGroupName `
    -ResourceGroup $appServicePlanResourceGroup `
    -ShortName $actionshortName `
    -Receiver $functionReceiver 

#Create the condition for which the alert will fire (CPU >75%)
$condition = New-AzMetricAlertRuleV2Criteria `
    -MetricName "CpuPercentage" `
    -MetricNameSpace "Microsoft.Web/serverFarms" `
    -TimeAggregation Average `
    -Operator GreaterThan -Threshold 75

#Create the alert rule for the app service plan
$alert = Add-AzMetricAlertRuleV2 `
    -Name $alertRuleName `
    -ResourceGroupName $appServicePlanResourceGroup `
    -WindowSize 0:5 `
    -Frequency 0:5 `
    -TargetResourceId $appServicePlan.Id `
    -Condition $condition `
    -ActionGroupId $actionGroup.Id `
    -Severity 2

And we’re done! If our app service plan’s CPU usage exceeds 75%, our function will get triggered and it will scale up the app service plan to Standard S2.

An Alternative Approach

Instead of using an Azure Function to scale up our app service plan, we could have used an Azure Automation Runbook. The rest of the solution would remain the same:

If we wanted to use an Azure Automation Runbook rather than an Azure Function, instead of using the steps in the Create and Deploy the Azure Function section, we would do the following:

  1. Create an Azure Automation Account.
  2. Enable the system-assigned managed identity for the created automation account.
  3. Give the managed identity a role assignment for the role Web Plan Contributor with the scope being the app service plan to scale up.
  4. Import the Az.Websites PowerShell module into the automation account.
  5. Create a runbook with the following code:
Import-Module Az.Websites
#Authenticate using the managed identity
Connect-AzAccount -Identity
Set-AzContext -Subscription "73efad4a-5557-4af3-98fd-097c0f81957f"
#Scale up the app service plan
Set-AzAppServicePlan `
    -Name 'scaletestzee' `
    -ResourceGroupName 'scaletestzee' `
    -Tier Standard -WorkerSize Medium

Of course, we would also have to modify the script to create the action group so that it runs the automation runbook instead of calling an Azure Function.

Closing Thoughts

We’ve now seen how to automatically scale up an Azure App Service Plan based on performance metrics by using Azure Monitor and an Azure Function. We’ve also looked at alternative solution which replaces the Azure Function with an Azure Automation Runbook.

There are some limitations of the solution we’ve presented above. One of them is that the app service plan doesn’t automatically scale back down to its previous level based on metrics. For this reason, I’d recommend adding an e-mail notification to the alert’s action group, so that someone knows to manually scale the app service plan back down when appropriate.

Another limitation is that we’ve only allowed ourselves to scale up one level to a Standard S2 instance. However, this can be easily fixed by modifying the Azure Function code so that it scales to Standard S3 if the instance is already at Standard S2 and so on.