Category Archives: ARM

Trigger logic app when Azure Virtual Desktop starts

We have several use cases where we want to “do something” when a user starts an Azure Virtual Desktop. One method could be a login/startup script, but this would run under the user’s or Managed Identity’s context.

A better way is to use an Azure Event Grid System Topic on the resource group that contains the VM’s, which can then forward any event that happens in the resource group.

A system topic is easily deployed using ARM:

    {
        "type": "Microsoft.EventGrid/systemTopics",
        "apiVersion": "2021-12-01",
        "name": "evgt-listenToAvdEvents-01",
        "location": "global",
        "properties": {
            "source": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01')]",
            "topicType": "microsoft.resources.resourcegroups"
        }
    }

That having been deployed, we’ll deploy a logic app that is triggered by the topic. In this case, I want to do some advanced filtering so the logic app is only triggered when a VM is started by a user (vs automation). This is indicated by the Guid (principal ID) of Microsoft’s AVD serviceprincipal, in our case 068e1c948d874baba249f9a122cd8003 because we use ‘Start On Connect

To use advanced filtering in a logic app, use “enableAdvancedFilteringOnArrays”: true

The full trigger section of the logic app (in ARM) is as follows:

                    "triggers": {
                        "When_a_resource_event_occurs": {
                            "splitOn": "@triggerBody()",
                            "type": "ApiConnectionWebhook",
                            "inputs": {
                                "body": {
                                    "properties": {
                                        "destination": {
                                            "endpointType": "webhook",
                                            "properties": {
                                                "endpointUrl": "@{listCallbackUrl()}"
                                            }
                                        },
                                        "filter": {
                                            "includedEventTypes": [
                                                "Microsoft.Resources.ResourceActionSuccess",
                                                "Microsoft.Resources.ResourceDeleteSuccess",
                                                "Microsoft.Resources.ResourceWriteSuccess"
                                            ],
                                            "subjectBeginsWith": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01/providers/Microsoft.Compute/virtualMachines')]",
                                            "enableAdvancedFilteringOnArrays": true,
                                            "advancedFilters": [
                                                {
                                                    "operatorType": "StringIn",
                                                    "key": "data.authorization.action",
                                                    "values": [
                                                        "Microsoft.Compute/virtualMachines/start/action"
                                                    ]
                                                },
                                                {
                                                    "operatorType": "StringIn",
                                                    "key": "data.authorization.evidence.principalId",
                                                    "values": [
                                                        "068e1c948d874baba249f9a122cd8003"
                                                    ]
                                                }
                                            ]
                                        },
                                        "topic": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01')]"
                                    }
                                },
                                "host": {
                                    "connection": {
                                        "name": "@parameters('$connections')['azureeventgrid']['connectionId']"
                                    }
                                },
                                "path": "[concat('/subscriptions/@{encodeURIComponent(''',subscription().subscriptionId,''')}/providers/@{encodeURIComponent(''Microsoft.Resources.ResourceGroups'')}/resource/eventSubscriptions')]",
                                "queries": {
                                    "x-ms-api-version": "2021-12-01"
                                }
                            }
                        }
                    },

You may also want to use the VM’s name in your logic app, this is easily parsed from the Subject field, e.g. as follows:

"Parse_Subject": {
    "runAfter": {},
    "type": "InitializeVariable",
    "inputs": {
        "variables": [
            {
                "name": "subject",
                "type": "string",
                "value": "@triggerBody()?['subject']"
            }
        ]
    }
},
"Parse_MachineName": {
    "runAfter": {
        "Parse_Subject": [
            "Succeeded"
        ]
    },
    "type": "InitializeVariable",
    "inputs": {
        "variables": [
            {
                "name": "machineName",
                "type": "string",
                "value": "@{last(split(variables('subject'),'/'))}"
            }
        ]
    }
},

Important considerations:

  1. the Logic App needs to have a managed identity
  2. The LA’s MI needs to have the EventGrid Contributor role on the system topic
  3. you cannot edit this logic app through the gui, doing so will break it and cause the following error: “Unable to match incoming request to an operation”

Adding eventgrid contributor:

New-AzRoleAssignment -ObjectId $la.Identity.PrincipalId -RoleDefinitionName "EventGrid Contributor" -Scope "/subscriptions/$($context.Subscription.Id)"

Keyvault RBAC model ARM role assignment

Yes, using ARM, not Bicep, I know, it’s bad!

Ran into a whole bunch of constrains and issue trying to assign an array of principals vs roles on keyvault using the RBAC access method, so sharing my working solution here as I couldn’t find a single good example on google:

        {
            "type": "Microsoft.KeyVault/vaults/providers/roleAssignments",
            "apiVersion": "2018-09-01-preview",
            "copy": {
                "name": "rbac-access-policy-loop",
                "count": "[length(parameters('accessPolicies'))]"
            },            
            "name": "[concat(variables('vaultName'),'/Microsoft.Authorization/',guid(concat(variables('vaultName'), parameters('accessPolicies')[copyIndex('rbac-access-policy-loop')].objectId, parameters('accessPolicies')[copyIndex('rbac-access-policy-loop')].roleId)))]",
            "dependsOn": [
                "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]"
            ],
            "properties": {
                "roleDefinitionId": "[concat('/providers/Microsoft.Authorization/roledefinitions/',parameters('accessPolicies')[copyIndex('rbac-access-policy-loop')].roleId)]",
                "principalId": "[parameters('accessPolicies')[copyIndex('rbac-access-policy-loop')].objectId]",
                "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]",
                "principalType": "Group"
            }
        }   

An example param would then look like this:

        "accessPolicies": {
            "value": [
                {
                    "roleId": "b86a8fe4-44ce-4948-aee5-eccb2c155cd7",
                    "objectId": "2d9cbd23-20b1-4921-a8e4-54b55161ad04"
                }                
            ]
        }  

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

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;"
    }
]

Distributing Teams Backgrounds to all users using MEM

I’ve seen a few examples of distributing a set of teams backgrounds to users in MEM, mostly they seemed a little overly complex, especially in targetting all local users and/or packaging the script into an application. I wanted to:

  • Deploy using MEM PS script in user context
  • Avoid access controls on the storage location of the backgrounds
  • Simplify the creation of required Azure resources
  • Have a simple update procedure

Here’s my take, first autocreate a blob storage location using my template:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/blob%20storage%20with%20container%20for%20Teams%20Backgrounds%20and%20public%20access.json

Note down the name you used for the storage account (which was the only parameter to the template), and use it to configure this PowerShell script:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/add-teamsBackgroundsFromBlobContainer.ps1

Finally, deploy the script to a group of your users in MEM, in their own context: