Since the launch of Azure Virtual Desktop a very important topic has been how to limit your costs. In this blog I’ll go over a couple of ways to limit your compute costs since these are the biggest costs.
Table of content
- What to do with users that don’t logoff
- Making sure your session hosts don’t keep running for nothing
- Start VM on Connect
What to do with users that don’t logoff
We all know that users often don’t logoff their sessions but just press the “x” at the end of their working day. Because of this the second part of this blogpost won’t be able to work properly.
To make sure that those disconnected sessions are being logged off we can create a GPO that sets a time limit for disconnected sessions and apply it to the Session Host OU.
Computer Configuration > Administrative Templets >
Windows Components > Remote Desktop Services > Remote Desktop Session
Host > Session Time limits
Be sure to set the “Set time limit for disconnected sessions”. You only need to make sure that you don’t set the time to short otherwise you might get frustrated users.
You might also want to consider to enable the “Set time limit for active but idle Remote Desktop Services session” setting in case your users didn’t disconnect but left their session open and left.
Making sure your session hosts don’t keep running for nothing
So now we got the disconnected sessions taken care of we can focus on getting the unused session hosts turned off. We have a couple of options for this.
- Scaling script (At least 1 session host keeps running)
- Create a Azure Function to shutdown unused session hosts.
Scaling Script
The scaling script is a very good way to make sure that you don’t have session host running that you don’t need. Some benefits of the tool are:
- Schedule VMs to start and stop based on Peak and Off-Peak business hours.
- Scale out VMs based on number of sessions per CPU core.
- Scale in VMs during Off-Peak hours, leaving the minimum number of session host VMs running.
The scaling script will see what hosts don’t have active sessions. So making sure disconnected sessions are being logged off is very important otherwise they will be ignored and stay active. Also Sessions hosts that are in drain mode will be ignored.
How to deploy this scaling script
Start with connecting to Azure (You need to be Contributor on the subscription where you deploy this)
Login-AzAccount
Next you need to download the script to create the Automation Account
New-Item -ItemType Directory -Path "C:\Temp" -Force
Set-Location -Path "C:\Temp"
$Uri = "https://raw.githubusercontent.com/Azure/RDS-Templates/master/wvd-templates/wvd-scaling-script/CreateOrUpdateAzAutoAccount.ps1"
# Download the script
Invoke-WebRequest -Uri $Uri -OutFile ".\CreateOrUpdateAzAutoAccount.ps1"
Now run the following Powershell script with the following parameters. Some parameters are optional.
$Params = @{
"AADTenantId" = "<Azure_Active_Directory_tenant_ID>" # Optional. If not specified, it will use the current Azure context
"SubscriptionId" = "<Azure_subscription_ID>" # Optional. If not specified, it will use the current Azure context
"UseARMAPI" = $true
"ResourceGroupName" = "<Resource_group_name>" # Optional. Default: "WVDAutoScaleResourceGroup"
"AutomationAccountName" = "<Automation_account_name>" # Optional. Default: "WVDAutoScaleAutomationAccount"
"Location" = "<Azure_region_for_deployment>"
"WorkspaceName" = "<Log_analytics_workspace_name>" # Optional. If specified, Log Analytics will be used to configure the custom log table that the runbook PowerShell script can send logs to
}
.\CreateOrUpdateAzAutoAccount.ps1 @Params
As soon as the script is finished you’ll see the Automation Account in the Azure portal
Next thing to do is to create an run as account. Look up your automation account and click on Create
the next step is to create the logic app. Use the below code to download the script for this.
New-Item -ItemType Directory -Path "C:\Temp" -Force
Set-Location -Path "C:\Temp"
$Uri = "https://raw.githubusercontent.com/Azure/RDS-Templates/master/wvd-templates/wvd-scaling-script/CreateOrUpdateAzLogicApp.ps1"
# Download the script
Invoke-WebRequest -Uri $Uri -OutFile ".\CreateOrUpdateAzLogicApp.ps1"
Now we run the code to create the app and set the execution schedule. This has to be done per hostpool, but you can use the same automation account.
$AADTenantId = (Get-AzContext).Tenant.Id
$AzSubscription = Get-AzSubscription | Out-GridView -OutputMode:Single -Title "Select your Azure Subscription"
Select-AzSubscription -Subscription $AzSubscription.Id
$ResourceGroup = Get-AzResourceGroup | Out-GridView -OutputMode:Single -Title "Select the resource group for the new Azure Logic App"
$WVDHostPool = Get-AzResource -ResourceType "Microsoft.DesktopVirtualization/hostpools" | Out-GridView -OutputMode:Single -Title "Select the host pool you'd like to scale"
$LogAnalyticsWorkspaceId = Read-Host -Prompt "If you want to use Log Analytics, enter the Log Analytics Workspace ID returned by when you created the Azure Automation account, otherwise leave it blank"
$LogAnalyticsPrimaryKey = Read-Host -Prompt "If you want to use Log Analytics, enter the Log Analytics Primary Key returned by when you created the Azure Automation account, otherwise leave it blank"
$RecurrenceInterval = Read-Host -Prompt "Enter how often you'd like the job to run in minutes, e.g. '15'"
$BeginPeakTime = Read-Host -Prompt "Enter the start time for peak hours in local time, e.g. 9:00"
$EndPeakTime = Read-Host -Prompt "Enter the end time for peak hours in local time, e.g. 18:00"
$TimeDifference = Read-Host -Prompt "Enter the time difference between local time and UTC in hours, e.g. +5:30"
$SessionThresholdPerCPU = Read-Host -Prompt "Enter the maximum number of sessions per CPU that will be used as a threshold to determine when new session host VMs need to be started during peak hours"
$MinimumNumberOfRDSH = Read-Host -Prompt "Enter the minimum number of session host VMs to keep running during off-peak hours"
$MaintenanceTagName = Read-Host -Prompt "Enter the name of the Tag associated with VMs you don't want to be managed by this scaling tool"
$LimitSecondsToForceLogOffUser = Read-Host -Prompt "Enter the number of seconds to wait before automatically signing out users. If set to 0, any session host VM that has user sessions, will be left untouched"
$LogOffMessageTitle = Read-Host -Prompt "Enter the title of the message sent to the user before they are forced to sign out"
$LogOffMessageBody = Read-Host -Prompt "Enter the body of the message sent to the user before they are forced to sign out"
$AutoAccount = Get-AzAutomationAccount | Out-GridView -OutputMode:Single -Title "Select the Azure Automation account"
$AutoAccountConnection = Get-AzAutomationConnection -ResourceGroupName $AutoAccount.ResourceGroupName -AutomationAccountName $AutoAccount.AutomationAccountName | Out-GridView -OutputMode:Single -Title "Select the Azure RunAs connection asset"
$WebhookURIAutoVar = Get-AzAutomationVariable -Name 'WebhookURIARMBased' -ResourceGroupName $AutoAccount.ResourceGroupName -AutomationAccountName $AutoAccount.AutomationAccountName
$Params = @{
"AADTenantId" = $AADTenantId # Optional. If not specified, it will use the current Azure context
"SubscriptionID" = $AzSubscription.Id # Optional. If not specified, it will use the current Azure context
"ResourceGroupName" = $ResourceGroup.ResourceGroupName # Optional. Default: "WVDAutoScaleResourceGroup"
"Location" = $ResourceGroup.Location # Optional. Default: "West US2"
"UseARMAPI" = $true
"HostPoolName" = $WVDHostPool.Name
"HostPoolResourceGroupName" = $WVDHostPool.ResourceGroupName # Optional. Default: same as ResourceGroupName param value
"LogAnalyticsWorkspaceId" = $LogAnalyticsWorkspaceId # Optional. If not specified, script will not log to the Log Analytics
"LogAnalyticsPrimaryKey" = $LogAnalyticsPrimaryKey # Optional. If not specified, script will not log to the Log Analytics
"ConnectionAssetName" = $AutoAccountConnection.Name # Optional. Default: "AzureRunAsConnection"
"RecurrenceInterval" = $RecurrenceInterval # Optional. Default: 15
"BeginPeakTime" = $BeginPeakTime # Optional. Default: "09:00"
"EndPeakTime" = $EndPeakTime # Optional. Default: "17:00"
"TimeDifference" = $TimeDifference # Optional. Default: "-7:00"
"SessionThresholdPerCPU" = $SessionThresholdPerCPU # Optional. Default: 1
"MinimumNumberOfRDSH" = $MinimumNumberOfRDSH # Optional. Default: 1
"MaintenanceTagName" = $MaintenanceTagName # Optional.
"LimitSecondsToForceLogOffUser" = $LimitSecondsToForceLogOffUser # Optional. Default: 1
"LogOffMessageTitle" = $LogOffMessageTitle # Optional. Default: "Machine is about to shutdown."
"LogOffMessageBody" = $LogOffMessageBody # Optional. Default: "Your session will be logged off. Please save and close everything."
"WebhookURI" = $WebhookURIAutoVar.Value
}
.\CreateOrUpdateAzLogicApp.ps1 @Params
When the script is finished you’ll be able to see it in the portal
Create a Azure Function to shutdown unused session hosts
The other option is to create an Azure Function to stop unused session hosts. Kudos for “Ciraltos” for creating this.
Go to the Azure portal and look for Function App and click on Create
The next steps are to make sure the functiopn app uses the powershell Az. Module and create a System Assigned Managed Identity.
In the left blade go to App Files and from the dropdown select “requirements.psd1” and remove the “#” from line 7.
to make sure the configuration changes are applied to the Function App you need to restart it.
Next we will configure the Managed Identity. To do this go to Idendity in the left blade and click on Identity. Now put the slider to “On” and click on Save.
Now you need to add the Role Assignment. The roles you need to assign are:
- Desktop Virtualization Reader
- Virtual Machine Contributor
Now it’s finally time to create the Azure Function. On the left click on functions and click create
Choose the Timer Trigger Template
In this example I’ll set the timer to 30 minutes.
Now go to the Code+Test blade and choose “Function.json” from the dropdown
Go back to the run.ps1 section and replace the code with the code
#Variables
## Update "HostPool" value with your host pool, and "HostPoolRG" with the value of the host pool resource group.
## See the next step if working with multiple host pools.
$allHostPools = @()
$allHostPools += (@{
HostPool = "<HostPoolName>";
HostPoolRG = "<HostPoolResourceGroup>"
})
You can use the code to manage multiple hostpools also by adding these to the script.
The script will list all the session host that have active connections.
$count = 0
while ($count -lt $allHostPools.Count) {
$pool = $allHostPools[$count].HostPool
$poolRg = $allHostPools[$count].HostPoolRG
Write-Output "This is the key (pool) $pool"
write-output "this is the value (rg) $poolRg"
# Get the active Session hosts
try {
$activeShs = (Get-AzWvdUserSession -ErrorAction Stop -HostPoolName $pool -ResourceGroupName $poolRg).name
}
catch {
$ErrorMessage = $_.Exception.message
Write-Error ("Error getting a list of user sessions: " + $ErrorMessage)
Break
}
$allActive = @()
foreach ($activeSh in $activeShs) {
$activeSh = ($activeSh -split { $_ -eq '.' -or $_ -eq '/' })[1]
if ($activeSh -notin $allActive) {
$allActive += $activeSh
}
}
# Get the Session Hosts
# Exclude servers in drain mode and do not allow new connections
try {
$runningSessionHosts = (Get-AzWvdSessionHost -ErrorAction Stop -HostPoolName $Pool -ResourceGroupName $PoolRg | Where-Object { $_.AllowNewSession -eq $true } )
}
catch {
$ErrorMessage = $_.Exception.message
Write-Error ("Error getting a list of running session hosts: " + $ErrorMessage)
Break
}
$availableSessionHosts = ($runningSessionHosts | Where-Object { $_.Status -eq "Available" })
#Evaluate the list of running session hosts against
foreach ($sessionHost in $availableSessionHosts) {
$sessionHostName = (($sessionHost).name -split { $_ -eq '.' -or $_ -eq '/' })[1]
if ($sessionHostName -notin $allActive) {
Write-Host "Server $sessionHostName is not active, shut down"
try {
# Stop the VM
Write-Output "Stopping Session Host $sessionHostName"
Get-azvm -ErrorAction Stop -Name $sessionHostName | Stop-AzVM -ErrorAction Stop -Force -NoWait
}
catch {
$ErrorMessage = $_.Exception.message
Write-Error ("Error stopping the VM: " + $ErrorMessage)
Break
}
}
else {
write-host "Server $sessionHostName has an active session, won't shut down"
}
}
Now save the script and try it out. All your session host that don’t have active sessions will be turned off saving you money.
Start VM on Connect
We have talked about shutting down unused session hosts but how can the user access them while they are turned off. Start VM on Connect to the rescue.
This feature will make sure that if a session host is turned off (Deallocated)a user will be able to start it.
Requirements
Start VM on Connect needs a custom role. Go to the scubscription and click on IAM and add a custom role.
The permissions that are needed are:
- Microsoft.Compute/virtualMachines/start/action
- Microsoft.Compute/virtualMachines/read
Now you need to assign the role to Azure Virtual Desktop. Go to IAM and look up the custom role you created and assign it.
You can also create the custom role with a Json template.
Configure Start VM on Connect
This can be done in the Azure Portal. Go to your hostpool properties and put the slider to Yes.
You can also do this via Powershell
Update-AzWvdHostPool -ResourceGroupName <resourcegroupname> -Name <hostpoolname> -StartVMOnConnect:$true
To disable the feature via Powershell use the following code.
Update-AzWvdHostPool -ResourceGroupName <resourcegroupname> -Name <hostpoolname> -StartVMOnConnect:$false
There you go, the most used methods to save costs for your Azure Virtual Desktop deployment. These are settings that you have to configure your self. If you don’t want to do this I suggest you take a look at some third party tools for AVD like Nerdio. They do this all for you with their Azure Virtual Desktop manager.
Thank you for reading this blogpost and see you next time.