The idea

Since a while I’m testing/playing around with Windows 365 Cloud PC’s. For performance purposes I would like to reboot and/or reprovision Windows 365 Cloud PC’s on a recurring schedule. Somehow, there’s no built-in option available to reboot Cloud PC’s overnight. Probably there’s no need to but it sounded like a great idea for a new blog! Curious how it’s done? Continue reading!


There are really, many, many prerequisites needed for this to work. Therefor I wrote some code which completely deploys the infrastructure parts required. The code is shared in the “Deploy Azure Automation” part of this blog.

  • Azure tenant (with credits)
  • Github repository (with Personal Access Token)
  • Azure Automation Account (with Runbook)
  • PowerShell modules
  • App Registration(s) in Azure AD
  • (Self-Signed) Certificate
  • Managed Identity in Azure AD

Note: Not all steps are being automated. There are some steps which have to be done manually. I talk you trough this in the blog below.

Github Repo (Source Control)

Because I was looking for an automated deployment I had to make sure I could store my code somewhere. Azure Automation Runbooks can import code from a local (device) path and can make use of Source Control. With Source Control you can store data elsewhere and sync the data (scripts/code) into your Azure Automation Runbook’s.

Source Control can import (sync) data from the following sources:

  • GitHub
  • Azure DevOps (Git)
  • Azure DevUps (TFVC)

I picked GitHub. Therefor I had to create a Repository and a Personal Access Token.

  1. Create a GitHub Repository: Create a repo – GitHub Docs
  2. Create a Personal Access Token: Creating a personal access token – GitHub Enterprise Server 3.4 Docs

When using GitHub, the following PAT permissions are required:

Note: Write-down the Personal Access Token! We need this later!

Deploy Azure Automation

The complete Azure Infrastructure required, can be deployed via command line. I figured out all the steps required and put them together in a script. The complete script can be found here in my GitHub. All steps/commands are described below.

Note: Some parts should be modified for yourself. All the steps (including what should be modified is shown below in steps.

Install required modules and Connect to AzAccount and AzureAD.

#Connect to your Tenant
Install-Module Az.Accounts -Force
Install-Module AzureAD -Force

Configure the required parameters. It’s fine to keep these parameters except for the $SubscriptionID and $SCRepo. The $SCRepo parameter should be the URL from the repository created in the step before.

$RGName = "rg-w365-automation" #ResourceGroup Name
$Region = "West Europe" #Region for Azure Resources
$MiName = "mi-w365-automation" #Managed Identity name
$AutomationAccount = "aa-w365-automation" #Automation Account name
$subscriptionID = "33eh30a9-8a73-****-*****-********" #Subscription ID
$SCRepo = "" #Github Repo used for Source Control (Code to run)
$ScheduleTimeZone = "Europe/Amsterdam" #Timezone used for scheduled activity
$RebootTime = "23:59:00" #Time used for scheduled activity

Create a new Resource Group based on the parameters shown above.

#Create ResourceGroup
New-AzResourceGroup -Name $RGName -Location $Region

Create a new Automation Account based on the parameters shown above.

#Create AutomationAccount
New-AzAutomationAccount -Name $AutomationAccount -Location $Region -ResourceGroupName $RGName

Install the PowerShell modules into the Azure Automation account. These are required modules. There’s also a loop configured which waits for the Microsoft.Graph.Authentication module to be installed successfully. This is done because the other modules are depending on this one. The Automation Account and Resource Group are picked from the parameters shown above.

#Import Required PowerShell Modules.
$moduleName = "Microsoft.Graph.Authentication"
$moduleVersion = "1.17.0"
New-AzAutomationModule -AutomationAccountName $AutomationAccount -ResourceGroupName $RGName -Name $moduleName -ContentLinkUri "$moduleName/$moduleVersion"

#Query Microsoft.Graph.Authentication state. Continue if PrivisioningState is "Succeeded"
While ((Get-AzAutomationModule -AutomationAccountName $AutomationAccount -ResourceGroupName $RGName -Name $moduleName).ProvisioningState -ne "Succeeded")
    write-host (get-date) "Required module is installing.. Waiting untill completed."
    Start-Sleep -s 30

Start-Sleep -s 5

if ($ModuleState.ProvisioningState -eq "Succeeded") 
    Write-Host "Required module is installed. Continue."
exit 99

$moduleName2 = "Microsoft.Graph.DeviceManagement"
$moduleVersion2 = "1.17.0"
New-AzAutomationModule -AutomationAccountName $AutomationAccount -ResourceGroupName $RGName -Name $moduleName2 -ContentLinkUri "$moduleName2/$moduleVersion2"

$moduleName3 = "Microsoft.Graph.DeviceManagement.Actions"
$moduleVersion3 = "1.17.0"
New-AzAutomationModule -AutomationAccountName $AutomationAccount -ResourceGroupName $RGName -Name $moduleName3 -ContentLinkUri "$moduleName3/$moduleVersion3"

$moduleName4 = "Microsoft.Graph.DeviceManagement.Administration"
$moduleVersion4 = "1.17.0"
New-AzAutomationModule -AutomationAccountName $AutomationAccount -ResourceGroupName $RGName -Name $moduleName4 -ContentLinkUri "$moduleName4/$moduleVersion4"

Create a User Assigned Managed Identity based on the parameters shown above.

#Create User Assigned Managed Identity
New-AzUserAssignedIdentity -ResourceGroupName $RGName -Name $MiName

Add the Managed Identity to the Resource Group based on the parameters shown above.

#Add Managed Identity to Resource
Set-AzAutomationAccount -ResourceGroupName $RGName -Name $automationAccount -AssignUserIdentity "/subscriptions/$subscriptionID/resourcegroups/$RGName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/$MiName" -AssignSystemIdentity

Create a required variable into the Automation Account. This is required when using a User Assigned Managed Identity. If you don’t configure this variable, it will pick a System Assigned Managed Identity. Therefor it is important that you DON’T modify the $VariableName in the parameters.

#Create Managed Identity ClientID variable
$VariableContent = Get-AzUserAssignedIdentity -Name $MiName -ResourceGroupName $RGName
New-AzAutomationVariable -AutomationAccountName $AutomationAccount -Name $VariableName -Value $VariableContent.ClientID -ResourceGroupName $RGName -encrypted $false

Assign Contributor permissions to the Managed Identity. I gave the permissions on the Resource Group level but it would be more secure to set these permissions on the Automation Account itself.

#Assign contributor permissions to the Managed Identity
$RoleAssignment = Get-AzADServicePrincipal -DisplayName $MiName
New-AzRoleAssignment -ObjectId $ -RoleDefinitionName "Contributor" -ResourceGroupName $RGName -WarningAction:SilentlyContinue

Configure Source Control in the Azure Automation Account. Here you have to modify some settings.

  1. $SecString should be modified to the Personal Access Token which you created before. This is the access token used to access the GitHub Repository.
  2. The -FolderPath parameter must probably be changed. This is the path where your actual code is stored within the GitHub Repository. If you create a folder named “Runbook” there’s no need to change anything.
  3. The -Branch parameter is main by default. If you are using a different branch in your GitHub Repository this should be changed.

We make use of the -EnableAutoSync parameter. When using this parameter all modifications within the Source (GitHub) are automatically synced into the Azure Automation Runbook. If you don’t like the auto sync feature, just remove the parameter from the script. In this case you need to sync your GitHub modifications manually.

Configure SourceControl (Github Repo)
#Note: Manual Source Control Sync is required from the Azure Portal or modify code in the source (Github). This initiates a sync also.
$SecString = ConvertTo-SecureString "*********************" -AsPlainText -Force
New-AzAutomationSourceControl -Name SCGitHub -RepoUrl $SCRepo -SourceType GitHub -FolderPath "/Runbook" -Branch main -ResourceGroupName $RGName -AutomationAccountName $AutomationAccount -AccessToken $SecString -EnableAutoSync

Configure the required reboot schedule. This code configures a schedule for the next 5 years which will run daily within at a specific time in a specific time-zone.

  • $StartTime = Current date with the configured $RebootTime parameter
  • $EndTime = 5 years from current date
  • $TimeZone = Time zone where $StartTime will execute.
  • DayInterval = Days running the schedule/script. 1 is every day
#Import Recurring Schedule
$StartTime = Get-Date "$RebootTime"
$EndTime = $StartTime.AddYears(5)
New-AzAutomationSchedule -AutomationAccountName "$AutomationAccount" -Name "DailyReboot" -StartTime $StartTime -ExpiryTime $EndTime -DayInterval 1 -ResourceGroupName "$RGName" -TimeZone "$ScheduleTimeZone"

Link the schedule to the runbook. We first lookup the available runbook in the Automation Account and then use the output to link the schedule. The Runbook name is copied from the GitHub Repository and therefor can be different in your scenario.

#Link Runbook to Recurring Schedule
$GetRunbook = Get-AzAutomationRunbook -AutomationAccountName "$AutomationAccount" -ResourceGroupName "$RGName"
Register-AzAutomationScheduledRunbook -AutomationAccountName "$AutomationAccount" -RunbookName $GetRunbook.Name -ScheduleName "DailyReboot" -ResourceGroupName "$RGName"

Last but not least. Create a App Registration for Microsoft Graph PowerShell. This is the only command which uses the “AzureAD” (connect-azuread) PowerShell Module.

#Add "Graph PowerShell" Enterprise APP.
New-AzureADApplication -DisplayName "Microsoft Graph PowerShell"

Create (self-signed) certificate

Connecting to MgGraph can be done via several authentication methods. The method we require is a certificate authentication with a Private Key. Therefor we have to create a (self-signed) certificate. Official Microsoft documentation can be found here.

Note: Make sure to modify the {myPassword} field and $ExportPath parameter

#Create Certificate and Export Private Key
$ExportPath = "C:\"
$certname = "W365-Automation"
$cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath "$ExportPath\W365-Automation.cer"
$mypwd = ConvertTo-SecureString -String "{myPassword}" -Force -AsPlainText  ## Replace {myPassword}
Export-PfxCertificate -Cert $cert -FilePath "$ExportPath\W365-Automation.pfx" -Password $mypwd   ## Specify your preferred location

Store the certificate and private key in a secure place. We need this in the next steps.

App Registration

Because Azure Automation is a automated process, we can’t logon with username/passwords. We need to connect using a App Registration in combination with a certificate. Official Microsoft Docs can be found here.

1. Create the App Registration. Give it a common name like W365-Automation. Pick the settings as shown below.

2. Write down the Application ID and Directory ID. We need these later!

3. Configure API Permissions as shown below. The official documentation for these permissions can be found here. Do not forget to Grant admin consent for… after configuring the required permissions.

4. Upload the (self-signed) certificate which we created in the the App Registration. Write down the Thumbprint shown after importing. We need this later!

5. Within the Azure Automation Account, we need to upload the private key (PFX) which we exported before. This can be found under Azure Automation -> Automation Account -> Certificates.

Code to initiate the Reboot (with Azure Automation)

Now we have deployed the Azure Automation Framework, we need some code which will be executed by Azure Automation. Because we are using the Source Control feature (GitHub) that is the place where we have to store the code. Make sure that you store the code in the exact path as configured in the Source Control command. This MUST match the -FolderPath parameter. Modify the following fields in the script:

ClientID: This is the Client ID as shown in the App Registration we created before.

TenantID: This is the Tenant ID we are running this automation and App Registration in.

CertificateThumbprint: This is the thumbprint of the (self-signed) certificate we created before.

The code can be found in my GitHub Repository, here.

# This script executes the Windows 365 Cloud PC Reboot process.
# This script is part of a Azure Automation runbook with a recurring schedule.

#Connect MgGraph
Connect-MgGraph `
    -ClientId "940063b6-80ab-****-****-************" `
    -TenantId "e077bfdf-65f9-****-****-*************" `
    -CertificateThumbprint "2a64bf*************************"

#Import required modules
Import-Module Microsoft.Graph.DeviceManagement.Actions
Import-Module Microsoft.Graph.DeviceManagement.Administration

#Switch to Beta Profile. Cloud PC API is only here available
Select-MgProfile -Name "Beta"

#Get all Cloud PC's
$AllCloudPC = Get-MgDeviceManagementVirtualEndpointCloudPC | Where-Object {($_.DisplayName -Like '*')}

#Restart all Cloud PC's
foreach ($CloudPC in $AllCloudPC.Id)
    Restart-MgDeviceManagementVirtualEndpointCloudPc -CloudPcId $CloudPC
    Write-Host "Cloud PC Restarted"

Note: This is not the greatest idea to be honest but, you can also reprovision the Windows 365 Cloud PC’s every day or every week or so. Therefor you can pick this code instead of the above. Don’t forget to modify the schedule otherwise it will reprovision every day! This could be done maybe once a week or once a month to keep your Windows 365 Cloud PC’s clean now and then.

Admin Experience

After completing all the steps there’s nothing left then monitoring for execution. This can be done manually and also automated via the schedule we have configured. If you would like to execute the script manually follow the next steps.

Note: Keep in mind that running this script will reboot all Windows 365 Cloud PC’s immediately! In this case you can better modify the reboot script for testing purposes!


$AllCloudPC = Get-MgDeviceManagementVirtualEndpointCloudPC | Where-Object {($_.DisplayName -Like '*')}

With: Change *CPC-MYTESTPC* to your testing device.

$AllCloudPC = Get-MgDeviceManagementVirtualEndpointCloudPC | Where-Object {($_.DisplayName -Like '*CPC-MYTESTPC*')}

In the Azure Portal:

Open your Azure Automation Account -> Runbooks -> W365-Reboot -> Start

In the Intune Portal:

Devices -> All Devices -> Select Device -> Device actions status

There you go! Your Windows 365 Cloud PC is rebooted via Azure Automation. Keep in mind that this was a manual action. The schedule which is implemented will continue to operate and reboot ALL Windows 365 Cloud PC’s at the configured schedule time.


Related Posts

2 thoughts on “Schedule Windows 365 Cloud PC Reboot’s with Azure Automation

  1. You could also use a managed identity in the automation account instead of a separate service principal to simply things, and make it more secure 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

15 − 14 =