Orphaned VHD files in Azure are a common problem and can cause substantial costs over time. The PowerShell script introduced in this post allows the removal of orphaned VHDs in Azure.

Many reasons exist why VHDs become orphaned in Azure. For instance, the migration of on-premises VMs to Azure is a common cause since every migration has its bumps, and the engineers involved usually tend to leave cleaning for later. However, since you pay for every byte of Azure storage, this could cost quite a lot in the long run. Today, I’ll show how to find unwanted VHDs residing in Azure storage accounts and provide a PowerShell script for removing these superfluous files.

Before explaining my PowerShell solution, however, let me demonstrate why the problem is relatively complex. One might think that simply using the Get-AzureDisk cmdlet would return all VHD objects that have ever been attached to a VM. Every such object has the Attached To property (see screenshot below), which reveals whether unattached VHDs exist.

Displaying orphaned VHDS with Get AzureDisk

Displaying orphaned VHDS with Get AzureDisk

As seen in the above screenshot, there is an unattached VHD in my storage account. I can remove the unattached VHD by piping the result Remove-AzureDisk as shown in the screenshot below:

Removing unattached VHDs with Remove AzureDisk

Removing unattached VHDs with Remove AzureDisk

Unfortunately, this is not the end. Many unwanted snapshots or VHDs could exist that were never converted to the Azure disks and, therefore, cannot be easily found.

Since everything stored in Azure storage accounts comes in chunks of data called blobs, the task is to find all of the blobs and cross reference them with all VHDs that are attached to the VMs. The ones that are not attached to any VM are orphaned and should be removed after careful consideration.

To solve this problem, I wrote three PowerShell scripts. The first script gets all Azure blobs from all storage accounts. The second one finds all VHD files attached to VMs. Finally, the last one cross references the results of the first two scripts.

Below is the first script. Comments have been added to help you understand how the script works.

$report = @()
$subscr = "SubscriptionName"
Get-AzureStorageAccount | select Label, AccountType | %{ 
#Getting all Azure storage account names and types and using ForEach-Object loop 
$staccount = $_.Label
$stactype = $_.AccountType
Set-AzureSubscription -SubscriptionName $subscr -CurrentStorageAccountName $staccount #Changing storage account status to current for the subscription
$AllContainers = Get-AzureStorageContainer #Fetting all containers from the storage account
ForEach ($Container in $AllContainers) 
#Looping through all containers in the current storage account
        $CurrentContainer = $Container.Name 
        $AllBlobs = Get-AzureStorageBlob -Container $CurrentContainer 
#Getting all blobs in this container 

        ForEach ($Blob in $AllBlobs) 
#Looping through all blobs in a container and get the properties of each container
              If ($Blob.Name -like '*.vhd' -and $Blob.BlobType -eq "PageBlob")
#Cecking if the blob name has a .vhd extension 
                  $info = ""| Select BlobName, BlobType, BlobLastChangeDate, BlobSizeGB, IsSnaphot, LeaseState, LeaseStatus, AbsoluteUri, StAccountName, AccountType
                  $info.BlobName = $Blob.Name 
                  $info.BlobType = $Blob.BlobType 
                  $info.BlobLastChangeDate = $Blob.LastModified
                  $info.LeaseState = $Blob.ICloudBlob.Properties.LeaseState
                  $info.LeaseStatus = $Blob.ICloudBlob.Properties.LeaseStatus
                  $info.BlobSizeGB = $Blob.Length/1gb
                  $info.IsSnaphot = $Blob.ICloudBlob.IsSnapshot
                  $info.AbsoluteUri = $Blob.ICloudBlob.Uri
                  $info.StAccountName = $staccount
                  $info.AccountType = $stactype
#Storing various blob properties in a custom PowerShell object


$report | Export-csv -Path C:\Temp\Blobs.csv
#Exporting the report into a CSV file

The screenshot below shows the output of the first script:

All blobs report

All blobs report

Now that I have all of the blobs, I need to get all the VHDs that are attached to VMs:

$report = @()
Get-AzureDisk | %{
     $info = "" | Select VmName, DiskName, DiskSize, DiskAbsoluteUri
     $info.VmName = $_.AttachedTo.RoleName
     $info.DiskName = $_.DiskName
     $info.DiskSize = $_.DiskSizeInGB
     $info.DiskAbsoluteUri = $_.MediaLink.AbsoluteUri
$report | Export-csv -Path C:\Temp\VHDs.csv

I do not think this script requires a lot of explanation. I’m just using the Get-AzureDisk cmdlet, already explained above, and saving the data about all the attached VHDs into a CSV file with the following columns: VMName, DiskName, DiskSize, and DiskAbsoluteURI.

In the final step, I compare the blobs to the VHDs to find the orphaned VHD files:

$report = @()
$VHDs = Import-Csv -Path C:\temp\VHDs.csv
$Blbs = Import-Csv -Path C:\temp\Blobs.csv
# Importing the Blobs.csv and VHDs.csv files
$Blbs | %{
#Looping through all blobs with a ForEach-Object loop
   foreach ($vhd in $VHDs){
#Nested ForEach loop where I compare the AbsoluteUrl of the blob to the DiskAbsoluteUri
       if($_.IsVHDFound -eq "yes"){
       if($_.AbsoluteUri -eq $vhd.DiskAbsoluteUri){
#Comparing the blob property AbsoluteUrl and the VHD property DiskAbsoluteUri

          $_ | Add-Member -NotePropertyName IsVHDFound -NotePropertyValue yes -Force
          $_ | Add-Member -NotePropertyName VmName -NotePropertyValue $vhd.VmName -Force
          $_ | Add-Member -NotePropertyName DiskSize -NotePropertyValue $vhd.DiskSize -Force
          $_ | Add-Member -NotePropertyName DiskAbsoluteUri -NotePropertyValue $vhd.DiskAbsoluteUri -Force
          $report+= $_
             $_ | Add-Member -NotePropertyName IsVHDFound -NotePropertyValue no
             $_ | Add-Member -NotePropertyName VmName -NotePropertyValue ""
             $_ | Add-Member -NotePropertyName DiskSize -NotePropertyValue ""
             $_ | Add-Member -NotePropertyName DiskAbsoluteUri -NotePropertyValue ""
             $report+= $_

 $report | Export-csv -Path C:\Temp\orphVHDs.csv

The crucial properties are AbsoluteUrl and DiskAbsoluteUri. Only the addresses that are identical for the blob and the VHD are assigned to VMs. Thus, blobs that do not have a VHD are not assigned to any VM and, therefore, orphaned.

If a VHD exists with the same DiskAbsoluteUri as the current blob, I extend the properties of the blob record using the Add-Member cmdlet and enrich it with the information from the corresponding VHD record. I also add the IsVHDFound field and set it to “yes” so I can skip this record the next time.

If there is no VHD corresponding to a current blob, I add the IsVHDFound property to the record and set it to “no.” I also add the empty properties for the VHD record in order to preserve the CSV file structure. At the end of the script, I export the results to a CSV file.

Subscribe to 4sysops newsletter!

Final report with the orphaned VHDs

Final report with the orphaned VHDs

I can now easily sort by the IsVHDFound column and determine which VHDs may need to be removed.

  1. Shayne 4 years ago

    Hi Alex,

    Thanks for putting in the effort to share these scripts and help the community. I am in the process of using it and wanted to let you know the code is missing a lot of spaces and needs editing to allow it to be parsed. Other than that, great post mate!


  2. Shan 3 years ago

    The script is not working. Could you please provide an updated one.

Leave a reply

Please enclose code in pre tags

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


© 4sysops 2006 - 2021


Please ask IT administration questions in the forums. Any other messages are welcome.


Log in with your credentials


Forgot your details?

Create Account