Skip to content

Clean up resources

This guide provides instructions for cleaning up resources that may be left behind if a process in Schoolyear AVD fails.

Use the following script to clean up Entra ID devices created by Schoolyear AVD.

This script might take a long time to run (over 15 minutes), depending on the number of devices in your directory. To run this script, you need the Device.ReadWrite.All permission.

Clean up Schoolyear Entra ID devices older than 15 days
Connect-MgGraph -Scopes "Device.ReadWrite.All"
$avdPrefix = "syvm"
$staleThresholdInDays = 15
$staleDate = (Get-Date).AddDays(-$staleThresholdInDays)
Write-Output "Searching for stale devices with prefix '$avdPrefix' inactive since $staleDate..."
$devices = Get-MgDevice -Filter "startsWith(displayName, '$avdPrefix')" -Property "Id,DisplayName,ApproximateLastSignInDateTime" -All
if ($null -eq $devices) {
Write-Output "No devices found with the prefix '$avdPrefix'."
return
}
Write-Output "Found $($devices.Count) devices with the prefix. Checking for staleness..."
foreach ($device in $devices) {
# Check if the last sign-in timestamp is older than the stale date
if ($null -ne $device.ApproximateLastSignInDateTime -and $device.ApproximateLastSignInDateTime -lt $staleDate) {
Write-Output "DELETING stale device: $($device.DisplayName) (ID: $($device.Id)). Last sign-in: $($device.ApproximateLastSignInDateTime)"
# To run the script safely first, comment out the line below and run it to see what WOULD be deleted.
# Use the -WhatIf parameter for an extra layer of safety during testing.
Remove-MgDevice -DeviceId $device.Id
}
}
Write-Output "Script finished."

If a deletion job in Schoolyear AVD fails, it might leave an orphaned DNS record in your DNS zone. Use the following script to clean up these orphaned records.

Use the -WhatIf flag to perform a dry run and see which records would be deleted without actually deleting them.

Clean up orphan DNS records
<#
.SYNOPSIS
Cleans up orphaned 'A' records in an Azure DNS zone.
.DESCRIPTION
This script queries 'A' DNS records in a specified Azure DNS zone and filters for records named with a UUID.
It then compares these records against resource groups in the current subscription that match the pattern 'syexam-<uuid>'.
If a DNS record's UUID does not have a corresponding resource group, the script will delete the DNS record set.
.PARAMETER DnsZoneName
The name of the DNS zone to query. This parameter is mandatory.
.PARAMETER DnsZoneResourceGroupName
The name of the resource group that contains the DNS zone. This parameter is mandatory.
.PARAMETER SubscriptionId
The ID of the subscription where the resources are located. This parameter is mandatory.
.PARAMETER WhatIf
A switch parameter to show what would be deleted without actually performing the deletion. This is highly recommended for the first run.
.EXAMPLE
.\Clean-OrphanDnsRecords.ps1 -DnsZoneName "yourdomain.com" -DnsZoneResourceGroupName "MyDns-RG" -SubscriptionId "00000000-0000-0000-0000-000000000000" -WhatIf
This command runs the script in a simulation mode. It will connect to your Azure subscription, analyze the DNS records and resource groups, and output a list of DNS records that would be deleted, but it will not make any changes.
.EXAMPLE
.\Clean-OrphanDnsRecords.ps1 -DnsZoneName "yourdomain.com" -DnsZoneResourceGroupName "MyDns-RG" -SubscriptionId "00000000-0000-0000-0000-000000000000"
This command executes the cleanup operation. After identifying the orphaned records, it will prompt you for a final confirmation before deleting them permanently.
#>
param(
[Parameter(Mandatory=$true)]
[string]$DnsZoneName,
[Parameter(Mandatory=$true)]
[string]$DnsZoneResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$SubscriptionId,
[Switch]$WhatIf
)
try {
#region SETUP AND CONNECTION
Write-Host "Connecting to Azure..." -ForegroundColor Cyan
Connect-AzAccount -ErrorAction Stop
Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop
Write-Host "Successfully connected. Operating in subscription '$SubscriptionId'." -ForegroundColor Green
# Regex to validate a UUID string.
$uuidRegex = "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}"
#endregion
#region GATHER RESOURCE GROUP UUIDs
Write-Host "Fetching all resource groups to find existing UUIDs..." -ForegroundColor Cyan
$resourceGroups = Get-AzResourceGroup
# Use a HashSet for efficient 'Contains' lookups, ignoring case.
$rgUuids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($rg in $resourceGroups) {
# Check if the RG name matches the pattern "syexam-<uuid>"
if ($rg.ResourceGroupName -match "^syexam-($uuidRegex)$") {
# $matches[1] contains the captured UUID part from the regex.
[void]$rgUuids.Add($matches[1])
}
}
if ($rgUuids.Count -eq 0) {
Write-Warning "No resource groups found matching the 'syexam-<uuid>' pattern."
} else {
Write-Host "Found $($rgUuids.Count) resource groups with the required 'syexam-<uuid>' name format." -ForegroundColor Green
}
#endregion
#region GATHER AND ANALYZE DNS RECORDS
Write-Host "Fetching 'A' records from DNS zone '$DnsZoneName'..." -ForegroundColor Cyan
$dnsRecords = Get-AzDnsRecordSet -ResourceGroupName $DnsZoneResourceGroupName -ZoneName $DnsZoneName -RecordType A -ErrorAction Stop
Write-Host "Identifying orphaned DNS records..." -ForegroundColor Cyan
$recordsToDelete = @()
foreach ($record in $dnsRecords) {
# Check if the DNS record's name is a UUID.
if ($record.Name -match "^($uuidRegex)$") {
$dnsUuid = $matches[1]
# If the UUID from the DNS record is NOT in our set of resource group UUIDs, it's an orphan.
if (-not $rgUuids.Contains($dnsUuid)) {
$recordsToDelete += $record
}
}
}
#endregion
#region DELETE ORPHANED RECORDS
if ($recordsToDelete.Count -eq 0) {
Write-Host "Scan complete. No orphaned DNS 'A' records found." -ForegroundColor Green
} else {
Write-Warning "Found $($recordsToDelete.Count) orphaned DNS records to delete:"
$recordsToDelete.Name | ForEach-Object { Write-Warning "- $_.$DnsZoneName" }
if ($WhatIf) {
Write-Host "`nRunning in -WhatIf mode. No actual changes will be made." -ForegroundColor Yellow
} else {
# Prompt for confirmation before deleting.
$confirmation = Read-Host "`nAre you sure you want to permanently delete these $($recordsToDelete.Count) DNS records? (y/n)"
if ($confirmation -eq 'y') {
Write-Host "Proceeding with deletion..." -ForegroundColor Cyan
foreach ($record in $recordsToDelete) {
try {
Write-Host "Deleting DNS record set: $($record.Name)..."
Remove-AzDnsRecordSet -Name $record.Name -RecordType $record.RecordType -ZoneName $DnsZoneName -ResourceGroupName $DnsZoneResourceGroupName -Confirm:$false -ErrorAction Stop
Write-Host "Successfully deleted $($record.Name)." -ForegroundColor Green
} catch {
Write-Error "Failed to delete DNS record '$($record.Name)'. Error: $_"
}
}
Write-Host "Cleanup complete." -ForegroundColor Green
} else {
Write-Host "Deletion cancelled by user." -ForegroundColor Yellow
}
}
}
#endregion
}
catch {
Write-Error "An unexpected error occurred: $_"
# Exit with a non-zero status code to indicate failure.
exit 1
}

If a deletion job in Schoolyear AVD fails, it might leave an orphaned IAM role assignment in your Key Vault. Follow these steps to remove these orphaned assignments:

  1. In the Azure portal, navigate to the Key Vault for your AVD implementation.

  2. Go to Access Control (IAM) > Role Assignments.

  3. Filter the list of assignments with the following settings:

    • Type: Unknown
    • Role: Key Vault Secrets User
    • Scope: This resource
  4. Select and delete all role assignments that match this filter.