Automating Azure Virtual Desktop App Attach with PowerShell

Last Updated on December 6, 2025

Managing Azure Virtual Desktop (AVD) App attach apps can quickly become complex when you’re dealing with dozens of applications, host pools, and user groups. This article explains how to streamline that process using PowerShell automation.

The goal is simple: make App Attach lifecycle management repeatable, scalable, and less error‑prone.

Export First, Then Automate

I recommend starting by creating one App Attach app manually in the Azure portal. Once you have that baseline, you can use the scripts below to export all existing App Attach apps into a CSV file.

This CSV will serve as the input for creating multiple App Attach apps later in this article.

If you only want to create a single App Attach app, you don’t need to manually create one first or extract it into a CSV. You can skip ahead directly to the Create Single App Attach App section.

Get-AllAppAttachStep1.ps1 (Export all appattach apps)

This script quickly exports all App Attach apps in a resource group to a CSV (AppAttachApps_Fast.csv). It includes:

  • App metadata (name, path, version, etc.)
  • Host pool assignments (names)
  • User group IDs (fast to collect, no Graph lookups)

Prerequisites

Note

  • Two columns (hostpool_packagepull and resourcegroup_hostpool_packagepull) are intentionally left empty in both Step 1 and Step 2 export scripts because the App Attach package object does not store the original host pool and resource group used during package import.
  • When you later use the exported CSV as input for Create-MultipleAppAttach.ps1, you can manually fill in these columns with the correct values for each app. This ensures the script knows which host pool to pull package information from (hostpool_packagepull) and which resource group that host pool resides in (resourcegroup_hostpool_packagepull).
  • Execution time depends on the number of App Attach apps in your environment. The more apps you have, the longer it will take to enumerate them
# Connect to Azure
Connect-AzAccount
Import-Module Az.DesktopVirtualization

# You can define multiple resource groups here by separating them with commas inside an array.
# Example: $resourceGroup = @("rg-avd-sam-uks-pool-compute","rg-avd-din-uks-service-obj","rg-avd-nut-uks-service-obj"
$resourceGroup = "<Name of the Resource group>"
$apps = Get-AzWvdAppAttachPackage -ResourceGroupName $resourceGroup

$export = foreach ($app in $apps) {
    $hostpools = ($app.HostPoolReference | ForEach-Object { ($_ -split '/')[-1] }) -join ';'

    $roleAssignments = Get-AzRoleAssignment -Scope $app.Id -RoleDefinitionName "Desktop Virtualization User" -ErrorAction SilentlyContinue
    $usergroupIds = ($roleAssignments | Select-Object -ExpandProperty ObjectId) -join ';'

    [PSCustomObject]@{
        app_name                        = $app.Name
        path                            = $app.ImagePath
        location                        = $app.Location
        resourcegroup_publish           = $app.ResourceGroupName
        hostpool_assign                 = $hostpools
        hostpool_packagepull            = ""   # intentionally empty
        resourcegroup_hostpool_packagepull = "" # intentionally empty
        usergroup_assign_ids            = $usergroupIds
        ImagePackageFamilyName          = $app.ImagePackageFamilyName
        ImagePackageApplication         = $app.ImagePackageApplication
        ImageVersion                    = $app.ImageVersion
        ImageIsActive                   = $app.ImageIsActive
        SystemDataCreatedAt             = $app.SystemDataCreatedAt
    }
}

$export | Export-Csv -Path "C:\Temp\AppAttachApps_Fast.csv" -NoTypeInformation

Get-AllAppAttachStep2.ps1 (Export all app attach apps)

This script:

  • Reads the Step 1 CSV and enriches it by resolving user group IDs into display names via Microsoft Graph.
  • Uses caching to avoid repeated Graph calls, but will still take longer than Step 1.
  • Outputs file: AppAttachApps_Complete.csv

Prerequisites

# Connect to Graph
Connect-MgGraph -Scopes 'Group.Read.All'

$apps = Import-Csv "C:\Temp\AppAttachApps_Fast.csv"
$groupCache = @{}

$enriched = foreach ($app in $apps) {
    $ids = $app.usergroup_assign_ids -split ';'
    $names = @()

    foreach ($id in $ids) {
        if (-not [string]::IsNullOrWhiteSpace($id)) {
            if (-not $groupCache.ContainsKey($id)) {
                try {
                    $grp = Get-MgGroup -GroupId $id -ErrorAction SilentlyContinue
                    $groupCache[$id] = $grp.DisplayName
                } catch {
                    $groupCache[$id] = $null
                }
            }
            if ($groupCache[$id]) { $names += $groupCache[$id] }
        }
    }

    [PSCustomObject]@{
        app_name                        = $app.app_name
        path                            = $app.path
        location                        = $app.location
        resourcegroup_publish           = $app.resourcegroup_publish
        hostpool_assign                 = $app.hostpool_assign
        hostpool_packagepull            = ""   # intentionally empty
        resourcegroup_hostpool_packagepull = "" # intentionally empty
        usergroup_assign                = ($names -join ';')
        ImagePackageFamilyName          = $app.ImagePackageFamilyName
        ImagePackageApplication         = $app.ImagePackageApplication
        ImageVersion                    = $app.ImageVersion
        ImageIsActive                   = $app.ImageIsActive
        SystemDataCreatedAt             = $app.SystemDataCreatedAt
    }
}

$enriched | Export-Csv -Path "C:\Temp\AppAttachApps_Complete.csv" -NoTypeInformation

Create Multiple App Attach Apps

Once you have your exported CSV, you can use it as input to bulk creation. This is where the Create-MultipleAppAttach.ps1 script comes in. It reads the CSV, publishes apps, assigns host pools, and maps user groups — all in one go.

This approach is perfect when you want to onboard a batch of applications consistently across your environment.

Prerequisites

Note

  • Make sure that hostpool_packagepull and resourcegroup_hostpool_packagepull are filled correctly.
  • If hostpool_packagepull or resourcegroup_hostpool_packagepull are empty, package import will fail.
  • User group resolution uses Graph search by display name. Ensure group names in the CSV match Azure AD exactly.
  • The script includes commented sections for unassigning host pools, unassigning user groups, and removing apps if needed.

Create-MultipleAppAttach.ps1

# Connect to Azure
Connect-AzAccount #-DeviceCode
Connect-MgGraph -Scopes 'Group.Read.All'
Import-Module Az.DesktopVirtualization

# Import CSV with app definitions
$apps = Import-Csv "C:\Temp\AppAttachApps_Complete.csv"

foreach ($row in $apps) {
    Write-Output "Processing app: $($row.app_name)"
    
    if (-not $row.app_name) { continue }  # skip blank rows
    
    # Parameters to fetch package properties
    $parameters_package = @{
        HostPoolName      = $row.hostpool_packagepull
        ResourceGroupName = $row.resourcegroup_hostpool_packagepull
        Path              = $row.path
    }

    $app = Import-AzWvdAppAttachPackageInfo @parameters_package

    # Optional: check if multiple package objects returned
    $app | Format-List *

    # Parameters to publish the App Attach app
    $parameters_publish = @{
        Name                          = $row.app_name
        ResourceGroupName             = $row.resourcegroup_publish
        Location                      = $row.location
        FailHealthCheckOnStagingFailure = "NeedsAssistance"
        ImageIsRegularRegistration    = $false
        ImageDisplayName              = $row.app_name
        ImageIsActive                 = $true
    }

    # Create App Attach app
    $newApp = New-AzWvdAppAttachPackage -AppAttachPackage $app @parameters_publish

    # Validate newly created app
    $parameters_fetch = @{
        Name              = $row.app_name
        ResourceGroupName = $row.resourcegroup_publish
    }

    Get-AzWvdAppAttachPackage @parameters_fetch |
        Format-List Name, ImagePackageApplication, ImagePackageFamilyName, ImagePath, ImageVersion, ImageIsActive, ImageIsRegularRegistration, SystemDataCreatedAt

   # Assign hostpool(s) to an Appattach app
    if ($row.hostpool_assign) {
        $hostpoolIds = @()
        $hostpools = $row.hostpool_assign -split ';'
        foreach ($hp in $hostpools) {
            $hostpoolIds += (Get-AzWvdHostPool | Where-Object Name -eq $hp).Id
    }

    $parameters_hostpool_assign = @{
        Name              = $row.app_name
        ResourceGroupName = $row.resourcegroup_publish
        HostPoolReference = $hostpoolIds
    }

    Update-AzWvdAppAttachPackage @parameters_hostpool_assign
    }

    # Assign user groups to the Appattach app
    if ($row.usergroup_assign) {
        $usergroupIds = @()

        # Split the semicolon-separated list from the CSV
        $groups = $row.usergroup_assign -split ';'
        foreach ($grp in $groups) {
            Write-Output "Resolving user group: $grp"
            $usergroupIds += (Get-MgGroup -Search "DisplayName:$grp" -ConsistencyLevel eventual).Id
        }

        # Fetch the App Attach package object
        $appAttachPackage = Get-AzWvdAppAttachPackage -Name $row.app_name -ResourceGroupName $row.resourcegroup_publish

        # Assign each group to the app
        foreach ($groupId in $usergroupIds) {
            Write-Output "Assigning $($row.app_name) to user group ID: $groupId"
            New-AzRoleAssignment -ObjectId $groupId `
                                 -RoleDefinitionName "Desktop Virtualization User" `
                                 -Scope $appAttachPackage.Id
        }
    }

    ## --------------------- Start [Unassign hostpools from Appattach app] --------------------- ##
    # To clear host pool assignments for a given app, set HostPoolReference to an empty array.
    # This removes all host pool associations.
    # Uncomment to use.

    # $parameters_hostpool_unassign = @{
    #     Name              = $row.app_name
    #     ResourceGroupName = $row.resourcegroup_publish
    #     HostPoolReference = @()
    # }
    # Update-AzWvdAppAttachPackage @parameters_hostpool_unassign
    ## --------------------- End [Unassign hostpools from Appattach app] ----------------------- ##


    ## --------------------- Start [Unassign usergroups from Appattach app] -------------------- ##
    # Fetch the App Attach package object
    # $appAttachPackage = Get-AzWvdAppAttachPackage -Name $row.app_name -ResourceGroupName $row.resourcegroup_publish

    # Loop through previously resolved user group IDs and remove role assignments
    # foreach ($groupId in $usergroupIds) {
    #     Remove-AzRoleAssignment -ObjectId $groupId `
    #                             -RoleDefinitionName "Desktop Virtualization User" `
    #                             -Scope $appAttachPackage.Id
    # }
    ## --------------------- End [Unassign usergroups from Appattach app] ---------------------- ##


    ## --------------------- Start [Remove Appattach app] -------------------------------------- ##
    # To delete the App Attach app object entirely:
    # Remove-AzWvdAppAttachPackage -Name $row.app_name -ResourceGroupName $row.resourcegroup_publish
    ## --------------------- End [Remove Appattach app] ---------------------------------------- ##

}

Create Single App Attach App

For scenarios where you only need to publish one app, you can use the Create-SingleAppAttach.ps1 script. This skips the CSV step and directly creates and assigns the app.

Prerequisites

Note

  • Assignments overwrite existing ones. To update, specify the full list of host pools or groups.
  • Commented sections in the script can be enabled to unassign or remove apps.
  • Best practice in enterprise AVD deployments is to separate resource groups for host pools, session hosts, networking, storage, and monitoring.
  • $app | Format-List * – If multiple package objects are returned (e.g., x64 and x86 versions), use the PackageFullName parameter to select the correct package.
$app = Import-AzWvdAppAttachPackageInfo @parameters_package | Where-Object { $_.ImagePackageFullName -like "*$packageFullName*" }

Create-SingleAppAttach.ps1

Connect-AzAccount
Connect-MgGraph -Scopes 'Group.Read.All'
Import-Module Az.DesktopVirtualization

$app_name = <Application name>
$path = <Path to the CIM, VHDX, or AppV package file>
$location = <Azure region>
$hostpool_packagepull = <Host pool used to pull package info>
$resourcegroup_hostpool_packagepull = <Resource group of the Host pool>
$resourcegroup_publish = <Resource group where the App Attach app object will reside>
$hostpool_assign = <Comma-separated list of host pools to assign>
$usergroup_assign = <Comma-separated list of user groups to assign>

# Parameters to fetch package properties from the host pool and resource group
$parameters_package = @{
    HostPoolName      = $hostpool_packagepull
    ResourceGroupName = $resourcegroup_hostpool_packagepull
    Path              = $path
}

$app = Import-AzWvdAppAttachPackageInfo @parameters_package

# Verify the imported package info.
$app | Format-List *

# Parameters to publish the App Attach app
$parameters_publish = @{
    Name                          = $app_name
    ResourceGroupName             = $resourcegroup_publish # Resource group where the app object will reside.
    Location                      = $location
    FailHealthCheckOnStagingFailure = "NeedsAssistance"
    ImageIsRegularRegistration    = $false
    ImageDisplayName              = $app_name
    ImageIsActive                 = $true
}

# Create the App Attach app
New-AzWvdAppAttachPackage -AppAttachPackage $app @parameters_publish

# Parameters to validate the newly created App Attach app
$parameters_fetch = @{
    Name              = $app_name
    ResourceGroupName = $resourcegroup_publish
}

# Fetch and display key properties of the published app
Get-AzWvdAppAttachPackage @parameters_fetch | 
    Format-List Name, ImagePackageApplication, ImagePackageFamilyName, ImagePath, ImageVersion, ImageIsActive, ImageIsRegularRegistration, SystemDataCreatedAt

# Assign hostpool(s) to an Appattach app
$hostpoolIds = @()
foreach ($hostpoolName in $hostpool_assign) {
    $hostpoolIds += (Get-AzWvdHostPool | ? Name -eq $hostpoolName).Id
}

$parameters_hostpool_assign = @{
    Name = $app_name
    ResourceGroupName = $resourcegroup_publish
    HostPoolReference = $hostpoolIds
}

Update-AzWvdAppAttachPackage @parameters_hostpool_assign

# Assign user groups to the Appattach app
$UsergroupIds = @()

foreach ($group in $usergroup_assign) {
   $usergroupIds += (Get-MgGroup -Search "DisplayName:$group" -ConsistencyLevel: eventual).Id
}

$appAttachPackage = Get-AzWvdAppAttachPackage -Name $app_name -ResourceGroupName $resourcegroup_publish

foreach ($groupId in $usergroupIds) {
   New-AzRoleAssignment -ObjectId $groupId -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id
}


## --------------------- Start [Unassign hostpools from Appattach app] --------------------- ##
# $parameters_hostpool_assign = @{
#    Name = $app_name
#    ResourceGroupName = $resourcegroup_publish
#    HostPoolReference = @()
# }
# Update-AzWvdAppAttachPackage @parameters_hostpool_assign
## --------------------- End [Unassign hostpools from Appattach app] ----------------------- ##

## --------------------- Start [Unassign usergroups from Appattach app] -------------------- ##
# $appAttachPackage = Get-AzWvdAppAttachPackage -Name $app_name -ResourceGroupName $resourcegroup_publish

# foreach ($groupId in $usergroupIds) {
#   Remove-AzRoleAssignment -ObjectId $groupId -RoleDefinitionName "Desktop Virtualization User" -Scope $appAttachPackage.Id
# }
## --------------------- End [Unassign usergroups from Appattach app] ---------------------- ##

## --------------------- Start [Remove Appattach app] -------------------------------------- ##
# Remove-AzWvdAppAttachPackage -Name $app_name -ResourceGroupName $resourcegroup_publish
## --------------------- End [Remove Appattach app] ---------------------------------------- ##

Feel free to contact me or leave a comment if any help is required.

Be the first to reply

Leave a Reply