Azure Tech Tip: Find VM by features and size easily

When searching for the right Azure VM size, the documentation can feel like a maze. It’s frustrating to endlessly scroll through size tables just to find “4 cores, 8 GB RAM, ephemeral disk support, and accelerated networking in West Europe.” Sound familiar?

To save yourself from this common struggle, here’s a PowerShell Core script that filters Azure VM SKUs using flexible criteria—no need to open browser tabs or squint at feature matrices.

Introducing Get-AvailableVMs.ps1

The script is available in the pws-helpers GitHub repository and is maintained under:

scripts/Get-AvailableVMs.ps1

What It Does

The script lets you search VM sizes in a specific Azure region by specifying exact or minimum requirements. It queries the Azure CLI’s VM SKU metadata, parses capabilities, and displays filtered results based on:

  • vCPU count
  • Memory (in GB)
  • Disk IOPS
  • NIC count
  • Accelerated networking support
  • Ephemeral OS disk capability
  • Premium disk support
  • Capacity reservation support
  • VM family (e.g., D, E, NV)
  • Availability for deployment in your subscription

Usage Examples

Find all DS-series VMs with 4 cores and ephemeral disk support in West Europe:


.\Get-AvailableVMs.ps1 -Region "westeurope" -Cores 4 -EphemeralOSDisk true -Family "DS"

Find the latest versions only (e.g., only DSv5, not DSv4):


.\Get-AvailableVMs.ps1 -Region "westeurope" -Family "DS" -Latest

Search for all available VMs with accelerated networking and PremiumIO:


.\Get-AvailableVMs.ps1 -Region "westeurope" -AcceleratedNetworking true -PremiumIO true -Available true

The script:


param (
    [string]$Region = "westeurope",
    [string]$Mode = "exact",
    [int]$Cores,
    [int]$Memory,
    [int]$IOPS,
    [int]$NICs,
    $AcceleratedNetworking,
    $EphemeralOSDisk,
    $PremiumIO,
    $CapacityReservation,
    $Available,
    [string]$Family,
    [switch]$Latest

)

# Normalize and validate parameters
$Region = $Region.ToLower()
$Mode = $Mode.ToLower()

# Convert boolean-like parameters to actual booleans
function ConvertTo-Boolean($value) {
    if ($null -eq $value) { return $null }
    if ($value -is [bool]) { return $value }
    if ($value -is [int]) { return [bool]$value }
    if ($value -is [string]) {
        switch ($value.ToLower()) {
            "true" { return $true }
            "false" { return $false }
            "1" { return $true }
            "0" { return $false }
            "`$true" { return $true }
            "`$false" { return $false }
            default { 
                Write-Error "Invalid boolean value: '$value'. Use true/false, 1/0, or `$true/`$false"
                exit 1
            }
        }
    }
    Write-Error "Cannot convert value '$value' to boolean"
    exit 1
}

# Convert boolean parameters
if ($PSBoundParameters.ContainsKey("AcceleratedNetworking")) {
    $AcceleratedNetworking = ConvertTo-Boolean $AcceleratedNetworking
}
if ($PSBoundParameters.ContainsKey("EphemeralOSDisk")) {
    $EphemeralOSDisk = ConvertTo-Boolean $EphemeralOSDisk
}
if ($PSBoundParameters.ContainsKey("PremiumIO")) {
    $PremiumIO = ConvertTo-Boolean $PremiumIO
}
if ($PSBoundParameters.ContainsKey("CapacityReservation")) {
    $CapacityReservation = ConvertTo-Boolean $CapacityReservation
}
if ($PSBoundParameters.ContainsKey("Available")) {
    $Available = ConvertTo-Boolean $Available
}

# Validate Mode parameter
if ($Mode -notin @("min", "exact")) {
    Write-Error "Mode must be 'min' or 'exact' (case-insensitive). Provided value: '$Mode'"
    exit 1
}

# Check Azure CLI
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
    Write-Error "Azure CLI (az) not found. Please install and log in using 'az login'."
    exit 1
}

if (-not $PSBoundParameters.ContainsKey("Cores") -and -not $PSBoundParameters.ContainsKey("Memory") -and
    -not $PSBoundParameters.ContainsKey("IOPS") -and -not $PSBoundParameters.ContainsKey("NICs") -and
    -not $PSBoundParameters.ContainsKey("AcceleratedNetworking") -and
    -not $PSBoundParameters.ContainsKey("EphemeralOSDisk") -and
    -not $PSBoundParameters.ContainsKey("PremiumIO") -and
    -not $PSBoundParameters.ContainsKey("CapacityReservation") -and
    -not $PSBoundParameters.ContainsKey("Available") -and
    -not $PSBoundParameters.ContainsKey("Family") -and
    -not $Latest) {
    Write-Error "At least one filter parameter must be provided."
    exit 1
}

Write-Output "Fetching VM SKUs for region '$Region'..."

# Fetch VM SKUs with better error handling for cross-platform compatibility
$rawSkusJson = az vm list-skus --location $Region --resource-type "virtualMachines" --output json
if ($LASTEXITCODE -ne 0) {
    Write-Error "Failed to fetch VM SKUs from Azure CLI. Please ensure you are logged in with 'az login'."
    exit 1
}

# Handle JSON conversion with better cross-platform support
try {
    $rawSkus = $rawSkusJson | ConvertFrom-Json -Depth 10
} catch {
    Write-Error "Failed to parse VM SKU data. Error: $($_.Exception.Message)"
    exit 1
}

# Process SKUs
$vmSkus = $rawSkus | Where-Object { 
    # Case-insensitive region comparison
    $_.locations | ForEach-Object { $_.ToLower() } | Where-Object { $_ -eq $Region }
} | ForEach-Object {
    $capabilities = $_.capabilities

    $skuCores     = ($capabilities | Where-Object { $_.name -eq "vCPUs" }).value
    $skuMemory    = ($capabilities | Where-Object { $_.name -eq "MemoryGB" }).value
    $skuIOPS      = ($capabilities | Where-Object { $_.name -eq "UncachedDiskIOPS" }).value
    $accelNet     = ($capabilities | Where-Object { $_.name -eq "AcceleratedNetworkingEnabled" }).value
    $ephemeral    = ($capabilities | Where-Object { $_.name -eq "EphemeralOSDiskSupported" }).value
    $premiumIOCap = ($capabilities | Where-Object { $_.name -eq "PremiumIO" })
    $premiumIOValue = if ($premiumIOCap) { $premiumIOCap.value } else { $null }
    $reservationCap = ($capabilities | Where-Object { $_.name -eq "CapacityReservationSupported" })
    $reservationValue = if ($reservationCap) { $reservationCap.value } else { $null }
    $maxNics      = ($capabilities | Where-Object { $_.name -eq "MaxNetworkInterfaces" }).value

    # Check if VM is available for deployment (no location restrictions) - case-insensitive
    $isAvailable = -not ($_.restrictions | Where-Object { 
        $_.type -eq "Location" -and 
        ($_.restrictionInfo.locations | ForEach-Object { $_.ToLower() } | Where-Object { $_ -eq $Region }) -and
        ($_.reasonCode -eq "NotAvailableForSubscription" -or $_.reasonCode -eq "NotAvailableForRegion")
    })

    [PSCustomObject]@{
        Name                   = $_.name
        Size                   = $_.size
        Cores                  = [int]$skuCores
        Memory                 = [double]$skuMemory
        IOPS                   = if ($skuIOPS) { [int]$skuIOPS } else { $null }
        AcceleratedNetworking  = if (![string]::IsNullOrWhiteSpace($accelNet)) { [bool]::Parse($accelNet) } else { $false }
        EphemeralOSDisk        = if (![string]::IsNullOrWhiteSpace($ephemeral)) { [bool]::Parse($ephemeral) } else { $false }
        PremiumIO              = if (![string]::IsNullOrWhiteSpace($premiumIOValue)) { [bool]::Parse($premiumIOValue) } else { $false }
        CapacityReservation    = if (![string]::IsNullOrWhiteSpace($reservationValue)) { [bool]::Parse($reservationValue) } else { $false }
        MaxNICs                = if ($maxNics) { [int]$maxNics } else { $null }
        Available              = $isAvailable
        Family                 = $_.family
    }

}

$filtered = $vmSkus | Where-Object {
    # If only -Latest is specified, include all VMs (no filtering)
    if ($Latest -and -not $PSBoundParameters.ContainsKey("Cores") -and -not $PSBoundParameters.ContainsKey("Memory") -and
        -not $PSBoundParameters.ContainsKey("IOPS") -and -not $PSBoundParameters.ContainsKey("NICs") -and
        -not $PSBoundParameters.ContainsKey("AcceleratedNetworking") -and
        -not $PSBoundParameters.ContainsKey("EphemeralOSDisk") -and
        -not $PSBoundParameters.ContainsKey("PremiumIO") -and
        -not $PSBoundParameters.ContainsKey("CapacityReservation") -and
        -not $PSBoundParameters.ContainsKey("Available") -and
        -not $PSBoundParameters.ContainsKey("Family")) {
        return $true
    }

    $match = $true

    if ($Mode -eq "exact") {
        if ($PSBoundParameters.ContainsKey("Cores")) {
            $match = $match -and ($_.Cores -eq $Cores)
        }
        if ($PSBoundParameters.ContainsKey("Memory")) {
            $match = $match -and ($_.Memory -eq $Memory)
        }
        if ($PSBoundParameters.ContainsKey("IOPS")) {
            $match = $match -and ($_.IOPS -eq $IOPS)
        }
        if ($PSBoundParameters.ContainsKey("NICs")) {
            $match = $match -and ($_.MaxNICs -eq $NICs)
        }
    } else {
        if ($PSBoundParameters.ContainsKey("Cores")) {
            $match = $match -and ($_.Cores -ge $Cores)
        }
        if ($PSBoundParameters.ContainsKey("Memory")) {
            $match = $match -and ($_.Memory -ge $Memory)
        }
        if ($PSBoundParameters.ContainsKey("IOPS")) {
            $match = $match -and ($_.IOPS -ge $IOPS)
        }
        if ($PSBoundParameters.ContainsKey("NICs")) {
            $match = $match -and ($_.MaxNICs -ge $NICs)
        }
    }

    if ($PSBoundParameters.ContainsKey("AcceleratedNetworking")) {
        $match = $match -and ($_.AcceleratedNetworking -eq $AcceleratedNetworking)
    }
    if ($PSBoundParameters.ContainsKey("EphemeralOSDisk")) {
        $match = $match -and ($_.EphemeralOSDisk -eq $EphemeralOSDisk)
    }
    if ($PSBoundParameters.ContainsKey("PremiumIO")) {
        $match = $match -and ($_.PremiumIO -eq $PremiumIO)
    }
    if ($PSBoundParameters.ContainsKey("CapacityReservation")) {
        $match = $match -and ($_.CapacityReservation -eq $CapacityReservation)
    }
    if ($PSBoundParameters.ContainsKey("Available")) {
        $match = $match -and ($_.Available -eq $Available)
    }
    if ($PSBoundParameters.ContainsKey("Family")) {
        $vmFamilyPrefix = ($_.Size -replace '^([A-Za-z]{1,2}).*', '$1')

        $match = $match -and ($vmFamilyPrefix -eq $Family)
    }

    return $match
}

# Filter for latest versions if requested
if ($Latest) {
    
    $filtered = $filtered | Group-Object { 
        # Extract base family name by removing version and promo suffixes
        $name = $_.Name -replace '^Standard_', ''
        $baseName = $name -replace '_v\d+.*$', '' -replace '_Promo$', ''
        return $baseName
    } | ForEach-Object {
        $familyVMs = $_.Group
        
        # Group by version number
        $versionGroups = $familyVMs | Group-Object {
            $name = $_.Name -replace '^Standard_', ''
            if ($name -match '_v(\d+)') {
                [int]$matches[1]
            } else {
                1  # No version suffix means version 1
            }
        }
        
        # Get the highest version number and return all VMs with that version
        $maxVersion = ($versionGroups | Measure-Object Name -Maximum).Maximum
        $selectedVMs = ($versionGroups | Where-Object { [int]$_.Name -eq $maxVersion }).Group
        
        return $selectedVMs
    }
}

$sorted = $filtered | Sort-Object Memory, Cores

if ($sorted.Count -eq 0) {
    Write-Output "No VM sizes match the criteria."
} else {
    # Count available VMs vs total filtered VMs
    $availableCount = ($sorted | Where-Object { $_.Available -eq $true }).Count
    $totalCount = $sorted.Count
    Write-Output "Available $availableCount/$totalCount VM sizes matching criteria in $Region"
    Write-Output ""
    
    if ($IsWindows) {
        $sorted | Out-GridView -Title "Matching Azure VM Sizes in $Region (Available: $availableCount/$totalCount)"
    } else {
        $sorted | Select-Object Name,
            @{Name="MainFamily"; Expression={($_.Name -replace '^Standard_', '') -replace '^([A-Za-z]{1,2}).*', '$1'}},
            Cores,
            @{Name="MemoryGB"; Expression = { ($_.Memory.ToString("0.###")) }},
            IOPS, AcceleratedNetworking, EphemeralOSDisk, PremiumIO, CapacityReservation, Available, MaxNICs, Family |
        Format-Table -AutoSize
    }
}

Why This Helps

  • Saves time compared to manually browsing Azure SKU documentation
  • Supports scripting, automation, and repeatable queries
  • Compatible with Windows, macOS, and Linux using PowerShell Core
  • Respects availability for your current subscription (some SKUs are restricted)

Tips

  • Use -Mode exact (default) for precise matches, or -Mode min for >= filters
  • Use the script in DevOps pipelines to validate SKU availability before deployment
This entry was posted in Azure and tagged , , , . Bookmark the permalink.