Category Archives: Azure Virtual Desktop

Automatic modular rightsizing of Azure VM’s with special focus on Azure Virtual Desktop

It has long annoyed me that all the scaling options in Azure just add and remove hosts. They never target the host itself. Hosts are either under or overutilized in 84% of the case.

And this is especially relevant for AVD personal hostpools where users each have their own personal “VDI”.

So I’m releasing a custom PowerShell module called “ADDRS” (Azure Data Driven Right Sizing) that grabs mem/cpu performance of the VM or all VM’s in a resource group you tell it to check. It will then do some smart voodoo magic to determine what size out of an allowlist best fits.

Instructions / Example:

  • Use -WhatIf if you don’t want it to resize the VM
  • Use -Force if you want to resize a VM even if it is online (which will cause it to be shut down!)
  • Use -Boot if you want the VM to be started after resizing (by default it will stay deallocated)
  • Use -domain with your domain if your VM is domain joined
  • Use -region if your region is not westeurope
  • Use -Verbose if you want the full output incl financial projection
  • Use -Report if you want to output data to csv. Can be used together with -WhatIf
  • Modify minMemoryGB, maxMemoryGB, minvCPUs, maxvCPUs as desired for your usecase
  • You can adjust the preconfigured allowedVMTypes array to only allow specific VM types, by default it contains “Standard_D2ds_v4″,”Standard_D4ds_v4″,”Standard_D8ds_v4″,”Standard_D2ds_v5″,”Standard_D4ds_v5″,”Standard_D8ds_v5″,”Standard_E2ds_v4″,”Standard_E4ds_v4″,”Standard_E8ds_v4″,”Standard_E2ds_v5″,”Standard_E4ds_v5″,”Standard_E8ds_v5”. Overwrite it by using the following parameter:
    -allowedVMTypes @(“Standard_D4ds_v4″,”Standard_D8ds_v4”)
  • use -maintenanceWindowStartHour, -maintenanceWindowLengthInHours and –maintenanceWindowDay if you want to ignore performance data during a maintenance window (e.g. for patching) as that isn’t representative
  • Set an Azure Tag called LCRightSizeConfig with the value disabled on machines you want to ignore
  • Set an Azure Tag called LCRightSizeConfig with a machine type value (e.g. “Standard_D4ds_v4“) if you want to lock a specific size for that machine, this can be useful if you want the script to resize from current to target automatically when it runs while the VM has been deallocated.

Example -Verbose output of two VM’s being resized:

Requirements:

The module requires that you’ve added the % Processor Time and Available MBytes performance counters to Log Analytics:

and that your host(s) have the Azure Monitor agent installed.

The module will check if there is sufficient data about the machine in Azure Monitor, if not, no action will be taken. You can determine how far back the function looks by modifying $measurePeriodHours

If you’re using the more recent Azure Monitoring agent, add the perf counters here:

Required access

Virtual Machine Contributor to the resource group(s) containing your VM’s and Log Analytics Reader on your log analytics workspace.

Download / Installation

Option 1: Install-Module ADDRS

Option 2: get relevant functions/code from Git

and run the set-vmRightSize or set-rsgRightSize function, e.g.:

set-vmRightSize -targetVMName azvm01 -workspaceId 7ccd0949-2fd4-414e-b58c-c013cc6e445d

set-vmRightSize -targetVMName azvm01 -workspaceId 7ccd0949-2fd4-414e-b58c-c013cc6e445d -allowedVMTypes (“Standard_E8ds_v4″,”Standard_E2ds_v5″,”Standard_E4ds_v5″,”Standard_E8ds_v5”)

set-rsgRightSize -targetRSG rg-avd-we-01 -workspaceId 7ccd0949-2fd4-414e-b58c-c013cc6e445d

Scheduling

If you wish to run this automatically on a schedule, I recommend either using an Azure DevOps pipeline or Automation account. I’ve compiled a small guide on how to use ADDRS in an Azure Automation Account.

Right Sizing Frequency

It is recommended to match job schedules to the lookback period, or at least not run multiple times in the same lookback period. Otherwise, the data that is being used for sizing may not be representative if the machine had already been resized in an earlier run. By default the script will prevent this from happening by checking each vm’s audit log entries.

Issues / notes

  • Make sure you’ve got enough data in Log Analytics
  • Make sure the allowedVMTypes list contains only VM types that you can actually upgrade to. If e.g. your VM has an ephemeral disk, and your allowList has types that do not, the resize will fail with an error message (but no harm will be done to the existing VM)
  • I’ve only tested the maintenance window parameters using UTC time, if you’re using different timezones your results in excluding data generated during the maintenance window may vary from mine
  • Spot and Low Priority Azure pricing is excluded by default

Convert FSLogix profile to local profile

FSLogix, the defacto profile management solution for Azure Virtual Desktop, allows you to easily roam profiles between different Azure Virtual Desktops.

A solution to convert profiles to FSLogix profiles is available, but for the reverse, I couldn’t find anything.

In some situations, e.g. dedicated VDI’s, a local profile may perform better and reduce infrastructure complexity (no share required). Especially in larger environments with thousands of VDI’s the required performance of the profile share is very high.

I wanted to test converting / migrating back from FSLogix profiles to local profiles on a specific machine, which resulted in a simple PowerShell script that can be executed using Run Command or by an admin on the VM.

It mounts the profile of a given user, copies the data to the local VM, unmounts the profile (so the profile is not deleted!) and sets a bunch of registry keys to properly connect the local profile and disable FSLogix roaming on that VM for that user.

See GitLab

Correct SessionDesktop friendlyname using AVD Rest API

When you deploy an Azure Virtual Desktop application group with the default desktop through ARM, the FriendlyName attribute is not respected, and remains at the default value of SessionDesktop.

This is easy to correct manually in the portal, but as I don’t want my admins having modify rights there, I introduced an extra pipeline step (YAML/Azure DevOps) to uses the Az module’s REST command to correctly set the FriendlyName of the SessionDesktop:

    - task: AzureCLI@2
      displayName: Correct app name
      inputs:
        azureSubscription: ${{ parameters.serviceConnection }}
        scriptType: ps
        scriptLocation: inlineScript
        inlineScript: |
          az rest --method PATCH --uri 'https://management.azure.com/subscriptions/${{ parameters.subscriptionId }}/resourceGroups/${{ parameters.resourceGroupName }}/providers/Microsoft.DesktopVirtualization/applicationGroups/ag-myappgroupname-01/desktops/SessionDesktop?api-version=2021-01-14-preview' --body '{""properties"":{""description"": ""Descriptive Tekst"",""friendlyName"": ""DevOps desktop""}}'

The API used is documented here: https://docs.microsoft.com/en-us/rest/api/desktopvirtualization/desktops/update

Lightweight LAPS solution for INtune (MEM)

UPDATE: LeanLAPS has finally been ‘superceded’ by Microsoft’s own LAPS 🙂 https://techcommunity.microsoft.com/t5/microsoft-entra-azure-ad-blog/introducing-windows-local-administrator-password-solution-with/ba-p/1942487

The main differences between Microsoft AAD LAPS and LeanLAPS:

  1. MS Won’t enable the account if it’s disabled.
  2. MS Won’t create the account if it doesn’t exist.
  3. MS Won’t add it to Administrators group if it’s not a member.
  4. MS Won’t remove accounts from Administrators group if they’re not supposed to be there.
  5. MS has an AAD integrated GUI and RBAC
  6. LeanLAPS requires P2 licensing because it used Proactive Remediations

LeanLAPS

Managing local admin accounts using Intune has a lot of quirks, my tele-colleague Rudy Ooms has already written extensively about this. He also wrote a PowerShell solution to rotate a specific local admin’s password and had the genius idea of using Proactive Remediations (a MEM feature) to display passwords to admins, integrated / free in the Intune Console.

However, I felt I needed a more lightweight solution that;

  • does not require/modify registry keys
  • does not store the password locally
  • can encrypt the password if desired
  • does not need separate detection and remediation scripts
  • automatically provisions a local admin account
  • can remove any other local admin accounts if desired
  • can whitelist approved admins or groups from AzureAD or Active Directory
  • is language/locale-agnostic (e.g. ‘Administrators’ vs ‘Administradores’….)

Thus LeanLAPS was born!

To install/use:

1. head into the Proactive Remediations section of MDE and click Create script package:

2. Fill out some details:

3. Download and doublecheck the config of LeanLAPS.ps1 (e.g. configure if other local admins should be removed, what the local admin name should be and the password length). Make sure to use NotePad++ / that the file stays UTF-8 Encoded without a BOM.

4. Set both the detection and remediation script to LeanLAPS.ps1 and run it in 64 bit:

5. Assign to a group and deploy. By default it will run every day, but you can also let it run more or less frequently, which determines how often the password is reset (hourly in below example):

6. Deploy, and then click on the script package:

7. Go to Device status and add both output columns:

Congratulations, you can now see the current local admin passwords for all managed Windows 10 devices!

Note: if you wish to trigger a quick remediation, delete the correct keys under Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts\Execution and Reports in the client’s registry, then restart the IntuneManagementExtension service and the remediation will re-run within 5 minutes.

8. If you want to display an encrypted password in Intune, generate a public and private key and configure the resulting values in gui.ps1 and leanLAPS.ps1

9. If you don’t want LeanLAPS to remove certain preapproved admins or groups as admin, make sure to configure the $approvedAdmins variable.

RBAC

If you provide e.g. your helpdesk with the correct Intune roles, they will be able to see local admin passwords as reported by above solution:

GUI

The community, in the form of Colton Lacy, also added an optional GUI frontend for LeanLAPS which you could use for e.g. helpdesk staff:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/leanLAPS/gui.ps1

Troubleshooting

If your passwords don’t rotate correctly, check https://smsagent.blog/2021/04/27/a-case-of-the-unexplained-intune-password-policy-and-forced-local-account-password-changes/

WVD Hostpool ExpirationTime

Spent some time troubleshooting an issue deploying new VM’s to our Windows Virtual Desktop hostpool today, and since the DSC extension that adds the WVD host to the hostpool completed successfully but the VM’s didn’t join the hostpool, here’s the error for those googling it in the future:

No match for RegistrationKey found in the meta mof file

The reason the RegistrationKey never made it to the extension ended up being an expirationTime set “only” 6 hours in the future. Apparently, one of the steps isn’t actually executed in our target zone (West Europe). Setting the host pool token expiration time to 20 hours resolved above error.

The full DSC extension log in Azure:

[
    {
        "code": "ComponentStatus/DscConfigurationLog/succeeded",
        "level": "Info",
        "displayStatus": "Provisioning succeeded",
        "message": "[2021-06-03 10:37:48Z] [VERBOSE] [REDACTED]: LCM:  [ Start  Set      ]  [[Script]ExecuteRdAgentInstallClient]\r\n[2021-06-03 10:37:48Z] [VERBOSE] [REDACTED]:                            [[Script]ExecuteRdAgentInstallClient] Performing the operation \"Set-TargetResource\" on target \"Executing the SetScript with the user supplied credential\".\r\n[2021-06-03 10:38:05Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]  [[Script]ExecuteRdAgentInstallClient]  in 16.5940 seconds.\r\n[2021-06-03 10:38:05Z] [VERBOSE] [REDACTED]: LCM:  [ End    Resource ]  [[Script]ExecuteRdAgentInstallClient]\r\n[2021-06-03 10:38:05Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]\r\n[2021-06-03 10:38:05Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]    in  17.3440 seconds.\r\n[2021-06-03 10:38:05Z] [VERBOSE] Operation 'Invoke CimMethod' complete.\r\n[2021-06-03 10:38:05Z] [VERBOSE] Time taken for configuration job to complete is 17.478 seconds\r\n[2021-06-03 10:38:08Z] [VERBOSE] Performing the operation \"Start-DscConfiguration: SendMetaConfigurationApply\" on target \"MSFT_DSCLocalConfigurationManager\".\r\n[2021-06-03 10:38:08Z] [VERBOSE] Perform operation 'Invoke CimMethod' with following parameters, ''methodName' = SendMetaConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' = root/Microsoft/Windows/DesiredStateConfiguration'.\r\n[2021-06-03 10:38:08Z] [VERBOSE] An LCM method call arrived from computer REDACTED with user sid S-1-5-18.\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ Start  Set      ]\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ Start  Resource ]  [MSFT_DSCMetaConfiguration]\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ Start  Set      ]  [MSFT_DSCMetaConfiguration]\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]  [MSFT_DSCMetaConfiguration]  in 0.0160 seconds.\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ End    Resource ]  [MSFT_DSCMetaConfiguration]\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]\r\n[2021-06-03 10:38:08Z] [VERBOSE] [REDACTED]: LCM:  [ End    Set      ]    in  0.0790 seconds.\r\n[2021-06-03 10:38:08Z] [VERBOSE] Operation 'Invoke CimMethod' complete.\r\n[2021-06-03 10:38:08Z] [VERBOSE] Set-DscLocalConfigurationManager finished in 0.194 seconds."
    },
    {
        "code": "ComponentStatus/DscExtensionLog/succeeded",
        "level": "Info",
        "displayStatus": "Provisioning succeeded",
        "message": "[2021-06-03 10:37:23Z] Creating Working directory: C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\bin\\..\\DSCWork\\Configuration.0\r\n[2021-06-03 10:37:23Z] Downloading configuration package\r\n[2021-06-03 10:37:24Z] Downloading https://raw.githubusercontent.com/Azure/RDS-Templates/master/ARM-wvd-templates/DSC/Configuration.zip to C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\bin\\..\\DSCWork\\Configuration.0\\Configuration.zip\r\n[2021-06-03 10:37:41Z] Extracting Configuration.zip\r\n[2021-06-03 10:37:41Z] Looking for the definition of the configuration function.\r\n[2021-06-03 10:37:41Z] Executing C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\bin\\..\\DSCWork\\Configuration.0\\Configuration.ps1\r\n[2021-06-03 10:37:41Z] Preparing configuration arguments and configuration data.\r\n[2021-06-03 10:37:41Z] Creating MOF files.\r\n[2021-06-03 10:37:41Z] Executing the configuration function to generate the MOF files.\r\n[2021-06-03 10:37:42Z] Verifying metaconfiguration for reboot information...\r\n[2021-06-03 10:37:42Z] Backing up C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\DSCWork\\Configuration.0\\AddSessionHost\\localhost.meta.mof\r\n[2021-06-03 10:37:42Z] No match for RegistrationKey found in the meta mof file\r\n[2021-06-03 10:37:42Z] WMF 5 or newer, Injecting RebootNodeIfNeeded = False and ActionAfterReboot = \"StopConfiguration\"\r\n[2021-06-03 10:37:42Z] Executing Set-DscLocalConfigurationManager...\r\n[2021-06-03 10:37:44Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:37:47Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:37:47Z] Get-DscLocalConfigurationManager: \r\n\r\nActionAfterReboot              : StopConfiguration\r\nAgentId                        : ADF6E52A-C457-11EB-9BDA-000D3A2633E7\r\nAllowModuleOverWrite           : False\r\nCertificateID                  : \r\nConfigurationDownloadManagers  : {}\r\nConfigurationID                : \r\nConfigurationMode              : ApplyOnly\r\nConfigurationModeFrequencyMins : 15\r\nCredential                     : \r\nDebugMode                      : {NONE}\r\nDownloadManagerCustomData      : \r\nDownloadManagerName            : \r\nLCMCompatibleVersions          : {1.0, 2.0}\r\nLCMState                       : Idle\r\nLCMStateDetail                 : \r\nLCMVersion                     : 2.0\r\nStatusRetentionTimeInDays      : 10\r\nSignatureValidationPolicy      : NONE\r\nSignatureValidations           : {}\r\nMaximumDownloadSizeMB          : 500\r\nPartialConfigurations          : \r\nRebootNodeIfNeeded             : False\r\nRefreshFrequencyMins           : 30\r\nRefreshMode                    : PUSH\r\nReportManagers                 : {}\r\nResourceModuleManagers         : {}\r\nPSComputerName                 : \r\n\r\n\r\n\r\n\r\n[2021-06-03 10:37:47Z] Executing Start-DscConfiguration...\r\n[2021-06-03 10:37:47Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:38:07Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:38:07Z] Updating execution status (HKLM:\\SOFTWARE\\Microsoft\\Azure\\DSC\\2.83.1.0\\Status)\r\n[2021-06-03 10:38:07Z] LCM state is Idle\r\n[2021-06-03 10:38:07Z] DSC configuration completed.\r\n[2021-06-03 10:38:07Z] Resetting metaconfiguration...\r\n[2021-06-03 10:38:07Z] Restoring C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\DSCWork\\Configuration.0\\AddSessionHost\\localhost.meta.mof.bk...\r\n[2021-06-03 10:38:07Z] Executing Set-DscLocalConfigurationManager...\r\n[2021-06-03 10:38:07Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:38:10Z] Settings handler status to 'transitioning' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)\r\n[2021-06-03 10:38:10Z] Get-DscLocalConfigurationManager: \r\n\r\nActionAfterReboot              : ContinueConfiguration\r\nAgentId                        : ADF6E52A-C457-11EB-9BDA-000D3A2633E7\r\nAllowModuleOverWrite           : False\r\nCertificateID                  : \r\nConfigurationDownloadManagers  : {}\r\nConfigurationID                : \r\nConfigurationMode              : ApplyOnly\r\nConfigurationModeFrequencyMins : 15\r\nCredential                     : \r\nDebugMode                      : {NONE}\r\nDownloadManagerCustomData      : \r\nDownloadManagerName            : \r\nLCMCompatibleVersions          : {1.0, 2.0}\r\nLCMState                       : Idle\r\nLCMStateDetail                 : \r\nLCMVersion                     : 2.0\r\nStatusRetentionTimeInDays      : 10\r\nSignatureValidationPolicy      : NONE\r\nSignatureValidations           : {}\r\nMaximumDownloadSizeMB          : 500\r\nPartialConfigurations          : \r\nRebootNodeIfNeeded             : True\r\nRefreshFrequencyMins           : 30\r\nRefreshMode                    : PUSH\r\nReportManagers                 : {}\r\nResourceModuleManagers         : {}\r\nPSComputerName                 : \r\n\r\n\r\n\r\n\r\n[2021-06-03 10:38:10Z] Settings handler status to 'success' (C:\\Packages\\Plugins\\Microsoft.Powershell.DSC\\2.83.1.0\\Status\\0.status)"
    },
    {
        "code": "ComponentStatus/Metadata/succeeded",
        "level": "Info",
        "displayStatus": "Provisioning succeeded",
        "message": "VMUUId=6DFC142E-0A63-41D1-BE3E-31F5D0D9D8A3;AgentId=BDF6E52A-C427-12EB-9BDA-001D3A2633E7;"
    }
]