Auto shutdown AVD Personal Host Pools

Auto shutdown AVD personal desktop host pools

Update (22/12/2021): Since Managed Identities for Automation accounts is now GA. I have updated the script to make use of this. This way the Automation account requires less permissions to run.

If you’re using Azure Virtual Desktop in a pooled scenario you are properly using the scaling script that Microsoft has provided . This script works great, but what about personal host pools. The scaling script doesn’t have support for this type of host pool. This is a shame because you can also save considerably amount of money if your personal session hosts would only be running if they are being used. Especially now that Start Virtual Machine on Connect is now generally available.  This means that users can power on the machines when they start working. However the machines are only turned on, and not deallocated. Which is only half of the solution…

In order to deallocate machines that are not being used I’ve create a PowerShell script that can be run in an Automation Account. The script does the following:

  • Checks if the host pool is set to personal, pooled is not supported
  • Checks if start on Connect is enabled. Link to how to configure this
  • Collects all the Session Hosts in the host pool
  • If the Session Host is running it checks if there is an active session, if there are no active sessions the Session Host will be Deallocated.
  • You can exclude machines from the script by using a tag

The script is available in my GitHub Repo. You can import the script into an automation account and set it up yourself. Or if you would like to use the script you can use the following script that will set everything up for you. This script is a slightly adjusted version of Microsoft’s script which is used for the scaling script itself.

The DeployAutomationAccount.ps1 script will:

  • Checks if you have the appropriate permissions
  • Checks if you have the correct modules installed on your computer
  • Deploys a new resource group for the automation account (if needed)
  • Deploys a new Automation Account (if needed) and imports the necessary modules and runbook
  • Creates an Automation Schedule which runs every 1 hour
  • Connects the Runbook to the Schedule so it will start
  • Validates if an Run As Account is present
  • Creates a managed identity with the required permissions

To deploy the automation account with the AVD-PersonalAUtoShutdown.ps1 script you first download the script:

New-Item -ItemType Directory -Path "C:\Temp" -Force
Set-Location -Path "C:\Temp"
$Uri = ""
# Download the script
Invoke-WebRequest -Uri $Uri -OutFile ".\DeployAutomationAccount.ps1"

Also download the Custom Role Definition for the role assignments

$Uri = ""
# Download the script
Invoke-WebRequest -Uri $Uri -OutFile ".\Automation-RoleDefinition.json"

Log in to your environment


Run the following cmdlet to execute the script and create the Automation Account. You can fill in the values or comment them out to use their default values.

$Params = @{
    "AADTenantId"               = "<Azure_Active_Directory_tenant_ID>"
    "SubscriptionId"            = "<Azure_subscription_ID>" 
    "AutomationRG"              = "<ResourceGroup of the Automation Account>" # Optional. Default: rgAVDAutoShutdown
    "AutomationAccountName"     = "<Automation Account Name>" # Optional. Default: AVDAutoScaleAccount
    "AutomationScheduleName"    = "<Automation Schedule Name>" # Optional. Default: AVDShutdownSchedule
    "AVDrg"                     = "<AVD resource group which holds the Host Pool Object>"
    "SessionHostrg"             = "<Resource group which contains the VMs of the session hosts>"
    "HostPoolName"              = "<Host pool Name>"
    "SkipTag"                   = "<Name of the tag to skip the vm from processing>" # Optional. Default: SkipAutoShutdown
    "TimeDifference"            = "<Time difference from UTC (e.g. +2:00) >" # Optional. Default: +2:00
    "Location"                  = "<Location of deployment (e.g West Europe)>" # Optional. Default: West Europe

.\DeployAutomationAccount.ps1 @Params

The deployment will kick off and the automation account with the AVD-PeronalAutoShutdown.ps1 script will be created.

The one thing that I will not do for you is the creation of an Run As Account. To create the Run as Account:

  1. In the Azure portal, select All services. In the list of resources, enter and select Automation accounts.
  2. On the Automation accounts page, select the name of your Azure Automation account. The default value is AVDAutoShutdownAutomationAccount
  3. In the pane on the left side of the window, select Run As accounts under the Account Settings section.
  4. Select Azure Run As account. When the Add Azure Run As account pane appears, review the overview information, and then select Create to start the account creation process.
  5. Wait for the deployment to complete

During the deployment of the Automation account a schedule as also created. This schedule will trigger the runbook every hour and will do so for the next 5 years. You can adjust the schedule to your requirements.

To view the out put of the AVD-PersonalShutdown script you:

  1. Go to the Automation account that hosts the script (default is AVDAutoShutdownAutomationAccount)
  2. Under Process Automation you find Jobs
  3. Select the top job, which is the latest.
  4. Select the tab Output. Here you will find all the output information that the script generated.

To exclude a machine from begin processed you can simply add a tag to the VM and the script will skip that practically machine, the default value for this is SkipAutoShutdown

Wrapping up

You can find the scripts and additional information in my GitHub Repo.

If you are using the script and liking it, I would love to read about it in the comments! Or if you have any suggestions on how to improve the script, I would love the read your suggestions as well!

28 thoughts on “Auto shutdown AVD Personal Host Pools”

  1. It looked like all was going well. However, when I run the script it gives an error “System.Management.Automation.CommandNotFoundException: The term ‘Get-AzWvdHostPool’ is not recognized as the name of a cmdlet, function, script file, or operable program”
    I manually ran that line ($Hostpool = Get-AzWvdHostPool -SubscriptionId $SubscriptionId -Name $HostPoolName -ResourceGroupName $AVDrg) in PowerShell, and it returns the name of the hostpool correctly.
    Not sure why it isn’t working when run as a Runbook.
    Do you have an idea?
    I changed the script line 132 to “$Hostpool = $HostPoolName” and did a test run, but then I got the error “Not a personal pool”, but it plainly is, and the first run didn’t give that error

    1. Hi Randy, It looks like the correct PowerShell modules aren’t imported into the automation account. You can check this by going to the automation account and select modules. For the script to work you need the following modules installed: Az.Accounts, Az.Compute, Az.Resources, Az.Automation, Az.DesktopVirtualization. If they aren’t present you can add them manually. Hopes this works
      Automation Account modules

      1. Stephan, thanks for the reply. I used an existing Automation Account. I think the script may not import the modules into an existing AA properly. I looked at the script, and it looked like it should, but I think that may be the problem.

  2. I checked, Az.Automation and Az.DesktopVirtualization were not installed. I installed them.
    When I look at the log for the job there is nothing in the errors tab. In the Exceptions tab it shows “The hostpool type is not set to personal. Pooled host pools are not supported by this script…”
    However, that is not correct. It is a Personal pool, with direct assignments.
    I am copying this from the AVD Host Pool screen:
    Resource group(change)
    East US
    Subscription ID
    Host pool type
    Assignment type

    1. Really strange.. Ive tested the script just now and it running fine on both automatic and direct personal host pools. Just to be sure:
      – Your AVD deployment is ARM based?
      – $AVDrg should be the resource group where the Host Pool Object is located
      – $SessionHostrg should be the resource group where the virtual machines are located. This can be the same location as $AVDrg
      – $Hostpool name is the name of the Host Pool
      – If you run 132 what do you get when running $Hostpool.HostPoolType and $Hostpool.StartVMOnConnect
      – You changed the script on line 132, did you change it back?

      1. Stephan,
        Thanks for your update. Changing line 132 changed the behavior, it is no longer showing the pool error.
        Yes, it is ARM based. Yes, $AVDrg is the RG where HP is. Yes $SessionHostRG is where actual VMs are located (and is same as $AVDrg). Yes, $Hostpool is name of Host Pool. I did change it back, behavior changed, but still not working. I then copied your complete code in place (only change was line 132, but I did that just to be 100%)

        I have 100% confirmed the WVD resource group name, using copy and paste, and not manual typing.
        I’m now getting three errors per each VM in the pool:
        The Resource ‘Microsoft.Compute/virtualMachines/WVD-Name.domain.local’ under resource group ‘rg_wvd_name’ was not found. For more details please go to ErrorCode: ResourceNotFound ErrorMessage: The Resource ‘Microsoft.Compute/virtualMachines/WVD-Name.domain.local’ under resource group ‘rg_wvd_name’ was not found. For more details please go to ErrorTarget: StatusCode: 404 ReasonPhrase: Not Found OperationID : 54cabb00-5f3d-4967-ba7c-c6c410f43926
        10/29/2021, 10:56:36 AM
        System.Management.Automation.RuntimeException: Cannot index into a null array. at CallSite.Target(Closure , CallSite , Object , Int32 ) at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1) at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame) at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
        10/29/2021, 10:56:37 AM
        The Resource ‘Microsoft.Compute/virtualMachines/WVD-Name.domain.local’ under resource group ‘rg_wvd_name’ was not found. For more details please go to ErrorCode: ResourceNotFound ErrorMessage: The Resource ‘Microsoft.Compute/virtualMachines/WVD-Name.domain.local’ under resource group ‘rg_wvd_name’ was not found. For more details please go to ErrorTarget: StatusCode: 404 ReasonPhrase: Not Found OperationID : 33799a9d-dd26-43c4-b7df-e6656917a66c
        10/29/2021, 10:56:39 AM

          1. Stephan,
            Thanks for the reply and the update. That worked! The VMs shut off.

          2. Hi Randy, That’s good to hear! 🙂
            Thanks for your feedback and providing me the information to optimize the script!

  3. Stephan,
    I have an environment that has multiple Personal Pools. When I tried to add a second runbook and schedule, I got an error that the modules are already loaded and can’t be loaded again. I commented out the Import Module section, and got an error that “Get-AzContext not found…run Import-Module Az.Accounts”
    I then tried to just create a new Runbook and schedule manually. When I run it I get “The term ‘Connect-AzAccount’ is not recognized as a name of a cmdlet, function, script file, or executable program.”
    When I created the task, I copied your new code in.
    Any idea how I could do multiple personal pools? I did the test in an environment that only has 1 pool. The real environment I want to deploy in has one subscription but one pool is in one resource group, 2 are in a different resource group from the first, but same as each other.
    The org I’m working with will not want another Automation Account.
    Thanks for your help!!

    1. Hi Randy,

      You only need to a link a new schedule to the runbook. You do this by going to the automation account and by selecting the AVDPersonalAUtoShutdown runbook. There you have the option ‘Link’ to schedule’. Create a new schedule to run every 1 hour. For Parameters and run settings enter the information of the specific hostpool and you are up and running

  4. Wanted to say thanks, and call out one improvement. If a user shuts down the VM themselves, then it is stopped, but not deallocated.

    if($VMStatus -eq ‘PowerState/stopped’){
    Write-Output “$SessionHostName is in a stopped state, trying to deallocate”
    $StopVM = Stop-AzVM -Name $VMinstance -ResourceGroupName $AVDrg -Force
    Write-Output “Stopping $SessionhostName ended with status: $($StopVM.Status)”

    1. Thanks for the feedback, much apricated! I’ve updated the script to also check for stopped VM’s

  5. I have attempted to use the defaults and custom variable names but still get the following error when I attempt to run the deployment script. I noticed that parameter listed on line 252. All other references to the parameter or variable use an upper case “A” so it stood out.

    C:\Temp\DeployAutomationAccount.ps1 : A parameter cannot be found that matches parameter name ‘automationAccountName’.
    At line:1 char:1
    + .\DeployAutomationAccount.ps1 @Params -Verbose
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [DeployAutomationAccount.ps1], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,DeployAutomationAccount.ps1

    1. Hi Chris,

      When do you get the error? When just using the required parameters the script runs fine for me

  6. Hi Stephan, I just tested the script but had a error, but I found the issue. On line 17o the command $SessionHosts = @(Get-AzWvdSessionHost -ResourceGroupName $SessionHostrg -HostPoolName $HostPoolName) is not working for me.

    I have the sessionhost in a different resourcegroup so I had to change the command to
    $SessionHosts = @(Get-AzWvdSessionHost -ResourceGroupName $AVDrg -HostPoolName $HostPoolName)

    So I changed the paramater $SessionHostrg to $AVDrg on line 170 to successful read the session hosts in the host pool

    1. Hi Johan,
      It should be no problem if your session hosts are in a different resource group. The $AVDrg states the location of your AVD objects, such as the host pool object. The $SessionHostrg should hold the resource group of the session hosts. Either way if it now works for you that’s great 🙂

  7. Hi
    I just wanted to point out the script does not work if you are using an external account to log into Azure. You need to transform the variable azcontext.account to the external user format otherwise the cmdlet get-azRoleAssignment throws an error

    Just letting you know in case you want to add an IF to transform the value when the signed in user is external.

    1. Thank you for letting me know. I will see if I can make an addition to the script

  8. Hi Stephen,

    Thanks for sharing your script, I appreciate it.

    I ran into a couple of issues when testing the deployment today:

    1. When running the deployment today it gets stuck at “Waiting for module ‘Az.Compute’ to get imported into Automation Account Modules” and eventually times out. It appeared to be Az.Compute failing to install because there is a dependency of Az.Accounts 2.11.0 and the installed version by default was 2.8.0 (from memory). After manually updating Az.Accounts to 2.11.0 from, I was able to re-run the script and it continued the deployment.

    2. After this the script continued to import all of the required modules but failed with “New-AzRoleDefinition : The access token is from the wrong issuer”. I updated the SubscriptionID under AssignableScopes in the c:\temp\Automation-RoleDefinition.json to my Subscription ID. I couldn’t see this required step documented in your post.

    It is working as expected now after remediating the above issues.



    1. Hi Asia, Thanks for you feedback. I have updated the Role Definition, there was indeed a subscription hard coded. It should now automatically update with the correct subscription.
      I also get the same error when importing the required PowerShell modules. I will update the script with a fix

  9. Hi Stephan,

    Great work on the scripts, i do have some issues with implementing it.
    When running the script/runbook, i’m getting the “error” saying that my host pool is not set to personal. When checking the Host Pool type, it is set to personal.

    Any ideas?

      1. Hi,

        Thanks for the reply! I wasn’t aware of this, thanks.
        i’ll look into it 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: