Dmitry Porotnikov / Script to Identify Unused VHDs in Azure Storage Accounts

Created Fri, 15 Nov 2024 00:00:00 +0100 Modified Fri, 15 Nov 2024 00:00:00 +0100
734 Words

Script to Identify Unused VHDs in Azure Storage Accounts

This script identifies unused VHDs in Azure storage accounts. It first retrieves the list of VHDs associated with virtual machines. Then, it collects all VHDs available in the storage accounts. After that, it compares both lists to find orphaned VHDs that are not attached to any virtual machine, and outputs the URIs of those unused VHDs.


# Script to Identify Unused VHDs in Azure Storage Accounts
# Author: Dmitry Porotnikov
# Disclaimer: This script is provided "as is" without warranty of any kind.
# Pre-requisites: Install the Azure Resource Graph Module using the following command: 
# Install-Module -Name Az.ResourceGraph -AllowClobber -Force
# ------------------------------------------------
# If you will face an issue with Az.ResourceGraph complaining about Az.Accounts being too old:
# Istall-Module -Name Az.Accounts -RequiredVersion 3.0.5
# Then open a new cloud shell session and do:
# Import-Module -Name Az.Accounts -RequiredVersion 3.0.5 -Force
# Import-Module -Name Az.ResourceGraph
#  ------------------------------------------------
# Note: Script tested in Azure Cloud Shell (PowerShell mode)

# Step 1: Get the list of VHDs associated with VMs using Azure Resource Graph
$vmVhds = Search-AzGraph -Query @"
resources
| where type == "microsoft.compute/virtualmachines"
| where properties.storageProfile.osDisk.managedDisk == ""
| extend diskUri = properties.storageProfile.osDisk.vhd.uri
| project diskUri
| union (
    resources
    | where type == "microsoft.compute/virtualmachines"
    | mvexpand dataDisk = properties.storageProfile.dataDisks
    | where dataDisk.managedDisk == ""
    | extend diskUri = dataDisk.vhd.uri
    | project diskUri
)
"@ 

$vmVhdUris = $vmVhds.diskUri

# Step 2: Get all VHDs from storage accounts
$storageAccounts = Get-AzStorageAccount
$allVhds = @()
foreach ($storageAccount in $storageAccounts) {
    $context = $storageAccount.Context
    $containers = Get-AzStorageContainer -Context $context

    foreach ($container in $containers) {
        $blobs = Get-AzStorageBlob -Container $container.Name -Context $context
        $vhdBlobs = $blobs | Where-Object { $_.Name -like "*.vhd" }
        foreach ($vhdBlob in $vhdBlobs) {
            $vhdUri = ($context.BlobEndPoint + $container.Name + "/" + $vhdBlob.Name)
            $allVhds += $vhdUri
        }
    }
}

# Step 3: Find orphaned VHDs by comparing the lists
$orphanedVhds = $allVhds | Where-Object { $_ -notin $vmVhdUris }

# Output the orphaned VHD URIs
$orphanedVhds

To check only certain Storage Accounts in Certain Resource Groups


$resourceGroupName = "YourResourceGroupName"

# Step 1: Get the list of VHDs associated with VMs using Azure Resource Graph
$vmVhds = Search-AzGraph -Query @"
resources
| where type == "microsoft.compute/virtualmachines"
| where properties.storageProfile.osDisk.managedDisk == ""
| extend diskUri = properties.storageProfile.osDisk.vhd.uri
| project diskUri
| union (
    resources
    | where type == "microsoft.compute/virtualmachines"
    | mvexpand dataDisk = properties.storageProfile.dataDisks
    | where dataDisk.managedDisk == ""
    | extend diskUri = dataDisk.vhd.uri
    | project diskUri
)
"@ 

$vmVhdUris = $vmVhds.diskUri

# Step 2: Get all storage accounts in the resource group
$storageAccounts = Get-AzStorageAccount -ResourceGroupName $resourceGroupName

# Step 3: Get all VHDs from all storage accounts
$allVhds = @()

foreach ($storageAccount in $storageAccounts) {
    $context = $storageAccount.Context
    $containers = Get-AzStorageContainer -Context $context

    foreach ($container in $containers) {
        $blobs = Get-AzStorageBlob -Container $container.Name -Context $context
        $vhdBlobs = $blobs | Where-Object { $_.Name -like "*.vhd" }

        foreach ($vhdBlob in $vhdBlobs) {
            $vhdUri = ($context.BlobEndPoint + $container.Name + "/" + $vhdBlob.Name)
            $allVhds += $vhdUri
        }
    }
}

# Step 4: Find orphaned VHDs by comparing the lists
$orphanedVhds = $allVhds | Where-Object { $_ -notin $vmVhdUris }

# Output the orphaned VHD URIs
$orphanedVhds

Multiple Resource Groups


# Define the list of resource groups
$resourceGroupNames = @("ResourceGroup1", "ResourceGroup2", "ResourceGroup3")

# Step 1: Get the list of VHDs associated with VMs using Azure Resource Graph
$vmVhds = Search-AzGraph -Query @"
resources
| where type == "microsoft.compute/virtualmachines"
| where properties.storageProfile.osDisk.managedDisk == ""
| extend diskUri = properties.storageProfile.osDisk.vhd.uri
| project diskUri
| union (
    resources
    | where type == "microsoft.compute/virtualmachines"
    | mvexpand dataDisk = properties.storageProfile.dataDisks
    | where dataDisk.managedDisk == ""
    | extend diskUri = dataDisk.vhd.uri
    | project diskUri
)
"@ 

$vmVhdUris = $vmVhds.diskUri

# Step 2: Initialize an array to store VHD URIs
$allVhds = @()

# Step 3: Iterate over each resource group and get the storage accounts
foreach ($resourceGroupName in $resourceGroupNames) {
    $storageAccounts = Get-AzStorageAccount -ResourceGroupName $resourceGroupName

    foreach ($storageAccount in $storageAccounts) {
        $context = $storageAccount.Context
        $containers = Get-AzStorageContainer -Context $context

        foreach ($container in $containers) {
            $blobs = Get-AzStorageBlob -Container $container.Name -Context $context
            $vhdBlobs = $blobs | Where-Object { $_.Name -like "*.vhd" }

            foreach ($vhdBlob in $vhdBlobs) {
                $vhdUri = ($context.BlobEndPoint + $container.Name + "/" + $vhdBlob.Name)
                $allVhds += $vhdUri
            }
        }
    }
}

# Step 4: Find orphaned VHDs by comparing the lists
$orphanedVhds = $allVhds | Where-Object { $_ -notin $vmVhdUris }

# Output the orphaned VHD URIs
$orphanedVhds