Jump to content


anyweb

Automating Windows 365 part 3 - Provisioning Cloud PC's

Recommended Posts

Introduction

This is Part 3 of a new series of guides which will cover managing Windows 365 Cloud PC's using PowerShell and Microsoft Graph. This mini series should help you get started with automating and managing your Cloud PC's using PowerShell via Microsoft Graph. If you are new to Windows 365 Cloud PC's then please read our previous series called Getting started with Windows 365 available here. At the time of writing, Paul is a 8 times Enterprise Mobility MVP based in the UK and Niall is a 14 times Enterprise Mobility & Windows and Devices MVP based in Sweden.

Below you can find all parts in this series:

The automation used in this part is based upon the manual actions we took in a previous series on Windows 365 here. In this part we'll cover the following:

  • Install Powershell 7
  • Assigning Licenses to Users
  • Adding licensed users to an Entra Id Group
  • Decide which network your Azure AD Joined Cloud PC's will use
  • Create or reuse a Virtual Network (optional)
  • Create an Azure Network Connection (optional)
  • Create a Provisioning Policy
  • Summary

Install Powershell 7

To avoid errors later on, and in order to get the full benefit of Powershell, we'll install Powershell version 7, you can determine your Powershell version using the following code, in Visual Studio Code.

$PSVersionTable

As you can see here, our PC is running an older version of Powershell so it's time to update it.

PSVersionTable.png

You can download Powershell 7 from here.

install powershell 7.png

Once installed, restart Visual Studio Code and check the version again and it should reflect PowerShell version 7.4.6 as below.

PSVersionTable 746.png

Assigning Licenses to Users

You need to assign a Windows 365 license to your users in order for them to use the service, much as you would with any Microsoft 365 product. To do this, open the Microsoft 365 admin center and expand the Billing node, select Licenses, and choose the appropriate Windows 365 product from those you've purchased. When it comes to automation however, we first need to know what SKUS are available in our tenant.

Using the following code, we can list all of those SKUS, this uses the following cmdlet Get-MgSubscribedSku documented here.

# 1. Get the SKUs
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Force -AllowClobber
Connect-MgGraph -Scopes "Organization.Read.All"
Get-MgSubscribedSku | Select-Object SkuId, SkuPartNumber

Launch Visual Studio Code as Administrator and give it a whirl. The results will be displayed similar to the below output.

 

list all available SKUS.png

and here are the corresponding licenses in admin.microsoft.com

licenses in admin center.png

Now that we know the SKU id of our available licenses, we will assign users to the Windows 365 Enterprise 2 vCPU, 8 GB, 128 GB license shown as e2aebe6c-897d-480f-9d62-fff1381581f7 CPC_E_2C_8GB_128GB in the Powershell output.

pointing to this sku.png

In order to do so, drop a file called userId.txt in C:\temp containing the UPN of the users you wish to assign licenses to:
 

# 2. Assign Users to SKU
Connect-MgGraph -Scopes "Group.ReadWrite.All"
# The SKU ID for the license you want to assign
$skuId = "e2aebe6c-897d-480f-9d62-fff1381581f7"
# Path to the text file containing UPNs (one per line)
$userIdsFilePath = "C:\temp\userId.txt"
# Read UPNs from the text file
$userIds = Get-Content -Path $userIdsFilePath
# Loop through each UPN and assign the license
foreach ($userId in $userIds) {
    # Assign the license
    Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$userId/assignLicense" -Body (@{
        addLicenses = @(@{ skuId = $skuId })
        removeLicenses = @()
    } | ConvertTo-Json) > Null
    Write-Host "License assigned to $userId"
}

After running the code above, the result is revealed:

license assigned to upn.png

which can be confirmed in the admin portal

license revealed in admin portal.png

Ok, now that we know how to assign licenses to users, let's automate the creation of an Entra Id group and populate it with users.

Adding licensed users to an Entra Id group
Next, you need to add the licensed user(s) to an Entra Id group, you can name the group whatever you want but it would be a good idea to match the name of your Entra Id group to the Provisioning policy that we will create later in this guide by using a naming convention. In this example, the script will create an Entra Id group called W365 North Europe AAD W11 users via Graph and it will add the licensed user(s) to that group.

By using this naming convention we can quickly determine that members of this group will get a Windows 365 Cloud PC configured for Northern Europe, using Azure AD Join and running Windows 11.

To accomplish this we'll use the following code:

# 3. Add Users to EntraID Group
# Install and connect to Microsoft Graph
Install-Module Microsoft.Graph.Beta.Groups -Force -AllowClobber
Install-Module Microsoft.Graph.Beta.Users -Force -AllowClobber
Connect-MgGraph -Scopes "Group.ReadWrite.All"
# group name
$groupName = "W365 North Europe AAD W11 users via Graph"
# create the group
$GroupParam = @{
                DisplayName = $groupName
                GroupTypes = @()
                SecurityEnabled = $true
                MailEnabled = $false
                MailNickname = (New-Guid).Guid.Substring(0,10)
            }  
New-MgBetaGroup -BodyParameter $GroupParam
# Retrieve the group by name
$group = Get-MgBetaGroup -Filter "displayName eq '$groupName'"
# Path to the text file containing UPNs (one per line)
$userIdsFilePath = "C:\temp\userId.txt"
# Read UPNs from the text file
$userUPNs = Get-Content -Path $userIdsFilePath
# Loop through each UPN and add the user to the group
foreach ($upn in $userUPNs) {
        # Add the user to the group using their object ID    
         $user = Get-MgBetaUser -UserId $upn    
         New-MgBetaGroupMember -GroupId $group.Id -DirectoryObjectId $user.Id   
         Write-Host "User $upn added to group"
    }

The code above not only creates the Entra Id group but then populates it with all users found in the text file.

user added to group that we created.png

Note: If you run the script more than once it will error out, as currently there is no error checking  to verify if the group was already created.

Decide which network your Azure AD joined Cloud PCs will use
You need to decide which network type your Cloud PC's will use for the Azure AD Join scenario. There are 2 choices listed below.
    •  A Microsoft-hosted network
    •  Your own network (using an Azure network connection)
Tip: If you want your Azure AD Joined Windows 365 Cloud PCs to be 100% Cloud Only then select the built-in Microsoft-hosted network. If you select that choice then you can skip the next three optional steps. If however you want to control the region where your network is located (in relation to your users) and which DNS settings your Cloud PC's will use plus many other additional network settings, then you should configure the next three steps.

Create or reuse a Resource Group (optional)
Windows 365 uses Resource Groups in Azure to store certain resources, such as Virtual networking. When creating a provisioning policy for a Cloud PC you can select to use the Microsoft hosted network (cloud only) or use a previously created Azure network connection (ANC). If you choose the option to use your own network via an Azure network connection, that ANC needs to be in a Resource Group. To prepare for that, we'll create a new Resource Group in Azure.

We need to run the following code

#4 Create or reuse a Resource Group (optional)
# Install Az.Resources and connect to Azure account

Install-Module Az.Resources -Force -AllowClobber
Connect-AzAccount
# Define resource group name and location
$resourceGroupName = "W365ResourceGroupviaGraph"
$location = "NorthEurope"

# Create the resource group
New-AzResourceGroup -Name $resourceGroupName -Location $location
Write-Host "Resource group '$resourceGroupName' created in location '$location'."

After running, it should prompt you for an account to use

which account to use.png

 

and next it'll prompt you for which tenant and subscription to use:

select a tenant and subscription.png

and after that, it should create the resource group.

succeeded to create resource group.png

which can be confirmed in Azure, Resource Groups.

resourcegroup listed in azure.png

 

Create or reuse a Virtual Network (optional)
Windows 365 in an Azure AD Join scenario can use a Microsoft Hosted Network to be completely cloud only, or can use Virtual Networks to allow your Cloud PC's to use specific network settings that you define.

Note: If you want your Azure AD Join based Windows 365 Cloud PC's to be cloud only you can skip this step.

To use your own network and provision Azure AD joined Cloud PCs, you must meet the following requirements:
    • Azure virtual network: You must have a virtual network (vNET) in your Azure subscription in the same region as where the Windows 365 desktops are created.
    • Network bandwidth: See Azure’s Network guidelines.
    • A subnet within the vNet and available IP address space.
In your newly created Resource Group, click on Create and select Virtual Network. Here you can define the ip addresses to use if that's your preference.

# 5. Create or reuse a Virtual Network (optional)
# Install Az.Network and connect to Azure account
Install-Module Az.Network -Force -AllowClobber
Connect-AzAccount
# Define hardcoded values for resource group, location, virtual network, and subnet
$resourceGroupName = "W365ResourceGroupviaGraph"
$location = "NorthEurope"
$vnetName = "W365VirtualNetworkviaGraph"
$addressPrefix = "10.0.0.0/16"
$subnetName = "W365SubnetviaGraph"
$subnetPrefix = "10.0.1.0/24"
# Create the virtual network and subnet
$subnetConfig = New-AzVirtualNetworkSubnetConfig -Name $subnetName -AddressPrefix $subnetPrefix
New-AzVirtualNetwork -ResourceGroupName $resourceGroupName -Location $location -Name $vnetName -AddressPrefix $addressPrefix -Subnet $subnetConfig
Write-Host "Virtual network '$vnetName' and subnet '$subnetName' created successfully."

After running, it will prompt you which account to use and once again prompt you which tenant and subscription to use, pressing enter will keep your previous choice. If everything went ok, your virtual network and subnet will be created successfully.

virtual network and subnet created successfully.png

And that virtual network will be in your chosen resource group in Azure.

virtual network in our resource group.png

 

Create Azure network connection (optional)
Windows 365 in an Azure AD Join scenario can use a Microsoft Hosted Network to be completely cloud only, or can use an Azure network connection to allow your Cloud PC's to access your on-premises network resources.
Note: If you want your Azure AD Join based Windows 365 Cloud PC's to be cloud only you can skip this step.


The following code will create the ANC for you, using the details of the Resource Group, virtual network and subnets which were created in the previous code. Keep in mind that each tenant has a limit of 10 Azure network connections, if you need more than that you must contact Microsoft support.

# 6. Create an ANC
# Install required modules with -Force and -AllowClobber
Install-Module -Name Az.Accounts -Force -AllowClobber
Install-Module -Name Az.Resources -Force -AllowClobber
Install-Module -Name Az.Network -Force -AllowClobber
Install-Module -Name Microsoft.Graph.Beta.DeviceManagement.Administration -Force -AllowClobber

# Connect to Azure and Microsoft Graph accounts
# For permissions required see - https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.beta.devicemanagement.administration/new-mgbetadevicemanagementvirtualendpointonpremiseconnection?view=graph-powershell-beta
Connect-AzAccount
#Capture the subscription ID entered by user
$subscription = (Get-AzContext).Subscription.Id
Write-Host "Using Subscription ID: $subscription"
Connect-MgGraph -NoWelcome -Scopes "CloudPC.ReadWrite.All"
Import-Module Microsoft.Graph.Beta.DeviceManagement.Administration
# Static variables (using values from previous examples)
$resourceGroupName = "W365ResourceGroupViaGraph"
$location = "NorthEurope"
$vnetName = "W365VirtualNetworkviaGraph"
$subnetName = "W365SubnetviaGraph"
$connectionName = "W365 North Europe EntraID via Graph"  # Azure Network Connection name
# Get the Virtual Network and Subnet IDs
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroupName
$subnet = $vnet.Subnets | Where-Object { $_.Name -eq $subnetName }
# Define parameters for ANC creation
$params = @{
    DisplayName      = $connectionName
    Type             = "azureADJoin"
    SubscriptionId   = $subscription
    ResourceGroupId  = "/subscriptions/$subscription/resourceGroups/$resourceGroupName"
    VirtualNetworkId = "/subscriptions/$subscription/resourceGroups/$resourceGroupName/providers/Microsoft.Network/virtualNetworks/$vnetName"
    SubnetId         = "/subscriptions/$subscription/resourceGroups/$resourceGroupName/providers/Microsoft.Network/virtualNetworks/$vnetName/subnets/$subnetName"
    scopeIds = @("0")
}
# Create Azure Network Connection
$ancProfile = New-MgBetaDeviceManagementVirtualEndpointOnPremiseConnection -BodyParameter $params

# Monitor the creation process
do {
    Write-Output "Azure Network Connection is being created... Running Checks, please wait."
    Start-Sleep -Seconds 60
    $policyState = Get-MgBetaDeviceManagementVirtualEndpointOnPremiseConnection -CloudPcOnPremisesConnectionId $ancProfile.Id
} while ($policyState.HealthCheckStatus -eq "running")
# Check the health status of the ANC
switch ($policyState.HealthCheckStatus) {
    "passed" {
        Write-Output "The Azure Network Connection created successfully."
    }
    default {
        throw "ANC creation failed. Review errors at: https://endpoint.microsoft.com/#view/Microsoft_Azure_CloudPC/EditAzureConnectionWizardBlade/connectionId/$($policyState.id)/tabIndexToActive~/0"
    }
}
Write-Host "Azure Network Connection '$connectionName' created successfully." -ForegroundColor Green

When executed, it'll once again prompt for username before asking you to confirm the subscription, next it'll probably launch a web browser asking you to login, and then once logged in, you'll have to confirm the scoped permissions. This permission request (shown below) will only happen as needed.

permissions requested via scoping.png

After it's got the permissions needed, It will check the health status every 60 seconds to see if it has completed.

Side note, while you are waiting for your ANC to be built, keep in mind that all of the Powershell examples we are using here are coming via help from Graph X-Ray, here's an example of the Graph X-Ray snippet used for this section.

graph xray snippet.png

And after some time, the ANC should be created successfully as we can see here in Visual Studio Code.

ANC created successfully.png

And it should appear in the Intune portal.

our ANC in Intune portal.png

 

Create provisioning policy

Next you need to create a provisioning policy. To create the Provisioning Policy using Graph we'll use the following script. This code will create the Provisioning Policy with the optional Azure Network Connection details added. So if you have used all the optional steps when executing the previous code then we recommend using this code to create a provisioning policy with an ANC.

# 7. Create a Cloud PC Provisioning Policy
# Install required modules with -Force and -AllowClobber
Install-Module -Name Microsoft.Graph.Beta.DeviceManagement.Administration -Force -AllowClobber
Install-Module -Name Microsoft.Graph.Groups -Force -AllowClobber
Install-Module Microsoft.Graph.Beta.DeviceManagement.Actions -Force -AllowClobber
# Connect to Microsoft Graph
Connect-MgGraph -NoWelcome -Scopes "CloudPC.ReadWrite.All","Group.ReadWrite.All"
# Static values for provisioning policy
$policyName = "W365 North Europe Entra ID W11 Via Graph"
$description = "Provisioning policy for W365 North Europe Entra ID W11 Via Graph"
$provisioningType = "dedicated"  # Enterprise license (dedicated provisioning)
$ancName = "W365 North Europe EntraID via Graph"  # ANC name
$imageName = "Windows 11 Enterprise + Microsoft 365 Apps 24H2"  # Gallery image name
$groupName = "W365 North Europe AAD W11 users via Graph"  # Group name
$selectedLanguageCode = "en-US"  # Language set to en-US
# Query for the ANC ID based on the ANC name
$ancId = (Get-MgBetaDeviceManagementVirtualEndpointOnPremiseConnection -Filter "displayName eq '$ancName'").Id
# Query for the gallery image ID based on the image name
$imageId = (Get-MgBetaDeviceManagementVirtualEndpointGalleryImage -Filter "displayName eq '$imageName'").Id
# Query for the group ID based on the group name
$groupId = (Get-MgGroup -Filter "displayName eq '$groupName'").Id
# Define the body for the provisioning policy creation following the correct formatting
$params = @{
    "@odata.type" = "#microsoft.graph.cloudPcProvisioningPolicy"
    description = $description
    displayName = $policyName
    domainJoinConfigurations = @(
        @{
            onPremisesConnectionId = $ancId
            type = "azureADJoin"
        }
    )
    enableSingleSignOn = $false  # Single Sign-On is disabled
    imageDisplayName = $imageName
    imageId = $imageId
    imageType = "gallery"
    windowsSettings = @{
        language = $selectedLanguageCode
    }
    provisioningType = $provisioningType
}
# Create the provisioning policy
$provisioningPolicy = New-MgBetaDeviceManagementVirtualEndpointProvisioningPolicy -BodyParameter $params
# Assign the provisioning policy to the group "W365 North Europe AAD W11 users via Graph"
$assignmentParams = @{
    assignments = @(
        @{
            target = @{
                groupId = $groupId  # Group ID of "W365 North Europe AAD W11 users via Graph"
            }
        }
    )
}

# Assign the policy to the group

try {
Set-MgBetaDeviceManagementVirtualEndpointProvisioningPolicy -CloudPcProvisioningPolicyId $provisioningPolicy.Id -BodyParameter $assignmentParams
Write-Host "Provisioning Policy '$policyName' created and assigned to group '$groupName' successfully." -ForegroundColor Green}
catch
{Write-Host "Provisioning Policy '$policyName' failed to assign to group '$groupName'." -ForegroundColor Red}

Here you can see the successful creation of the policy in Intune with the Azure network connection that we specified.

image.png

If you are not doing the optional steps above and are going to use the Microsoft Hosted Network, then use the following code, don't forget to adjust the variables as appropriate for your environment, eg: $regionGroup

# 7. Create a Cloud PC Provisioning Policy
# Install required modules with -Force and -AllowClobber
Install-Module -Name Microsoft.Graph.Beta.DeviceManagement.Administration -Force -AllowClobber
Install-Module -Name Microsoft.Graph.Groups -Force -AllowClobber
Install-Module -Name Microsoft.Graph.Beta.DeviceManagement.Actions -Force -AllowClobber

# Connect to Microsoft Graph
Connect-MgGraph -NoWelcome -Scopes "CloudPC.ReadWrite.All","Group.ReadWrite.All"
# Static values for provisioning policy
$policyName = "W365 North Europe Entra ID W11 Via Graph"
$description = "Provisioning policy for W365 North Europe Entra ID W11 Via Graph"
$provisioningType = "dedicated"  # Enterprise license (dedicated provisioning)
$regionGroup = "europeUnion" 
$imageName = "Windows 11 Enterprise + Microsoft 365 Apps 24H2"  # Gallery image name
$groupName = "W365 North Europe AAD W11 users via Graph"  # Group name
$selectedLanguageCode = "en-US"  # Language set to en-US
# Query for the gallery image ID based on the image name
$imageId = (Get-MgBetaDeviceManagementVirtualEndpointGalleryImage -Filter "displayName eq '$imageName'").Id
# Query for the group ID based on the group name
$groupId = (Get-MgGroup -Filter "displayName eq '$groupName'").Id
# Define the body for the provisioning policy creation following the correct formatting
$params = @{
    "@odata.type" = "#microsoft.graph.cloudPcProvisioningPolicy"
    description = $description
    displayName = $policyName
    domainJoinConfigurations = @(		
        @{			
            type = "azureADJoin"			
            regionGroup = "$regiongroup"
            regionName = "automatic"
        }
        )
    enableSingleSignOn = $false  # Single Sign-On is disabled
    imageDisplayName = $imageName
    imageId = $imageId
    imageType = "gallery"
    windowsSettings = @{
        language = $selectedLanguageCode
    }
    provisioningType = $provisioningType
}
# Create the provisioning policy
$provisioningPolicy = New-MgBetaDeviceManagementVirtualEndpointProvisioningPolicy -BodyParameter $params
# Assign the provisioning policy to the group "W365 North Europe AAD W11 users via Graph"
$assignmentParams = @{
    assignments = @(
        @{
            target = @{
                groupId = $groupId  # Group ID of "W365 North Europe AAD W11 users via Graph"
            }
        }
    )
}

# Assign the policy to the group
try {Set-MgBetaDeviceManagementVirtualEndpointProvisioningPolicy -CloudPcProvisioningPolicyId $provisioningPolicy.Id -BodyParameter $assignmentParams

Write-Host "Provisioning Policy '$policyName' created and assigned to group '$groupName' successfully." -ForegroundColor Green}
    catch
    {Write-Host "Provisioning Policy '$policyName' failed to assign to group '$groupName'." -ForegroundColor Red}
    

here's the output

provisioning policy created successfully.png

 

Job done! Your newly provisioned Cloud PC is now available to the targeted user(s).

image.png

Summary

Automating tasks for your Cloud PC users is doable  with PowerShell and Microsoft Graph. Using the Graph X-Ray web browser extension and Copilot to assist with your scripts makes your job a whole lot easier as now you can automate repetitive tasks instead of manually doing them in the various portals.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...


×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.