SOLVED

Deallocate VM on user logoff

Microsoft

Recently we've announced the public preview of start VM on connect which will allow your deallocated virtual machines getting started automatically when the assigned user tries to connect. Let us have a look how we can further optimize our cost by deallocating the VM when it is not used anymore.

 

Let's first have a high level view on the needed steps:

  • Create a custom role to deallocate a virtual machine
  • Create a managed identity for your virtual machines
  • Allow each virtual machine to deallocate itself via a role assignment
  • implement logoff script and policies around idle/disconnected sessions

 

So let's start by creating our custom role:

  1. Open the Azure portal, go to Subscriptions and select the appropriate subscription

  2. Go to Access control (IAM) and select Add a custom role.
    add-custom-role.png

  3. Next, name the custom role and add a description. In this example I'll call it "Deallocate VM on logoff"

  4. On the Permissions tab, add the following permission to the subscription you're assigning the role to:

    • Microsoft.Compute/virtualMachines/deallocate/action
  5. When you're finished, select Ok.

If you prefer a JSON definition, please use the following template:

 

{
    "properties": {
        "roleName": "Deallocate VM on logoff",
        "description": "This custom role will allow your virtual machines to be deallocated when the user logs off.",
        "assignableScopes": [
            "/subscriptions/<<<SubscriptionID>>>"
        ],
        "permissions": [
            {
                "actions": [
                    "Microsoft.Compute/virtualMachines/deallocate/action"
                ],
                "notActions": [],
                "dataActions": [],
                "notDataActions": []
            }
        ]
    }
}

 

 

After we've created our custom role, we'll need to create a managed identity for our virtual machines. By this managed identities we don't need to store any credentials locally on the virtual machine or in an Azure KeyVault and can assign each virtual machine granular permission to shutdown only itself.

As this can be a bigger task, depending on the number of virtual machines you have in your personal host pools, I've prepared a script that will utilize the Azure PowerShell modules to assign that fine-grained permissions, so you may need to install those modules first:

 

Install-Module -Name Az.Account,Az.Compute,Az.DesktopVirtualization,Az.Resources

 

 

The script itself takes the host pool name, associated resource group and the role definition name selected above as parameters. It will then iterate through all virtual machines assigned to the specified host pool, create a managed identity when not already present and create a role assignment limited to the virtual machine itself:

 

$hostPoolName = "<<<HostPoolName>>>"
$resourceGroupName = "<<<ResourceGroupName>>>"
$roleDefinitionName = "<<<RoleDefinitionName>>>"

Connect-AzAccount
$sessionHosts = Get-AzWvdSessionHost -HostPoolName $hostPoolName -ResourceGroupName $resourceGroupName
foreach ($sessionHost in $sessionHosts) {
	<# get virtual machine by session host reference #>
	$resource =  Get-AzResource -ResourceId $sessionHost.ResourceId
	$vm = Get-AzVM -ResourceGroupName $resource.ResourceGroupName -Name $resource.Name
	
	<# create system-assigned managed identiy unless it already exists #>
	$managedIdentity = ($vm.Identity | where Type -eq "SystemAssigned").PrincipalId
	if ($managedIdentity -eq $Null) {
		Update-AzVM -ResourceGroupName $vm.ResourceGroupName -VM $vm -IdentityType SystemAssigned
		$managedIdentity = ((Get-AzVM -ResourceGroupName $vm.ResourceGroupName -VMName $vm.Name).Identity | where Type -eq "SystemAssigned").PrincipalId
	}
	
	<# create role-assignment unless it already exists #>
	if ((Get-AzRoleAssignment -RoleDefinitionName $roleDefinitionName -ObjectId $managedIdentity) -eq $Null) {
		New-AzRoleAssignment -ObjectId $managedIdentity -RoleDefinitionName $roleDefinitionName -Scope $vm.Id
	}
}

 

 

Next we'll configure our session host to disconnect idle sessions and logoff disconnected sessions after a certain period of time:

  1. Connect remotely to the VM that you want to set the policy for.

  2. Open the Group Policy Editor, then go to Local Computer Policy > Computer Configuration > Administrative Templates > Windows Components > Remote Desktop Services > Remote Desktop Session Host > Session Time Limits.

  3. Find the policy that says Set time limit for disconnected sessions, then change its value to Enabled.

  4. After you've enabled the policy, select your preferred time limit at End a disconnected session.

  5. Find the policy that says Set time limit for active but idle Remote Desktop Services sessions, then change its value to Enabled.
  6. After you've enabled the policy, select your preferred time limit at Idle session limit.

The above settings also ensure that an user will get a warning message two minutes before reaching the specified time limit so he can press a key or move the mouse to prevent getting disconnected.

idle-timer-expired.png

In the last step we'll create the PowerShell script initiating the deallocation and configure it as a logoff script.

 

The PowerShell script will query the details of your virtual machine using the Azure instance metadata, connect to Azure using the created managed identity and initiate the actual deallocation via REST API:

 

$metadata = Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Proxy $Null -Uri "http://169.254.169.254/metadata/instance?api-version=2021-01-01"
$authorizationToken = Invoke-RestMethod -Headers @{"Metadata"="true"} -Method Get -Proxy $Null -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-01-01&resource=https://management.azure.com/"

$subscriptionId = $metadata.compute.subscriptionId
$resourceGroupName = $metadata.compute.resourceGroupName
$vmName = $metadata.compute.name
$accessToken = $authorizationToken.access_token

$RestartEvents = Get-EventLog -LogName System -After (Get-Date).AddMinutes(-1) |? {($_.EventID -eq 1074) -and ($_.Message -match "restart" )}
$SessionCount = (query user | Measure-Object | select Count).count - 1 # remove headline

if (($SessionCount -gt 1) -or ($RestartEvents.count -ge 1))
{
	# skip deallocate because of user-sessions or initiated reboot
} else {
	Invoke-WebRequest -UseBasicParsing -Headers @{ Authorization ="Bearer $accessToken"} -Method POST -Proxy $Null -Uri https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Compute/virtualMachines/$vmName/deallocate?api-version=2021-03-01 -ContentType "application/json"
}

 

 

To have that script executed upon logoff, we need to configure it:

  1. Connect remotely to the VM that you want to set the policy for.

  2. Open the Group Policy Editor, then go to Local Computer Policy > User Configuration > Windows Settings > Scripts (Logon/Logoff).

  3. Find the item that says Logoff.

  4. Specify the script you've created on the PowerShell Scripts tab.

PlacePlace

When rolling out to bigger host-pools, ideally place the logoff script on a centrally accessible location, e.g. SYSVOL-share.

 

Hope this short tutorial will help you take full benefit of the start VM on connect feature. Happy to read your feedback and comments below.

22 Replies

@BerndLoehlein This is awesome and I appreciate you sharing your scripts! Do you know if this will be baked into the WVD offering in the future?

One thing I wanted to call out for the community is you have to ensure you don't allow your users the ability to restart the VMs from within the OS as this will cause the script to kick off and deallocate the VMs without ever rebooting the VM. You can block this with a GPO as well.

@bking0100 Isn't it problematic to not allow users to restart the VM on demand?  I can see a few situations where for instance post install of software, etc. where a restart is needed.  

@BerndLoehlein 

very good solution. Thank you.

I try to implement the script fro create managedid and AzRoleAssgniment in a azure function. 
Which Azure Role permission i need for:

New-AzRoleAssignment -ObjectId $managedIdentity -RoleDefinitionName $roleDefinitionName -Scope $vm.Id

 Because i become the following error:
New-AzRoleAssignmentParameterSetName : EmptyParameterSetContent-Type : application/json; charset=utf-8Content-Length : 116Response :StatusCode : ForbiddenReasonPhrase : ForbiddenContent : {"odata.error":{"code":"Authorization_RequestDenied","message":{"lang":"en","value":"Insufficient privileges to complete the operation."}

@WVDExpert 

 

Assignments of roles requires the Microsoft.Authorization/roleAssignments/write permissions which are only included in the pre-defined roles of "User Access Administrator" or "Owner".

I've updated the script in above post to check if the user has initiated a reboot or another user is still active on the same machine. Deallocation won't be initiated in those cases.
Hi @BerndLoehlein , thank you heaps for this and it works like a charm for the VMs we use in our host pool. Just wanted to ask if there's a way to automate the deployment of this group policy somehow in Azure, rather than remoting into the VM and setting the policies manually?

Hi @krayste,

 

For an AD-joined host pool I would recommend configuring the mentioned settings via central group policy and targeting this to the OU where your session-hosts reside. You would then also make the script accessible on a central location, e.g. the SYSVOL share, so you don't need to deploy the script on every host.

 

For AAD-only environments there may be an option to automate it using MEM/Intune.

 

Regards,

Bernd

I've been having an issue with this, so much so I've had to use a different method. What happens when users log out is that the user session is not correctly removed. This means when they try to login thier VM doesn't start. We get the error ConnectionFailedUserHasValidSessionButRdshIsUnhealthy (-2146233088).

@HenryGelderbloem You use the actually Remote Desktop App Version?

@WVDExpert We do, and it happens on different OSes too. SO the macOS and Windows version are affected as well as the iPadOS version.

@HenryGelderbloem same here. Tried also an option with an Azure Function which checks for active sessions and shut down the VM with the Stop-AzVM PowerShell command. Same issue. The start on connect feature throws the same error. 

Can you probably share which method you finally used to avoid this error?  

My workaround was to not use this method at all. I have configured it in a way that shuts down the VM if the user logs out (or is logged out due to being idle). I then have an Azure Function that runs every 30 minutes and checks for VMs that are powered off but not deallocated. If they are powered off but not deallocated, they are then deallocated.
best response confirmed by evasse (Microsoft)
Solution

@HenryGelderbloem @PatrickBrack 

Could you please try to update the custom role used for Start VM on Connect and add Microsoft.Compute/virtualMachines/instanceView/read to it? This should ensure that sessions are removed in the service upon deallocation of the virtual machine.

 

We've already updated our docs for Start VM on Connect, but could be easily missed by customers already using this feature.

@BerndLoehlein  ,need your help i am getting this error ,however i have followed the same.

 

Invoke-WebRequest : {"error":{"code":"AuthorizationFailed","message":"The client

'aa8367c5-e8a0-3333-a1e9-b377d7c0922a' with object id 'aa8367c5-e8a0-3333-a1e9-b377d7c0922a' does not have authorization to perform action.

@lokeshchouksey This sounds like the virtual machine doesn't have permissions to deallocate itself. Can you run the script in the initial post again to assign proper permissions to the system managed identities?

@BerndLoehlein yes now machine are deallocating after assigned contributor rights to Service principle.but now there is another problem happening , i can see machines are deallocating but after 2-3 min they are automatically starting and running, is there any thing i have missed or doing wrong ?  also i have noticed it is happening only if  myself shutting down/logoff/restarting the machine.

 

it is working only if i leave the machine and machine itself disconnected and logoff via set policy.

Disregard, previous comment was RE: PowerShell 7 and could not get script to run on PS 5.1

We use Azure Virtual Desktop using Azure AD Join (AAD) and Intune (Microsoft Endpoint Manager), so I base64 encoded your sample script and used it in a scheduled task that fires on signoff events.

Here's the code for others who would like to use it:
https://gitlab.com/Lieben/assortedFunctions/-/blob/master/set-AVDDeallocateOnLogoff.ps1
and a short post describing it:
https://www.lieben.nu/liebensraum/2022/08/deallocate-azure-ad-joined-azure-virtual-desktop-vms-when-...
Hi, I had completed every step you mentioned, but my machine is only stopped not deallocated.Do you know why this is happening?
1 best response

Accepted Solutions
best response confirmed by evasse (Microsoft)
Solution

@HenryGelderbloem @PatrickBrack 

Could you please try to update the custom role used for Start VM on Connect and add Microsoft.Compute/virtualMachines/instanceView/read to it? This should ensure that sessions are removed in the service upon deallocation of the virtual machine.

 

We've already updated our docs for Start VM on Connect, but could be easily missed by customers already using this feature.

View solution in original post