Category Archives: ARM

Publishing an MSIX as CIM to AVD in a Pipeline

I wanted to put this out there as it felt like a nifty way to pipeline AVD MSIX files into AVD without any user interaction (other than a pipeline kicking off the script).

https://github.com/jflieben/assortedFunctionsV2/blob/main/publish-MSIXPackageToHostpool.ps1

Basically, above will grab the MSIX file from a known Azure Fileshare (after mounting). It’ll read the MSIX’s primary CIM file for meta data, use the Azure Rest API to add it to the hostpool and then updates a param file of an ARM template which can be used to e.g. update the appgroup in Azure.

You’ll need some background knowledge to re-use above in your specific situation ūüôā

Code example:

#create the MSIX package object in the hostpool. Ensure the lastUpdated value is always unique otherwise it will fail to overwrite an existing package with the same value
$apiPostData = @{
    "properties" = @{
        "displayName" = if($packageMeta -match "(?<=<DisplayName>)(.*?)(?=<\/DisplayName>)"){$matches[1]}else{Throw "No display name found in AppManifest"}
        "imagePath" = $imagePath
        "isActive" = $True
        "isRegularRegistration" = $False
        "lastUpdated" = (get-itemproperty $packageFolder.FullName).LastWriteTimeUtc.AddSeconds((Get-Random -Minimum "-150" -Maximum 150)).ToString("yyyy-MM-ddThh:mm:ss")
        "packageApplications" = $packageApplications
        "packageDependencies" = @()
        "packageFamilyName" = "$($packageShortName)_$($packageFamily)"
        "packageName" = $packageShortName
        "packageRelativePath" = "\MSIXPackages\$($packageFolder.Name)"
        "version" = $packageVersion
    }
}

#send the actual API request to register the package in the hostpool using the pipeline serviceprincipal
try{
    $context = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext
    $token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id.ToString(), $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, "https://management.azure.com")          
    Invoke-RestMethod -Uri "https://management.azure.com/subscriptions/$((get-azcontext).Subscription.id)/resourcegroups/rg-common-$($environment)-weeu-01/providers/Microsoft.DesktopVirtualization/hostPools/vdhp-common-$($environment)-weeu-01/msixPackages/$($packageFolder.Name)?api-version=2021-07-12" -Method PUT -UseBasicParsing -ContentType "application/json" -Body ($apiPostData | convertto-json -Depth 15) -Headers @{"Authorization"="Bearer $($token.AccessToken)"} -ErrorAction Stop
}catch{
    Write-Output $_
    closeCIMSession
    Throw
}

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