Azure Automation - Device Cleanup v2

Almost a year ago, I shared how we can use Azure Automation to clean up devices in Azure AD. Unfortunately, Graph API only supported Disable as an option at the time (when using Application permissions), but apparently that changed some time around February 1!

Another thing I was unhappy about with my previous post was that I used Run As accounts which wasn't really the best option and is now deprecated. Over the next few days, I will make changes to the old article to highlight how we can migrate to this new method and point to this one. If interested, you can find that article here:

Azure Automation - Device Cleanup
I’m a huge fan of Azure Automation. If you’re an #AzureAD / #M365 Admin and haven’t used it before, then this thread is for you. You will need an Azure subscription, but the first 500 minutes/month are free! Here’s an example of how to automate Azure AD device cleanup :) First,

The final thing I'm excited about is this is the first time I'll be sharing how we can use Graph PowerShell V2 modules in Azure Automation, and using the PowerShell Gallery to do it makes this process significantly faster and easier! :)

Create the Automation Account

If you don't already have an existing Automation Account that would make sense to grant permissions to delete devices, let's create one for this purpose. Log into the Azure Portal, search for automation, and select Automation Accounts.

In the Automation Accounts, click Create.

On the Basics page, select the Subscription and Resource group where you want the Automation Account to be created, provide a name and region, then click Next.

To keep this short, we're going to click Review + Create since the defaults work for what we need, but if you want to change to Private Endpoints and add Tags, you can do that here (or after deployment).

Grant Device.ReadWrite.All Permissions

By default, Automation accounts enable a System Assigned Managed Identity, which is similar to a group managed service account from the days of Active Directory. These are great because we don't have to manage credentials, and with Graph PowerShell V2 modules, it really simplifies connecting to Graph.

In order to delete devices, we need to assign the appropriate permissions to the System Assigned Managed Identity associated with our Automation Account. To determine what permissions we need, we can look at the Delete device API docs.

Delete device - Microsoft Graph v1.0 | Microsoft Learn

Unfortunately, there is currently no way to assign these permissions using Azure AD, so we will need to do this through Graph PowerShell. We will need to obtain the Object ID for the System Assigned Managed Identity, so we can grant it permissions. To find this, go to Identity under Account Settings and copy the Object (principal) ID.

Use the following script to grant Device.ReadWrite.All to the Automation Account:

$SP_ID = 'Object (prinicpal) ID from above'
$AppId = (Get-MgServicePrincipal -ServicePrincipalId $SP_ID).AppId

# Connect with scopes requried to grant permissions
# May require admin consent for Graph PowerShell
Connect-MgGraph -Scopes appRoleAssignment.ReadWrite.All,Application.Read.All,Group.ReadWrite.All

# Grant Device.ReadWrite.All Graph Permissions
"Device.ReadWrite.All" | ForEach-Object {
   $PermissionName = $_
   $GraphSP = Get-MgServicePrincipal -Filter "startswith(DisplayName,'Microsoft Graph')" | Select-Object -first 1 #Graph App ID: 00000003-0000-0000-c000-000000000000
   $AppRole = $GraphSP.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"}
   New-MgServicePrincipalAppRoleAssignment -AppRoleId $AppRole.Id -ServicePrincipalId $SP_ID -ResourceId $GraphSP.Id -PrincipalId $SP_ID
}

If you would like to visually verify permissions, go to Enterprise Applications in Azure AD, search for the Object (principal) ID copied from above, and then select the application representing our Automation account.

Now go click Permissions under Security, and you should see the Microsoft Graph permission of Device.ReadWrite.All using Application permissions.

Add the Graph PowerShell V2 modules

Azure Automation does not include the Graph PowerShell modules by default, so we will need to add them. If we add the modules through the gallery within Azure Automation, we will be limited to the V1 modules. Instead, we are going to add the modules through the PowerShell Gallery, select the Azure Automation tab, then click Deploy to Azure Automation.

Microsoft.Graph.Identity.DirectoryManagement 2.0.0-preview8
Microsoft Graph PowerShell Cmdlets

This should redirect you to Azure, and it may ask you to choose which account you want to sign in with. Once signed in, select the Automation account we created, then click OK to import. Note: It is totally normal for the fields to look blank like the below screenshot - just click OK, and the modules will import.

One of the really cool things about deploying the modules this way is that it automatically handles dependencies for us and really saves a lot of issues with trying to repackage or change the extension and try to get it to manually import.

To verify that the modules imported successfully, go back to our Automation account, select Modules from under Shared Resources, then search for Microsoft. You should see Microsoft.Graph.Authentication and Microsoft.Graph.identity.DirectoryManagement listed. Unfortunately, we're stuck with Runtime version 5.1 when using this method, so I'll update this blog post once V2 modules are generally available so we can optimize this more.

Create the Runbook

Assuming you have no failures (if you do, delete them and try again), scroll up on the left side and select Runbooks under Process Automation, then click Create a runbook.

Provide a name, select PowerShell for the Runbook type, and most imporantly select 5.1 for the Runtime version as it has to match the version of the modules that we imported. Click Create when finished.

This should open us up into the editor, and we'll copy/paste the following code in.

# Connect to the Graph API as the automation account managed identity
Connect-MgGraph -Identity

# Get Zulu formatted time from 90 days ago for filter
$date = (Get-Date (Get-Date).AddDays(-90) -Format u).Replace(' ','T')

# Get devices that haven't signed in for more than 90 days
$devices = Get-MgDevice -All -Filter "ApproximateLastSignInDateTime le $date"

# If less than 20 devices, delete them (TY @tuna_gezer for threshhold idea!)
if ($devices.count -lt 20) {
   $devices | ForEach-Object { 
      Write-Output "Deleting $($_.DisplayName),$($_.DeviceId)"
      #Remove-MgDevice -DeviceId $_.Id
   }
} else { Write-Output "Delete threshold reached - $($devices.count) devices found" }
💡
Note that I have commented out the Remove-MgDevice command, and used 90 days of inactivity with a safety threshold of 20 devices. Change the number of days/devices to fit your needs, then run it as is to see the output, review, and then you can remove the # when comfortable.

You can test by clicking on the Test pane, then click Start. Assuming you have devices that haven't signed in for more days than your filter, you'll see a nice list.

Obviously my lab is too clean

You can close the testing pane by clicking the X in the top right, and when happy with things, click Publish and hit Yes.

Setting up schedules

Now all we have left is to configure our script to run as often as we want. To do this, go to Schedules under Resources, then click Add a schedule.

On the next screen, click Link a schedule to your runbook, then click OK.

Unless you already had an Automation account with schedules set up, you'll have to create a schedule. Click Add a schedule, then fill out the information for your new schedule and click Create.

This will automatically select the schedule you created, and to finish, click OK. You should end up with something like this:

You can add additional schedules if needed, but keep in mind that Azure Automation is billed by runtime, so we want to reduce that as much as possible.

The last part to know is that you can see all of the previous runs and their details by going to Jobs under the Resources section. Selecting a previous Job allows you to see the output of the script which can be really helpful for troubleshooting :)

I hope this helps others out there, and please reach out to me on Mastodon or Twitter if you have any questions!