The following post is all about a complete process automation of gathering and upload of a device Autopilot information to the Windows Autopilot service with an Azure Automation Runbook.
Table of content for easy navigation
- The Problem
- Autopilot Graph API
- How to do unattended authentication with the Intune API?
- Setup the Azure Automation environment
- Sample output of the Runbook
- Important observations during testing
- Further information
Many organizations are still using Windows 7 and are on it’s way to Windows 10. Windows 10 is the new “baseline” in the Modern Management story. It provides the capability to join Azure AD and the usage of a Windows as a Service model.
How do we get to the new baseline?
If we purchase a new device, the OEM vendor takes care of installing Windows 10 with a signature edition or provisioning ready installation including all necessary drivers. If we buy new hardware the information for Autopilot can be synced into our tenant from the OEM vendor (Lenovo is already capable of doing that and others will follow). We will get the device information in Intune and we can start to assign an Autopilot Deployment Profile and start to enroll the devices.
What if we have a bunch of Windows 7 devices in the environment?
A way to handle this is that we are playing the role of the OEM vendor and do the install of a Windows 10 signature edition on the existing Windows 7 devices, gathering Autopilot information, and let Windows 10 start in the Out of Box Experience (OOBE) again for user enrollment. Depending what is available we can use ConfigMgr or MDT for this. My following example uses MDT to demonstrate this.
Now imagine a situation where a rollout team is preparing a lot of machines. We would end up in a lot of .csv files on different devices. To make this a little easier for IT to import the hardware information of new devices into the Autopilot service, we build up the following logic to automate the whole process.
First, I will explain the architecture and how it works and then I’m going to describe the way to implement it (this is a fairly long post with many steps to follow). The overall architecture is based on an Azure Automation Runbook and looks like this:
The procedure is as follows:
- Gather hardware information via PowerShell Script Get-WindowsAutoPilotInfo during wipe and reload scenario
- Upload .csv file via AzCopy and SAS signature to an Azure Blob Storage
- Gather .csv files from Azure Blob Storage and combine them into a single .csv file with the help of a scheduled Azure Runbook
- Upload combined .csv file information to Windows Autopilot service via PowerShell Script WindowsAutoPilotIntune running in an Azure Automation Runbook
- Cleanup Azure Blob Storage (delete all .csv files of successfully imported devices and delete all .csv files of already imported devices)
- Generate import notification and summary and post it to a Microsoft Teams channel
- Autopilot information is available for the OOBE user enrollment scenario with Autopilot. The Autopilot profile gets automatically assigned by a dynamic AzureAD device group membership.
- Device can be delivered to the end user like it where shipped by the OEM vendor
I’ve chosen the copy via AzCopy of individual .csv files to the Azure Blob Storage approach as we can then limit the access quite well via shared access signature and we can easily limit permission to write blob objects only. No need to provide credentials or Blob Storage keys on the client side. Sure, we could build up a more advanced HTTP endpoint to gather device information, but this approach is quick and simple. I’m pretty sure that the complete solution of this automation task is something which we do not need in future when devices are Windows 10 migrated and we then buy Autopilot ready hardware only.
Autopilot Graph API
The Autopilot Graph API is an API with focus on batch processing. This means we import new device information into a kind of staging area and the Windows Autopilot service will pick up the new device information and starts importing it. This process varies in the amount of time it takes and we have to check the status of all devices to get the import result. As soon as the devices are processed we can clean up the staging area and the import is done. Normally we would do this by wrapping the Graph API calls (REST) into some PowerShell functions and build the logic for the described process. Luckily Microsoft released a new PowerShell Module WindowsAutoPilotIntune (thx to @mniehaus) based on the Graph API to import new AutoPilot information into Intune.
I use an unattended authentication technique to utilize the PowerShell Module WindowsAutoPilotIntune to import the device information into Autopilot service in the following Runbook. Additionally, the Runbook is built to protect concurrent execution (thx to Tao Yang, I used his implementation for it) to ensure a sequential processing and to keep track of current running imports. If we would design this as a concurrent solution it would get much harder in terms of monitoring and reporting in the end. In addition, there is a max import of 175 devices into the staging area of the API which we are taking care of by limiting the Runbook import to 175 devices during one run.
How to do unattended authentication with the Intune API?
The problem with the Intune API and Microsoft Graph is, that we can’t authenticate as an application as this is not supported at the time of writing. See section here Intune Device Management permissions > Application permissions: None.
We need to authenticate as an user (service account). This requires additional credentials and a secure storage of them to automate. Microsoft has a good guide how to set up an Azure application to support this scenario: How to use Azure AD to access the Intune APIs in Microsoft Graph. One aspect is that the Microsoft How-To guide will end up in a scenario which still prompts for credentials with a input form. This is because of the usage of:
For Azure Automation we need to change this behavior a bit to support credentials within our code:
We can use the How-To guide or the official GitHub Intune sample scripts which have the following lines of code:
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters, $userId).Result
The lines of code need to be changed to support our new AcquireTokenAsync call to specify UserPasswordCredentials as additional parameter:
$intuneAutomationCredential = Get-AutomationPSCredential -Name automation $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($intuneAutomationCredential.Username, "OptionalDisplayableId") $userCredentials = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential -ArgumentList $intuneAutomationCredential.Username, $intuneAutomationCredential.Password $authResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceAppIdURI, $intuneAutomationAppId, $userCredentials);
The credentials will be received from the Azure Automation account in PowerShell via Get-AutomationPSCredential. We will provision the service account credentials securely for the Azure Automation account via Credential assets.
Setup the Azure Automation environment
The following steps must be followed to setup the Azure Automation environment:
- Creation of a native application in Azure AD
- Assigning permissions to the registered application
- Grant permissions (administrator consent)
- Create Azure Automation Account
- Add credentials to the Azure Automation account
- Add variables to the Azure Automation account
- Add Runbook to the Azure Automation account
- Edit Runbook
- Create Azure Blob Storage
- Client-side gather script
1. Creation of a native application in Azure AD
The best description for a native application is found in the Intune documentation for the Intune API here: How to use Azure AD to access the Intune APIs in Microsoft Graph. I will outline the necessary steps to setup the environment.
New application registration
fill out the details and give it a name, create a native application with redirect URI: urn:ietf:wg:oauth:2.0:oob
in the end a new registered application is available. Important is to copy the application id as we need it in our PowerShell script Runbook later.
2. Assigning permissions to the registered application
The registered application must have Intune Configuration Read and Write permissions.
3. Grant permissions (administrator consent)
Finally we grant the selected permissions to the newly registered application.
4. Create Azure Automation Account
Creation of the Azure Automation Account in a existing or new resource group.
5. Add credentials to the Azure Automation account
Go to Azure AD and create a new user, in my case user automation with Display Name Intune Automation and use a complex password for it.
The service account credentials can even be limited to the necessary permission needed to write the Autopilot device information. I’ve not done this as the Global Administrator account credentials are very well protected, but it’s certainly possible.
Note from Credential assets in Azure Automation:
Secure assets in Azure Automation include credentials, certificates, connections, and encrypted variables. These assets are encrypted and stored in Azure Automation using a unique key that is generated for each automation account. This key is stored in Key Vault. Before storing a secure asset, the key is loaded from Key Vault and then used to encrypt the asset.
For the concurrent execution protection of the Runbook, we need our automation credential to have Reader permission and for Blob Storage access we need Contributor permissions on the subscription. As the result we grant Contributor permission to the automation account:
After user creation we add the credential to the Azure Automation account.
6. Add variables to the Azure Automation account
The PowerShell Runbook needs the native registered application ID also called Client ID. Therefore we create an Azure Automation variable IntuneClientId and we need the tenant ID as a variable, we use Tenant as identifier:
Additional variables needed for Azure Blob Storage access:
|* as encrypted variable|
Additional variables needed for Microsoft Teams Channel notification:
|* as encrypted variable|
These are the added variables to the Automation Account, remember to add the StorageKey and TeamsWebhookUrl as encrypted variables for further protection:
The subscription URL can be found as shown below:
Please do not copy the /overview end of the URL. The URL should end with the subscription GUID only (like highlighted):
The Teams Webook URL can be found as shown below:
Open MS Teams and choose a Channel where the Autopilot notification from the Azure Runbook should be displayed. Click on the three dots and choose Connectors:
Look for Incoming Webhook and click Configure
Type in the details and upload a icon and click Create
Finally copy the Webhook URL:
Paste it into the Azure Automation variable TeamsWebHookUrl and set encrypted value to Yes
7. Add Runbook to the Azure Automation account
Adding a Runbook with the name Import-AutopilotInfo as type PowerShell
8. Edit Runbook
Finally, we can implement the complete Runbook which can be found on my GitHub account here:
The Runbook is written in PowerShell and follows the logic described in the beginning of this post – section architecture.
Edit the PowerShell Runbook and paste-in the code:
9. Create Azure Blob Storage
Login to Azure portal and click on “Storage accounts”
fill out name, Account kind: Blob storage
after creation you should see the storage account
create a container called hashes
create a shared access signature for Blob | Write | an extended expiry date/time | HTTPS only and create a SAS token. Shared Access Signature is used to limit the permission and the limit the period of time to access the account. See Delegating Access with a Shared Access Signature
Copy the SAS token as we need it in the gathering script.
10. Client-side gather script
The client-side script uses AzCopy.exe (incl. dependency files) and Get-WindowsAutoPilotInfo.ps1 to gather and upload the .csv file via SAS to the Blob Storage. Both dependencies can be in the same execution directory or if not available they are downloaded from an additional Blob Storage container named resources. The additional container resources must be created and the AzCopy.zip and Get-WindowsAutoPilotInfo.ps1 must be uploaded there to successfully run the script:
The client-side gather script can be found on my GitHub account here:
Replace ZZZZ with your Blob Storage account name and ZZZZ with your SAS signature in the script above. See here Delegating Access with a Shared Access Signature for more SAS signature details.
I assume the MDT share is built and a Standard Task Sequence for a vanilla Windows 10 installation is available. We should take care of the right drivers (have a look at our Modern Driver Management for Intune) and in the end we let the device start the OOBE again (sysprep.exe /oobe /reboot|shutdown). Then we add a task sequence step “Run PowerShell Script” to the folder “Custom Tasks“:
and configure the Get-WindowsAutoPilotInfoAndUpload.ps1 script:
Now you are ready to run a MDT deployment of a Windows 10 with an automatic upload of the hardware information to the Azure Blob Storage.
This script can also be used to be executed via Microsoft Intune Management Extension to run it on existing Windows 10 devices. It is possible to collect all targeted device information and the Runbook will import the device information. Already imported devices will be skipped. This way we can make sure every device is imported to the Autopilot service.
Sample output of the Runbook
If a device information <hostname>.csv is successfully imported the .csv files will be deleted from the Azure Blob Storage. In case of an error it will be left there untouched but reported via Runbook output and Teams notification. There is one case where the Runbook will delete the .csv file from the Azure Blob Storage also. This is if the Runbook detects an error 806 – ZtdDeviceAlreadyAssigned. In that case we can delete the .csv as it has no consequences. In every other error situation someone needs to resolve the error manually. The Teams notification is only generated if there is some device information in the Azure Blob Storage. The normal procedure would be if operations gets an import notification it should check and in case of errors they should be resolved manually.
Important observations during testing
The Autopilot import via Graph API takes some time and it may also timeout. So, it is not a typical request response REST API in this case. Remember all device information is staged and then the devices are monitored for their success or error state. This led to situations where I had to wait up to 1 hour to get a successful import, even when the UI in Intune tells us it takes up to 15 min. So be prepared that it might take longer or fails during the run. I chose this approach as it is the same procedure as the Azure Portal does the import. In fact, we really automated the import process in Intune but did not invent a complete different process which might cause different problems of device tracking or concurrency and so on. Depending on the use case you can run the Runbook on a recurring schedule. My normal use case for this scenario is to support people who are preparing older devices when they are getting reinstalled from Windows 7 to Windows 10. If someone expects immediate availability of Autopilot information after such a reinstall this might be problematic as the API is not designed for this. Another fact is that the Autopilot Deployment Profile assignment using Intune does take some time also at the moment. I observed that it took several hours sometimes. I suggest to re-arrange the operational processes and hand out reinstalled Windows 10 devices after some additional time and not directly after reinstalling, as this increases the possibility that the Autopilot information is not imported and profile assigned yet.
To run the Runbook on a recurring schedule just go to the Runbook and add a schedule. The max recurrence is limited to every hour.
Just define a schedule (max once per hour – this should be enough as the API and the AutoPilot Profile assignment are not that fast in the moment) and monitor the recent job overview if it works:
Should we see an improvement in processing and it may be needed to run the Runbook more often, the schedule can even be enhanced by using a simple Logic App and not using the Azure Automation Runbook schedule at all:
Please see Stefan Strangers post for detailed instructions how to implement a simple Logic App to trigger an Azure Automation Runbook:
In case something goes wrong I have created a second Runbook to clean up the staging area of the Autopilot Graph API. Get it from my GitHub account and run it in case of fatal errors where you might want to clean up the staging area:
Here is a sample output of the Cleanup Runbook:
Azure Logic Apps – Schedule your Runbooks more often than every hour
Preventing Azure Automation Concurrent Jobs In the Runbook
Post notifications to Microsoft Teams using PowerShell
importedWindowsAutopilotDeviceIdentity resource type
Autopilot profile assignment using Intune
Adaptive Cards Designer
Thanks for reading this fairly long post, I hope this can increase your throughput on the way to an Autopilot Windows 10 modern management environment.
When someone finds bugs or problems with the solution let me know and leave a comment. I will do my best to fix them as it should be a reliable part during preparing old devices with Windows 10.