Manage Azure via PowerShell #1: Get-AzVmInventory

#Requires -Version 7.0
#Requires -Modules Az.Accounts, Az.Compute
<#
.SYNOPSIS
Lists all virtual machines across every subscription the current account can access.
.DESCRIPTION
Iterates through all subscriptions in the active tenant, collects VM details
(name, resource group, location, size, OS, power state, tags), and returns
them as objects suitable for piping or formatting.
.EXAMPLE
./Get-AzVmInventory.ps1
Lists all VMs across all accessible subscriptions in a console table.
.EXAMPLE
./Get-AzVmInventory.ps1 | Where-Object PowerState -eq 'VM running'
Filters to running VMs only.
.EXAMPLE
./Get-AzVmInventory.ps1 | Export-Csv -Path vms.csv -NoTypeInformation
Exports to CSV (we'll add this as a built-in option later).
.NOTES
Author : Sedat Salman
Repo : https://github.com/sdtslmn/AzToolkit
#>
[CmdletBinding()]
param()
# --- 1. Make sure we're authenticated ---------------------------------------
# Get-AzContext returns $null if no one has run Connect-AzAccount in this session.
# We fail early with a clear message rather than hitting cryptic errors later.
if (-not (Get-AzContext)) {
throw "Not connected to Azure. Run 'Connect-AzAccount' first."
}
# --- 2. Discover all subscriptions ------------------------------------------
# Get-AzSubscription returns every subscription the signed-in identity can see.
# We filter to 'Enabled' to skip disabled/deleted subs that would just throw.
Write-Verbose "Discovering subscriptions..."
$subscriptions = Get-AzSubscription | Where-Object State -eq 'Enabled'
if (-not $subscriptions) {
Write-Warning "No enabled subscriptions found for the current account."
return
}
Write-Verbose "Found $($subscriptions.Count) enabled subscription(s)."
# --- 3. Loop through each subscription and collect VMs ----------------------
# Using a generic List<T> instead of += on an array — much faster as the list grows
# (arrays in PowerShell are immutable; += rebuilds the whole array each time).
$inventory = [System.Collections.Generic.List[object]]::new()
foreach ($sub in $subscriptions) {
Write-Verbose "Scanning subscription: $($sub.Name) ($($sub.Id))"
# Set context so subsequent Az cmdlets target this subscription.
# -WarningAction SilentlyContinue suppresses the noisy tenant-warning banner.
try {
Set-AzContext -SubscriptionId $sub.Id -WarningAction SilentlyContinue | Out-Null
}
catch {
Write-Warning "Could not switch to subscription '$($sub.Name)': $_"
continue # skip to the next sub instead of dying
}
# -Status enriches the result with PowerState ('VM running', 'VM deallocated', etc.)
# but it's slower because it makes an extra API call per VM. Worth it for inventory.
try {
$vms = Get-AzVM -Status -ErrorAction Stop
}
catch {
Write-Warning "Failed to list VMs in '$($sub.Name)': $_"
continue
}
foreach ($vm in $vms) {
# Build a flat object with the columns we actually want to see.
# [PSCustomObject] gives us a clean, typed-feeling row in the output table.
$inventory.Add([PSCustomObject]@{
Subscription = $sub.Name
ResourceGroup = $vm.ResourceGroupName
Name = $vm.Name
Location = $vm.Location
Size = $vm.HardwareProfile.VmSize
OS = $vm.StorageProfile.OsDisk.OsType
PowerState = ($vm.PowerState -replace '^VM ', '') # 'running' instead of 'VM running'
# Tags is a hashtable; flatten to "key=value; key=value" for table display
Tags = if ($vm.Tags) {
($vm.Tags.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '; '
} else { '' }
})
}
}
# --- 4. Output --------------------------------------------------------------
# Emit objects to the pipeline. The caller decides what to do with them
# (Format-Table, Export-Csv, Where-Object, etc.). This is the PowerShell way —
# don't format inside the script, output objects.
$inventory | Sort-Object Subscription, ResourceGroup, Name

Leave a comment