Azure / Scripts / Inventory and clean-up
Get-AzVMInventory
Lists every Azure VM in every subscription you can see, with size, power state, disks, IPs and tags.
- Runs in
- Az PowerShell with Az.ResourceGraph
- Safety
- Read-only
Download Get-AzVMInventory.ps1
What it does
Uses Azure Resource Graph, so it covers all subscriptions in seconds. For each VM it reports the subscription, resource group, location, zone, size, OS, power state, OS disk type, number of data disks, private IP addresses, Azure Hybrid Benefit license type and tags. Read-only.
Watch the PowerState column: "stopped" (not "deallocated") means the VM is shut down but still billed for compute.
Parameters
-SubscriptionIdstring[] | Limit the report to these subscription IDs. Default: every subscription you can see. |
|---|---|
-OutputFilestring | CSV output path.Default: Timestamped file in the current folder |
Examples
.\Get-AzVMInventory.ps1
.\Get-AzVMInventory.ps1 -SubscriptionId 00000000-0000-0000-0000-000000000000
Notes
Requires Az.Accounts and Az.ResourceGraph, and Reader access. Sign in first with Connect-AzAccount.
Full script
<#
.SYNOPSIS
Lists every Azure VM in every subscription you can see, with size, power state, disks, IPs and tags.
.DESCRIPTION
Uses Azure Resource Graph, so it covers all subscriptions in seconds. For each VM
it reports the subscription, resource group, location, zone, size, OS, power state,
OS disk type, number of data disks, private IP addresses, Azure Hybrid Benefit
license type and tags. Read-only.
Watch the PowerState column: "stopped" (not "deallocated") means the VM is shut
down but still billed for compute.
.PARAMETER SubscriptionId
Limit the report to these subscription IDs. Default: every subscription you can see.
.PARAMETER OutputFile
CSV output path.
.EXAMPLE
.\Get-AzVMInventory.ps1
.EXAMPLE
.\Get-AzVMInventory.ps1 -SubscriptionId 00000000-0000-0000-0000-000000000000
.NOTES
Requires Az.Accounts and Az.ResourceGraph, and Reader access. Sign in first with Connect-AzAccount.
#>
[CmdletBinding()]
param(
[string[]]$SubscriptionId,
[string]$OutputFile = (Join-Path -Path (Get-Location) -ChildPath ('AzVMInventory_{0:yyyyMMdd_HHmm}.csv' -f (Get-Date)))
)
if (-not (Get-Command Search-AzGraph -ErrorAction SilentlyContinue)) {
throw 'Search-AzGraph not found. Run: Install-Module Az.ResourceGraph -Scope CurrentUser'
}
if (-not (Get-AzContext)) { throw 'Not signed in. Run Connect-AzAccount first.' }
function Invoke-GraphQuery {
param([Parameter(Mandatory)][string]$Query)
$results = New-Object System.Collections.Generic.List[object]
$skipToken = $null
do {
$params = @{ Query = $Query; First = 1000 }
if ($SubscriptionId) { $params.Subscription = $SubscriptionId } else { $params.UseTenantScope = $true }
if ($skipToken) { $params.SkipToken = $skipToken }
$page = Search-AzGraph @params
$rows = if ($page.PSObject.Properties['Data']) { $page.Data } else { $page }
foreach ($r in $rows) { $results.Add($r) }
$skipToken = $page.SkipToken
} while ($skipToken)
$results
}
$subNames = @{}
foreach ($s in Invoke-GraphQuery "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name") {
$subNames[$s.subscriptionId] = $s.name
}
Write-Host 'Reading virtual machines...' -ForegroundColor Cyan
$vms = Invoke-GraphQuery @"
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| project id, name, resourceGroup, subscriptionId, location, tags,
zone = tostring(zones[0]),
size = tostring(properties.hardwareProfile.vmSize),
osType = tostring(properties.storageProfile.osDisk.osType),
osDiskType = tostring(properties.storageProfile.osDisk.managedDisk.storageAccountType),
dataDisks = array_length(properties.storageProfile.dataDisks),
powerState = tostring(properties.extended.instanceView.powerState.code),
licenseType = tostring(properties.licenseType),
imageRef = strcat(tostring(properties.storageProfile.imageReference.offer), ' ', tostring(properties.storageProfile.imageReference.sku))
"@
# Private IPs come from the network interfaces attached to each VM
$ipByVm = @{}
$nics = Invoke-GraphQuery @"
Resources
| where type =~ 'microsoft.network/networkinterfaces'
| where isnotnull(properties.virtualMachine)
| mv-expand ipconfig = properties.ipConfigurations
| project vmId = tolower(tostring(properties.virtualMachine.id)), ip = tostring(ipconfig.properties.privateIPAddress)
"@
foreach ($n in $nics) {
if (-not $ipByVm.ContainsKey($n.vmId)) { $ipByVm[$n.vmId] = New-Object System.Collections.Generic.List[string] }
if ($n.ip) { $ipByVm[$n.vmId].Add($n.ip) }
}
$report = foreach ($vm in $vms) {
$tagText = if ($vm.tags) { ($vm.tags.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join '; ' } else { '' }
$key = $vm.id.ToLower()
[pscustomobject]@{
Subscription = $subNames[$vm.subscriptionId]
ResourceGroup = $vm.resourceGroup
Name = $vm.name
Location = $vm.location
Zone = $vm.zone
Size = $vm.size
OS = $vm.osType
Image = "$($vm.imageRef)".Trim()
PowerState = ($vm.powerState -replace '^PowerState/', '')
OsDiskType = $vm.osDiskType
DataDisks = [int]$vm.dataDisks
PrivateIPs = if ($ipByVm.ContainsKey($key)) { $ipByVm[$key] -join ', ' } else { '' }
HybridBenefit = if ($vm.licenseType) { $vm.licenseType } else { 'None' }
Tags = $tagText
}
}
$report | Sort-Object Subscription, ResourceGroup, Name | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8
Write-Host "VMs: $(@($report).Count)" -ForegroundColor Cyan
$report | Group-Object PowerState | Select-Object @{n='PowerState';e={$_.Name}}, Count | Format-Table -AutoSize | Out-Host
$stopped = @($report | Where-Object PowerState -eq 'stopped')
if ($stopped.Count) { Write-Warning "$($stopped.Count) VM(s) are stopped but NOT deallocated, so still billed for compute." }
Write-Host "Report: $OutputFile" -ForegroundColor Green