In my previous article I went over why and when we sometimes face capacity issues in Azure while trying to provision Virtual Machines : Understanding and Overcoming Azure VM SKU Capacity Limitations.
In this article I want to share more advanced automation to deal with that task automatically:
Scripted SKU Availability Testing
To help assess which SKUs are currently available in specific zones, you can use a PowerShell script that:
- Lists available VM SKUs in your selected region.
- Tests zonal provisioning via
-WhatIf
simulation. - Helps you proactively plan capacity before scaling or migrating workloads.
This script is especially helpful when preparing infrastructure for:
- Production launches
- Lift-and-shift migrations
- Scaling workloads into new zones
Feel free to use and tweek according to your needs:
<# | |
.AUTHOR | |
Vukasin Terzic | |
https://azureis.fun | |
#> | |
function Get-AzVMSKUAvailabilityInZone { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$Region, | |
[Parameter(Mandatory = $true)] | |
[string]$ResourceGroupName, | |
[Parameter(Mandatory = $false)] | |
[string[]]$Skus, | |
[Parameter(Mandatory = $false)] | |
[string]$ExportCsvPath, | |
[Parameter(Mandatory = $false)] | |
[switch]$CleanUpAfter | |
) | |
# Ensure Az module is available | |
if (-not (Get-Module -ListAvailable -Name Az.Compute)) { | |
Write-Error "Az.Compute module not found. Please install the Az module first." | |
return | |
} | |
# Create Resource Group if it doesn't exist | |
if (-not (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) { | |
Write-Host "Creating resource group '$ResourceGroupName' in region '$Region'..." | |
New-AzResourceGroup -Name $ResourceGroupName -Location $Region | Out-Null | |
Start-Sleep -Seconds 5 # Wait for resource group to be created | |
} else { | |
Write-Host "Using existing Resource group '$ResourceGroupName'." | |
} | |
# Create test networking resources | |
$vnetName = "vmtest-vnet1" | |
$subnetName = "vmtest-subnet1" | |
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue | |
if (-not $vnet) { | |
Write-Host "Creating test VNet and Subnet..." | |
$subnetConfig = New-AzVirtualNetworkSubnetConfig -Name $subnetName -AddressPrefix "10.0.0.0/24" | |
$vnet = New-AzVirtualNetwork -Name $vnetName -ResourceGroupName $ResourceGroupName -Location $Region -AddressPrefix "10.0.0.0/16" -Subnet $subnetConfig | |
} else { | |
Write-Host "Using existing VNet '$vnetName' and Subnet '$subnetName'..." | |
} | |
$subnet = $vnet.Subnets[0] | |
# Check if the region is valid | |
Write-Host "Validating region '$Region'..." | |
$validRegions = Get-AzLocation | Select-Object -ExpandProperty Location | |
if ($Region -notin $validRegions) { | |
Write-Error "The specified region '$Region' is not valid. Available regions are: $($validRegions -join ', ')" | |
return | |
} | |
# Get available VM SKUs | |
Write-Host "Fetching VM SKUs for region '$Region'..." | |
$allSkus = Get-AzComputeResourceSku | Where-Object { $_.Locations -contains $Region -and $_.ResourceType -eq "virtualMachines" } | |
# Validate user-supplied SKUs | |
if ($Skus) { | |
$availableSkuNames = $allSkus.Name | |
$notFoundSkus = $Skus | Where-Object { $_ -notin $availableSkuNames } | |
if ($notFoundSkus.Count -gt 0) { | |
Write-Warning "The following SKUs were not found or are not available in region '$Region': $($notFoundSkus -join ', ') and will be ignored." | |
} | |
$filteredSkus = $allSkus | Where-Object { $Skus -contains $_.Name } | |
} else { | |
$filteredSkus = $allSkus | |
} | |
$results = @() | |
# Generate temporary random credentials | |
function New-RandomPassword { | |
param([int]$length = 16) | |
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*' | |
-join ((1..$length) | ForEach-Object { $chars | Get-Random }) | |
} | |
$adminUsername = "vmadmin" | |
$adminPassword = ConvertTo-SecureString (New-RandomPassword) -AsPlainText -Force | |
$cred = New-Object System.Management.Automation.PSCredential ($adminUsername, $adminPassword) | |
foreach ($sku in $filteredSkus) { | |
$zones = $sku.LocationInfo | Where-Object { $_.Location -eq $Region } | Select-Object -ExpandProperty Zones | |
if ($zones) { | |
foreach ($zone in $zones) { | |
$vmName = "vmtest-$($sku.Name.ToLower())-z$zone" | |
$nicName = "$vmName-nic" | |
try { | |
Write-Host "Creating NIC for $vmName ..." | |
$nic = New-AzNetworkInterface -Name $nicName -ResourceGroupName $ResourceGroupName -Location $Region -SubnetId $subnet.Id -ErrorAction Stop | |
$vmConfig = New-AzVMConfig -VMName $vmName -VMSize $sku.Name -Zone $zone | |
$vmConfig = Set-AzVMOperatingSystem -VM $vmConfig -Windows -ComputerName $vmName -Credential $cred -ProvisionVMAgent -EnableAutoUpdate | |
$vmConfig = Set-AzVMSourceImage -VM $vmConfig -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus "2019-Datacenter" -Version "latest" | |
$vmConfig = Add-AzVMNetworkInterface -VM $vmConfig -Id $nic.Id | |
#Disable boot diagnostics by not attaching a diagnostics profile. Otherwise it will require storage account. | |
Write-Host "Disabling boot diagnostics for $vmName ..." | |
$vmConfig.DiagnosticsProfile = $null | |
Write-Host "Testing provisioning of $vmName in zone $zone using WhatIf..." | |
$whatIf = New-AzVM -ResourceGroupName $ResourceGroupName -Location $Region -VM $vmConfig -WhatIf -ErrorAction Stop | |
$results += [PSCustomObject]@{ | |
VMSize = $sku.Name | |
Zone = $zone | |
AvailableInZone = $true | |
ProvisionableNow = $true | |
} | |
} | |
catch { | |
Write-Warning "Provisioning test failed for $($sku.Name) in zone $zone : $($_.Exception.Message)" | |
$results += [PSCustomObject]@{ | |
VMSize = $sku.Name | |
Zone = $zone | |
AvailableInZone = $true | |
ProvisionableNow = $false | |
} | |
} | |
# Cleanup NIC | |
Write-Host "Cleaning up NIC: $nicName" | |
Remove-AzNetworkInterface -Name $nicName -ResourceGroupName $ResourceGroupName -Force -ErrorAction SilentlyContinue | |
} | |
} | |
else { | |
$results += [PSCustomObject]@{ | |
VMSize = $sku.Name | |
Zone = "-" | |
AvailableInZone = $false | |
ProvisionableNow = $false | |
} | |
} | |
} | |
if ($ExportCsvPath) { | |
Write-Host "Exporting results to $ExportCsvPath" | |
$results | Export-Csv -Path $ExportCsvPath -NoTypeInformation | |
} | |
# Optional cleanup of entire resource group | |
if ($CleanUpAfter) { | |
Write-Host "Cleaning up entire resource group '$ResourceGroupName'..." | |
Remove-AzResourceGroup -Name $ResourceGroupName -Force -AsJob | |
} else { | |
Write-Host "Test completed. Resource group '$ResourceGroupName' retained for review." | |
} | |
return $results | |
} | |
# Example usage | |
Get-AzVMSKUAvailabilityInZone -Region "eastus2" -ResourceGroupName "TempRG1" -Skus "Standard_D4s_v5", "Standard_D8s_v5" -ExportCsvPath "C:\Temp\Report.csv" -CleanUpAfter |
The result you will receive will look like this:
In case you have any questions feel free to reach out.
Thanks for reading and keep clouding around.
Vukasin Terzic