ADDRS.psm1
<#
.SYNOPSIS Automatically right sizes a given VM based on CPU, memory, performance rating and cost. Can run in many modes and is highly configurable (WhatIf, Force, etc) Check Get-Help for the following functions to determine which one to use: * set-vmRightSize * set-rsgRightSize .NOTES filename: AADRS.psm1 author: Jos Lieben / jos@lieben.nu copyright: https://www.lieben.nu/liebensraum/commercial-use/ (Commercial (re)use not allowed without prior written consent by the author, otherwise free to use/modify as long as header are kept intact) site: https://www.lieben.nu/liebensraum/2022/05/automatic-modular-rightsizing-of-azure-vms-with-special-focus-on-azure-virtual-desktop/ Created: 16/05/2022 Updated: see Git: https://gitlab.com/Lieben/assortedFunctions/-/tree/master/ADDRS #> function set-vmToSize{ [cmdletbinding()] Param( [Parameter(Mandatory)][Object]$vm, [Parameter(Mandatory)][String]$newSize, [Switch]$Force, [Switch]$Boot, [Switch]$WhatIf ) if($vm.HardwareProfile.VmSize -ne $newSize){ if($vm.PowerState -eq "VM running"){ if($Force){ Write-Verbose "Stopping $($vm.Name) as it is running and -Force was specified" if(!$WhatIf){ Stop-AzVM -Name $($vm.Name) -Confirm:$False -Force } Write-Verbose "Stopped $($vm.Name)" }else{ Throw "$($vm.Name) still running, cannot resize a running VM. Use -Force if you wish to shut down $($vm.Name) automatically" } }else{ Write-Verbose "$($vm.Name) is already stopped or deallocated" } $vm.HardwareProfile.VmSize = $newSize if(!$WhatIf){ Write-Verbose "Sending resize command" $retVal = ($vm | Update-AzVM).StatusCode Write-Host "VM resize result: $($retVal)" }else{ Write-Host "Not sending resize command because running in -WhatIf" $retVal = "OK" } if($Boot){ if(!$WhatIf){ Write-Verbose "Starting $($vm.Name) as -Boot was specified" Start-AzVM -Name $($vm.Name) -Confirm:$False -NoWait }else{ Write-Verbose "-Boot specified, but not booting as -WhatIf was specified" } } return $retVal }else{ Throw "VM already at specified size" } } function get-azureVMPricesAndPerformance{ [cmdletbinding()] Param( [String][Parameter(Mandatory)]$region ) $vmPrices = @() $vmPricingData = Invoke-RestMethod -Uri "https://prices.azure.com/api/retail/prices?meterRegion=primary&api-version=2021-10-01-preview¤cyCode='USD'&`$filter=serviceName eq 'Virtual Machines' and priceType eq 'Consumption' and armRegionName eq '$region'" -Usebasicparsing -Method GET -ContentType "application/json" $vmPrices += $vmPricingData.Items while($vmPricingData.NextPageLink){ $vmPricingData = Invoke-RestMethod -Uri $vmPricingData.NextPageLink -Usebasicparsing -Method GET -ContentType "application/json" $vmPrices += $vmPricingData.Items } Write-Verbose "$($vmPrices.Count) prices retrieved, retrieving performance scores..." $vmScoreRawData = (Invoke-RestMethod -Uri "https://raw.githubusercontent.com/MicrosoftDocs/azure-docs/main/articles/virtual-machines/linux/compute-benchmark-scores.md" -Method GET -UseBasicParsing) -split "`n" $vmScoreData = @() $inTable = $False for($l=0;$l -lt $vmScoreRawData.Count; $l++){ if($vmScoreRawData[$l].StartsWith("| VM Size |")){ #skip a line $l++ $inTable = $True continue } if($inTable){ if(!$vmScoreRawData[$l].StartsWith("| ")){ $inTable = $False continue } $lineData = $vmScoreRawData[$l].Split("|") $vmScoreData += [PSCustomObject]@{ "type" = $lineData[1].Trim() "perf" = $lineData[6].Trim() } } } Write-Verbose "$($vmScoreData.Count) performance rows retrieved, merging data..." $global:azureVMPrices = @() $vmPrices = $vmPrices | where{-Not($_.skuName.EndsWith("Spot")) -and -Not($_.skuName.EndsWith("Low Priority"))} foreach($sku in ($vmPrices.armSkuName | Select-Object -Unique)){ $vmPricing = $vmPrices | where{$_.armSkuName -eq $sku} $obj = [PSCustomObject]@{ "Name" = $sku "numberOfCores" = $($global:azureAvailableVMSizes | where{$_.Name -eq $sku}).NumberOfCores "memoryInMB" = $($global:azureAvailableVMSizes | where{$_.Name -eq $sku}).MemoryInMB "linuxPrice" = $($vmPricing | where{!$_.productName.EndsWith("Windows")}).retailPrice "windowsPrice" = $($vmPricing | where{$_.productName.EndsWith("Windows")}).retailPrice "perf" = $($vmScoreData | where{$_.type -eq $sku} | Sort-Object -Property perf | Select-Object -Last 1).perf } $global:azureVMPrices+= $obj } } function get-vmRightSize{ [cmdletbinding()] Param( [Parameter(Mandatory)][String]$targetVMName, [Parameter(Mandatory)][Guid]$workspaceId, #workspace GUID where perf data is stored (use Get-AzOperationalInsightsWorkspace to find this) $domain, #if your machines are domain joined, enter the domain name here [Int]$maintenanceWindowStartHour, #start hour of maintenancewindow in military time UTC (0-23) [Int]$maintenanceWindowLengthInHours, #length of maintenance window in hours (round up if needed) [ValidateSet(0,1,2,3,4,5,6)][Int]$maintenanceWindowDay, #day on which the maintenance window starts (UTC) where 0 = Sunday and 6 = Saturday [String]$region = "westeurope", #you can find yours using Get-AzLocation | select Location [Int]$measurePeriodHours = 152, #lookback period for a VM's performance while it was online, this is used to calculate the optimum. It is not recommended to size multiple times in this period! [Array]$allowedVMTypes = @("Standard_D2ds_v4","Standard_D4ds_v4","Standard_D8ds_v4","Standard_D2ds_v5","Standard_D4ds_v5","Standard_D8ds_v5","Standard_E2ds_v4","Standard_E4ds_v4","Standard_E8ds_v4","Standard_E2ds_v5","Standard_E4ds_v5","Standard_E8ds_v5") ) $script:reportRow = [PSCustomObject]@{ "vmName"=$targetVMName "currentSize"=$Null "targetSize"=$Null "resized"=$False "costImpactPercent"=$Null "reason"=$Null } #####CONFIGURATION########################## $vCPUTrigger = 0.75 #if a CPU is over 75% + the differerence percent on average, a vCPU should be added. If under 75% - the difference percent, the optimum amount should be calculated $memoryTrigger = 0.75 #if this percentage of memory + the difference percent is in use on average, more should be added. If under this percentage - the difference percent, memory should be recalculated $rightSizingMinimumDifferencePercent = 0.10 #minimum difference/buffer of 10% to avoid VM's getting resized back and forth every time you call this function $minMemoryGB = 4 #will never assign less than this (even if you've allowed VM's with more) $maxMemoryGB = 32 #will never assign more than this (even if you've allowed VM's with more) $minvCPUs = 2 #min 2 required for network acceleration! $maxvCPUs = 12 #in no case will this function assign a vmtype with more vCPU's than this $defaultSize = "" #if specified, VM's that do not have performance data will be sized to this size as the fallback size. If you don't specify anything, they will remain at their current size untill performance data for right sizing is available #####END OF OPTIONAL CONFIGURATION######### $cul = $vCPUTrigger + $rightSizingMinimumDifferencePercent $cll = $vCPUTrigger - $rightSizingMinimumDifferencePercent $mul = $memoryTrigger + $rightSizingMinimumDifferencePercent $mll = $memoryTrigger - $rightSizingMinimumDifferencePercent #determine Azure Monitor query parameters in case a maintenance window was specified if($domain){ $domain = ".$($domain)" } if($maintenanceWindowStartHour -and $maintenanceWindowDay -and $maintenanceWindowLengthInHours){ $start = ([datetime]"2022-02-01T$($maintenanceWindowStartHour):00:00") $end = $start.AddHours($maintenanceWindowLengthInHours) if($start.Day -eq $end.Day){ $queryAddition = " and ((dayofweek(TimeGenerated) == $($maintenanceWindowDay)d and (hourofday(TimeGenerated) < $maintenanceWindowStartHour or hourofday(TimeGenerated) > $($end.Hour))) or dayofweek(TimeGenerated) != $($maintenanceWindowDay)d)" }else{ $queryAddition = " and ((dayofweek(TimeGenerated) == $($maintenanceWindowDay)d and (hourofday(TimeGenerated) < $maintenanceWindowStartHour)) or dayofweek(TimeGenerated) != $($maintenanceWindowDay)d) and ((dayofweek(TimeGenerated) == $($maintenanceWindowDay+1)d and (hourofday(TimeGenerated) > $($end.Hour))) or dayofweek(TimeGenerated) != $($maintenanceWindowDay+1)d)" } Write-Verbose "$targetVMName grabbing data to calculate optimal size excluding maintenance window on day $maintenanceWindowDay at $maintenanceWindowStartHour for $maintenanceWindowLengthInHours hours" }else{ $queryAddition = $Null Write-Verbose "$targetVMName grabbing data to calculate optimal size" } #use a global var to cache data between subsequent calls to list all available Azure VM sizes in the region if(!$global:azureAvailableVMSizes){ try{ Write-Host "No VM size cache for $region yet, creating this first...." $global:azureAvailableVMSizes = Get-AzVMSize -Location $region -ErrorAction Stop Write-Host "VM Size cache created" Write-Verbose "Cached the following available VM types in $region :" Write-Verbose ($global:azureAvailableVMSizes.Name -Join ",") }catch{ Throw "$targetVMName failed to retrieve available Azure VM sizes in region $region because of $_" } } #use a global var to cache data between subsequent calls to list cost and performance data in the selected region if(!$global:azureVMPrices){ try{ Write-Host "No cache of VM performance and pricing data yet, creating this first...." get-azureVMPricesAndPerformance -region $region Write-Host "VM Performance and pricing data cached" }catch{ Throw "$targetVMName failed to get pricing and performance data for Azure VM sizes because of $_" } } #enrich all allowed VM's with pricing data and remove any that are not availabe in the selected region $selectedVMTypes = @() foreach($allowedVMType in $allowedVMTypes){ if($azureAvailableVMSizes.Name -contains $allowedVMType){ $vmPricingInfo = $Null $vmPricingInfo = $azureVMPrices | where{$_.Name -eq $allowedVMType} if($vmPricingInfo){ $selectedVMTypes += [PSCustomObject]@{ "Name" = $allowedVMType "NumberOfCores" = $vmPricingInfo.numberOfCores "MemoryInMB" = $vmPricingInfo.memoryInMB "linuxPrice" = $vmPricingInfo.linuxPrice "windowsPrice" = $vmPricingInfo.windowsPrice #https://docs.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices "perf" = $vmPricingInfo.perf #https://docs.microsoft.com/en-us/azure/virtual-machines/linux/compute-benchmark-scores#about-coremark } } } } #sort the VM types we may use based on their price first, then performance rating $selectedVMTypes = $selectedVMTypes | Sort-Object @{e={$_.windowsPrice};a=1},@{e={$_.perf}; a=0},@{e={$_.Name.Split("_")[-1]}; a=0} Write-Verbose "Allowed VM types: $($selectedVMTypes.Name -Join ",")" #error out if none match if($selectedVMTypes.Count -le 0){ Throw "$targetVMName failed to determine optimal size because your `$allowedVMTypes list does not contain any VM's that are available in this subscription and region" } #get meta data of targeted VM try{ $targetVM = Get-AzVM -Name $targetVMName -ErrorAction Stop if(!$targetVM){ Throw "$targetVMName does not exist in this subscription or you do not have permissions to access it" } $script:reportRow.currentSize = $targetVM.HardwareProfile.VmSize $targetVMPricing = $Null $targetVMPricing = $azureVMPrices | where{$_.name -eq $targetVM.HardwareProfile.VmSize} $targetVMCurrentHardware = $global:azureAvailableVMSizes | where{$_.Name -eq $targetVM.HardwareProfile.VmSize} if(!$targetVMCurrentHardware){ Throw "Current VM type $($targetVM.HardwareProfile.VmSize) could not be found in Azure's Available VM list, please resize manually to a currently supported size before using this function or wait until it becomes available again (this is sometimes transitive while Msft scales to customer demand)" } Write-Verbose "$targetVMName currently runs on $($targetVMCurrentHardware.NumberOfCores) vCPU's and $($targetVMCurrentHardware.MemoryInMB)MB memory ($($targetVM.HardwareProfile.VmSize))" }catch{ Throw "$targetVMName failed to get VM metadata from Azure because of $_" } #check for the LCRightSizeConfig tag if($targetVM.Tags["LCRightSizeConfig"]){ Write-Verbose "$targetVMName has right sizing tag with value $($targetVM.Tags["LCRightSizeConfig"])" if($targetVM.Tags["LCRightSizeConfig"] -eq "disabled"){ Throw "$targetVMName right sizing disabled through Azure Tag" }else{ $script:reportRow.targetSize = $targetVM.Tags["LCRightSizeConfig"] return $targetVM.Tags["LCRightSizeConfig"] } } #get memory performance of targeted VM in configured period try{ $query = "Perf | where TimeGenerated between (ago($($measurePeriodHours)h) .. ago(0h)) and CounterName =~ 'Available Mbytes' and Computer =~ '$($targetVMName)$($domain)'$queryAddition | project TimeGenerated, CounterValue | order by CounterValue" Write-Verbose "$targetVMName querying log analytics: $query" $result = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $query -ErrorAction Stop $resultsArray = [System.Linq.Enumerable]::ToArray($result.Results) Write-Verbose "$targetVMName retrieved $($resultsArray.Count) MB (LA type counter) memory datapoints from Azure Monitor" if($resultsArray.Count -le 0){ Write-Verbose "No data returned by Log Analytics for LA type counter, checking for AM type counter" $query = "Perf | where TimeGenerated between (ago($($measurePeriodHours)h) .. ago(0h)) and CounterName =~ 'Available Bytes' and Computer =~ '$($targetVMName)$($domain)'$queryAddition | project TimeGenerated, CounterValue | order by CounterValue" Write-Verbose "$targetVMName querying azure monitor: $query" $result = $Null; $result = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $query -ErrorAction Stop $resultsArray = $Null; $resultsArray = [System.Linq.Enumerable]::ToArray($result.Results) if($resultsArray.Count -le 0){ Write-Verbose "No data returned by Log Analytics for AM type counter" Throw "no data returned by Log Analytics. Was the VM turned on the past hours, and has the 'Available Mbytes' or 'Available Bytes' counter been turned on, and do you have permissions to query Log Analytics?" }else{ $resultsArray = $resultsArray | % {[PSCustomObject]@{"TimeGenerated" = $_.TimeGenerated;"CounterValue"=$_.CounterValue/1MB}} } } #we need to ensure enough datapoints exist if($resultsArray.Count -le $measurePeriodHours*4){ if($defaultSize){ Write-Verbose "Insufficient performance data to right size, default size specified at $defaultSize" $script:reportRow.targetSize = $defaultSize return $defaultSize } Throw "too few MEM perf data points to reliably calculate optimal VM size" } $memoryStats = get-vmCounterStats -Data $resultsArray.CounterValue #memory is expressed in Free MB's, recalculate to used % so we can apply similar logic as with CPU's $memUsedPct = (($targetVMCurrentHardware.MemoryInMB-$memoryStats.Percentile5)/$targetVMCurrentHardware.MemoryInMB) if($memUsedPct -gt 100 -or $memUsedPct -lt 0 -or $memoryStats.Maximum -gt $targetVMCurrentHardware.MemoryInMB){ Throw "Unexpected (negative or too large) memory perf value detected, VM was probably already resized less than $measurePeriodHours hours ago" } }catch{ Throw "$targetVMName failed to get memory performance data from Azure Monitor because $_" } Write-Verbose "$targetVMName has $($targetVMCurrentHardware.MemoryInMB)MB and in the top 5% of the time it averages at $($targetVMCurrentHardware.MemoryInMB - $memoryStats.Percentile5)MB ($([Math]::Round($memUsedPct*100,2))%) used" #get cpu performance of targeted VM in configured period try{ $query = "Perf | where TimeGenerated between (ago($($measurePeriodHours)h) .. ago(0h)) and CounterName =~ '% Processor Time' and Computer =~ '$($targetVMName)$($domain)'$queryAddition | project TimeGenerated, CounterValue | order by CounterValue" $result = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $query -ErrorAction Stop $resultsArray = [System.Linq.Enumerable]::ToArray($result.Results) Write-Verbose "$targetVMName retrieved $($resultsArray.Count) cpu datapoints from Azure Monitor" if($resultsArray.Count -le 0){ Write-Verbose "No data returned by Log Analytics" Throw "no data returned by Log Analytics. Was the VM turned on the past hours, and has the '% Processor Time' counter been turned on, and do you have permissions to query Log Analytics?" } #we need to ensure enough datapoints exist if($resultsArray.Count -le $measurePeriodHours*4){ if($defaultSize){ Write-Verbose "Insufficient performance data to right size, default size specified at $defaultSize" $script:reportRow.targetSize = $defaultSize return $defaultSize } Throw "too few CPU perf data points to reliably calculate optimal VM size" } $cpuStats = get-vmCounterStats -Data $resultsArray.CounterValue $cpuUsedPct = $cpuStats.Percentile95/100 if($cpuUsedPct -lt 0){ Throw "Negative value detected, VM was probably already resized less than $measurePeriodHours hours ago" } }catch{ Throw "$targetVMName failed to get CPU performnace data from Azure Monitor because $_" } Write-Verbose "$targetVMName has $($targetVMCurrentHardware.NumberOfCores) cpu cores and in the top 5% of the time it averages at $([Math]::Round($cpuStats.Percentile95,2))% max of the cores" $targetMinimumCPUCount=$targetVMCurrentHardware.NumberOfCores $targetMinimumMemoryInMB=$targetVMCurrentHardware.MemoryInMB #determine if CPU needs to be increased if($cpuUsedPct -gt $cul){ $targetMinimumCPUCount = [Math]::Min($maxvCPUs,[Math]::Max($minvCPUs,[Math]::Ceiling($targetVMCurrentHardware.NumberOfCores*($cpuUsedPct/$cll)))) } #determine if CPU needs to be decreased if($cpuUsedPct -lt $cll){ $targetMinimumCPUCount = [Math]::Max($minvCPUs,[Math]::Min($maxvCPUs,[Math]::Ceiling($targetVMCurrentHardware.NumberOfCores*($cpuUsedPct/$cul)))) } #determine if Memory needs to be increased if($memUsedPct -gt $mul){ $targetMinimumMemoryInMB = [Math]::Min($maxMemoryGB*1024,[Math]::Max($minMemoryGB*1024,[Math]::Ceiling($targetVMCurrentHardware.MemoryInMB*($memUsedPct/$mll)))) } #determine if memory needs to be decreased if($memUsedPct -lt $mll){ $targetMinimumMemoryInMB = [Math]::Max($minMemoryGB*1024,[Math]::Min($maxMemoryGB*1024,[Math]::Ceiling($targetVMCurrentHardware.MemoryInMB*($memUsedPct/$mul)))) } Write-Verbose "$targetVMName should have at least $targetMinimumCPUCount vCPU's and $targetMinimumMemoryInMB MB memory" $desiredVMType = $Null for($i=0;$i -lt $selectedVMTypes.Count;$i++){ if($selectedVMTypes[$i].NumberOfCores -ge $targetMinimumCPUCount -and $selectedVMTypes[$i].NumberOfCores -le $maxvCPUs -and $selectedVMTypes[$i].MemoryInMB -le $maxMemoryGB*1024 -and $selectedVMTypes[$i].MemoryInMB -ge $targetMinimumMemoryInMB){ $desiredVMType = $selectedVMTypes[$i] $script:reportRow.targetSize = $desiredVMType.Name break } } if($targetVMPricing -and $desiredVMType){ $costFactor = ($targetVMPricing.windowsPrice-$desiredVMType.windowsPrice)/$targetVMPricing.windowsPrice } if($desiredVMType){ if($desiredVMType.Name -eq $targetVM.HardwareProfile.VmSize){ Write-Verbose "$targetVMName is already sized correctly at $($targetVM.HardwareProfile.VmSize)" return $targetVM.HardwareProfile.VmSize }else{ if($costFactor){ if($costFactor -gt 0){ Write-Verbose "$targetVMName financial impact: $([Math]::Round($costFactor*100,2))% cost reduction" }else{ Write-Verbose "$targetVMName financial impact: $([Math]::Round($costFactor*100*-1,2))% cost increase" } $script:reportRow.costImpactPercent = $costFactor*-100 } Write-Verbose "$targetVMName should be resized from $($targetVM.HardwareProfile.VmSize) to $($desiredVMType.Name)" Write-Verbose "$targetVMName $($desiredVMType.Name) has $($desiredVMType.NumberOfCores) vCPU's and $($desiredVMType.MemoryInMB)MB Memory" return $desiredVMType.Name } }else{ Throw "$targetVMName failed to find a VM with at least $($desiredVMType.NumberOfCores) vCPU's and $($desiredVMType.MemoryInMB)MB Memory in your `$allowedVMTypes list" } } function get-vmCounterStats{ [cmdletbinding()] Param( [Parameter(Mandatory)]$Data ) $Data = [System.Collections.ArrayList][Double[]]$resultsArray.CounterValue | Sort-Object $Stats = $Data | Microsoft.PowerShell.Utility\Measure-Object -Minimum -Maximum -Sum -Average if ($Data.Count % 2 -eq 0) { $MedianIndex = ($Data.Count / 2) - 1 $LowerMedian = $Data[$MedianIndex] $UpperMedian = $Data[$MedianIndex - 1] $Median = ($LowerMedian + $UpperMedian) / 2 } else { $MedianIndex = [math]::Ceiling(($Data.Count - 1) / 2) $Median = $Data[$MedianIndex] } Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Median' -Value $Median -Force $Variance = 0 foreach ($_ in $Data) { $Variance += [math]::Pow($_ - $Stats.Average, 2) / $Stats.Count } $Variance /= $Stats.Count Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Variance' -Value $Variance -Force $StandardDeviation = [math]::Sqrt($Stats.Variance) Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'StandardDeviation' -Value $StandardDeviation -Force $Percentile1Index = [math]::Ceiling(1 / 100 * $Data.Count) $Percentile5Index = [math]::Ceiling(5 / 100 * $Data.Count) $Percentile10Index = [math]::Ceiling(10 / 100 * $Data.Count) $Percentile25Index = [math]::Ceiling(25 / 100 * $Data.Count) $Percentile75Index = [math]::Ceiling(75 / 100 * $Data.Count) $Percentile90Index = [math]::Ceiling(90 / 100 * $Data.Count) $Percentile95Index = [math]::Ceiling(95 / 100 * $Data.Count) $Percentile99Index = [math]::Ceiling(99 / 100 * $Data.Count) Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile1' -Value $Data[$Percentile1Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile5' -Value $Data[$Percentile5Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile10' -Value $Data[$Percentile10Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile25' -Value $Data[$Percentile25Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile75' -Value $Data[$Percentile75Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile90' -Value $Data[$Percentile90Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile95' -Value $Data[$Percentile95Index] -Force Add-Member -InputObject $Stats -MemberType NoteProperty -Name 'Percentile99' -Value $Data[$Percentile99Index] -Force Return $Stats } function set-rsgRightSize{ <# .SYNOPSIS Targets all VM's in a given resource group for right sizing. Use -Force to also resize VM's that are running, and -WhatIf with -Verbose to see what would happen without actually resizing Use -Report to output a full report in csv format .EXAMPLE set-rsgRightSize -targetRSG rg-avd-we-01 -domain company.local -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -Force set-rsgRightSize -targetRSG rg-avd-we-01 -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -WhatIf set-rsgRightSize -targetRSG rg-avd-we-01 -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -allowedVMTypes @("Standard_D2ds_v4","Standard_D4ds_v4","Standard_D8ds_v4") #> [cmdletbinding()] Param( [Parameter(Mandatory)][String]$targetRSG, [Parameter(Mandatory)][Guid]$workspaceId, #workspace GUID where perf data is stored (use Get-AzOperationalInsightsWorkspace to find this) $domain, #if your machines are domain joined, enter the domain name here [Int]$maintenanceWindowStartHour, #start hour of maintenancewindow in military time UTC (0-23) [Int]$maintenanceWindowLengthInHours, #length of maintenance window in hours (round up if needed) [ValidateSet(0,1,2,3,4,5,6)][Int]$maintenanceWindowDay, #day on which the maintenance window starts (UTC) where 0 = Sunday and 6 = Saturday [String]$region = "westeurope", #you can find yours using Get-AzLocation | select Location [Int]$measurePeriodHours = 152, #lookback period for a VM's performance while it was online, this is used to calculate the optimum. It is not recommended to size multiple times in this period! [Switch]$Force, #shuts a VM down to resize it if it detects the VM is still running when you run this command [Switch]$Boot, #after resizing, by default a VM stays offline. Use -Boot to automatically start if after resizing [Switch]$WhatIf, #best used together with -Verbose. Causes the script not to modify anything, just to log what it would do [Switch]$Report, [Array]$allowedVMTypes = @("Standard_D2ds_v4","Standard_D4ds_v4","Standard_D8ds_v4","Standard_D2ds_v5","Standard_D4ds_v5","Standard_D8ds_v5","Standard_E2ds_v4","Standard_E4ds_v4","Standard_E8ds_v4","Standard_E2ds_v5","Standard_E4ds_v5","Standard_E8ds_v5") ) Write-Verbose "Getting VM's for RSG $targetRSG" $targetVMs = Get-AzVM -ResourceGroupName $targetRSG -ErrorAction Stop $reportRows = @() foreach($vm in $targetVMs){ Write-Verbose "calling set-vmRightSize for $($vm.Name)" $retVal = set-vmRightSize -allowedVMTypes $allowedVMTypes -targetVMName $vm.Name -domain $domain -workspaceId $workspaceId -maintenanceWindowStartHour $maintenanceWindowStartHour -maintenanceWindowLengthInHours $maintenanceWindowLengthInHours -maintenanceWindowDay $maintenanceWindowDay -region $region -measurePeriodHours $measurePeriodHours -Report:$Report.IsPresent -Force:$Force.IsPresent -Boot:$Boot.IsPresent -WhatIf:$WhatIf.IsPresent if($Report){ $reportRows += $retVal }else{ $retVal } } if($Report){ $reportPath = Join-Path $Env:TEMP -ChildPath "addrs-report.csv" Write-Output "Writing report with $($reportRows.Count) lines to $reportPath" $reportRows | Export-CSV -Path $reportPath -Force -Encoding UTF8 -NoTypeInformation -Confirm:$False Start-Process $reportPath Write-Output "Report written and launched, script has completed" } } function set-vmRightSize{ <# .SYNOPSIS Targets a single VM for right sizing. Use set-rsgRightSize if you wish to resize all VM's in a resource group Use -Force to also resize VM's that are running, and -WhatIf with -Verbose to see what would happen without actually resizing Use -Report to output a report object .EXAMPLE set-vmRightSize -targetVMName azvm01 -domain company.local -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -Force set-vmRightSize -targetVMName azvm01 -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -WhatIf set-vmRightSize -targetVMName azvm01 -workspaceId e32b3dbe-2850-4f88-9acb-2b919cce4126 -allowedVMTypes @("Standard_D2ds_v4","Standard_D4ds_v4","Standard_D8ds_v4") #> [cmdletbinding()] Param( [Parameter(Mandatory)][String]$targetVMName, [Parameter(Mandatory)][Guid]$workspaceId, #workspace GUID where perf data is stored (use Get-AzOperationalInsightsWorkspace to find this) $domain, #if your machines are domain joined, enter the domain name here [Int]$maintenanceWindowStartHour, #start hour of maintenancewindow in military time UTC (0-23) [Int]$maintenanceWindowLengthInHours, #length of maintenance window in hours (round up if needed) [ValidateSet(0,1,2,3,4,5,6)][Int]$maintenanceWindowDay, #day on which the maintenance window starts (UTC) where 0 = Sunday and 6 = Saturday [String]$region = "westeurope", #you can find yours using Get-AzLocation | select Location [Int]$measurePeriodHours = 152, #lookback period for a VM's performance while it was online, this is used to calculate the optimum. It is not recommended to size multiple times in this period! [Switch]$Force, #shuts a VM down to resize it if it detects the VM is still running when you run this command [Switch]$Boot, #after resizing, by default a VM stays offline. Use -Boot to automatically start if after resizing [Switch]$WhatIf, #best used together with -Verbose. Causes the script not to modify anything, just to log what it would do [Switch]$Report, [Array]$allowedVMTypes = @("Standard_D2ds_v4","Standard_D4ds_v4","Standard_D8ds_v4","Standard_D2ds_v5","Standard_D4ds_v5","Standard_D8ds_v5","Standard_E2ds_v4","Standard_E4ds_v4","Standard_E8ds_v4","Standard_E2ds_v5","Standard_E4ds_v5","Standard_E8ds_v5") ) try{ Write-Verbose "$targetVMName getting metadata" $vm = Get-AzVM -Name $targetVMName -Status Write-Verbose "$targetVMName calculating optimal size" $optimalSize = get-vmRightSize -allowedVMTypes $allowedVMTypes -targetVMName $targetVMName -workspaceId $workspaceId -maintenanceWindowStartHour $maintenanceWindowStartHour -maintenanceWindowLengthInHours $maintenanceWindowLengthInHours -maintenanceWindowDay $maintenanceWindowDay -region $region -measurePeriodHours $measurePeriodHours -domain $domain if($optimalSize -eq $vm.HardwareProfile.VmSize){ Write-Host "$targetVMName already at optimal size" if($Report){ return $script:reportRow }else{ return $False } }else{ Write-Host "$targetVMName resizing from $($vm.HardwareProfile.VmSize) to $optimalSize ..." $retVal = set-vmToSize -vm $vm -newSize $optimalSize -Force:$Force.IsPresent -Boot:$Boot.IsPresent -WhatIf:$WhatIf.IsPresent if($retVal -eq "OK"){ $script:reportRow.resized = $True } if($Report){ return $script:reportRow }else{ return $retVal } } }catch{ Write-Error $_ if($Report){ $script:reportRow.reason = $_ return $script:reportRow }else{ return $False } } } |