Start\stop Azure resources based on tags using Azure Resource Graph
Howdy,
an update to a previous post I did, which significantly improves performance by using Azure Resource Graph:
function compare-time( $actual ) {
$result = if ( $actual -match ',' ) {
$actual.Split(',').Trim().foreach{ parse-time $_ }
}
else {
parse-time $actual
}
if ( $result ) { $true }
}
function parse-time( $inputillo ) {
try {
$inputillo = (Get-Date $inputillo).DayOfYear
}
catch {
"Not Datetime" | Out-Null
}
if ( $inputillo -in $global:timeDate.week, $global:timeDate.day, $global:timeDate.year ) {
$true
}
if ( $inputillo -eq 'start') {
$global:entityStart = $false
}
if ( $inputillo -eq 'stop') {
$global:entityStop = $false
}
}
function parse-entity( $inputillo ) {
try {
$inputillo | ConvertTo-Json -Compress
$global:entityStop = $global:entityStart = $true
$schedule = $inputillo.schedule | ConvertFrom-Json -ErrorAction Stop
if ( compare-time $schedule.ignore ) {
"SKIP: {0} [ {1} ]" -f $inputillo.Id, $schedule.ignore
continue
}
[int]$start, [int]$stop = $schedule.($global:timeDate.week).Split("-")
if ( ( $start - $stop ) -eq 0 -or ( $start - $stop ) -ge 20 ) {
$global:messages += "Error: Malformed Start\Stop at {0}" -f $inputillo.Id
continue
}
if ( $global:timeDate.hour -eq ( $stop - $schedule.UTC ) -and $global:entityStop ) {
$action = $mapper[$inputillo.Type].action
}
elseif ( $global:timeDate.hour -eq ( $start - $schedule.UTC ) -and $global:entityStart ) {
$action = "start"
}
if ( [System.String]::IsNullOrEmpty( $action ) ) {
"NOOP: {0}" -f $inputillo.Id
}
else {
$global:processed += New-Object -TypeName psobject -Property @{
action = $action
apiVer = $mapper[$inputillo.Type].apiver
delay = if ( $schedule.delayed ) { $true } else { $false }
id = $inputillo.id
}
}
}
catch {
$global:messages += "Error: Malformed JSON at {0}, proceeding with the next one" -f $inputillo.Id
}
}
function process-entity( $inputillo, $header ) {
try {
"Performing action {0} on {1}" -f $inputillo.action, $inputillo.Id
$uri = "https://management.azure.com{0}/{1}?api-version={2}" -f $inputillo.Id, $inputillo.action, $inputillo.apiVer
$global:requests += Invoke-AzRest -Method Post -Uri $uri | Add-Member @{ id = $inputillo.Id } -PassThru
}
catch {
Write-Error ( $PSItem | Out-String )
$global:messages += "Error: start\stop operation failed at {0}" -f $inputillo.id
$retries += $inputillo
}
}
# Initialize variables
$processed = $requests = $messages = $retries = @(); [Int]$maxRetry = 5; [Int]$retry = 0
$dateNow = ( Get-Date ).ToUniversalTime()
$timeDate = @{
hour = $dateNow.Hour
day = $dateNow.DayOfWeek
week = if ( $dateNow.DayOfWeek -in "Sunday", "Saturday" ) { "weekends" } else { "weekdays" }
year = $dateNow.DayOfYear
}
$mapper = @{
'Microsoft.Network/applicationGateways' = @{
action = 'stop'
apiver = '2017-04-01'
}
'Microsoft.Compute/virtualMachines' = @{
action = 'deallocate'
apiver = '2017-03-30'
}
'Microsoft.Compute/virtualMachineScaleSets' = @{
action = 'deallocate'
apiver = '2017-03-30'
}
'Microsoft.ClassicCompute/virtualMachines' = @{
action = 'shutdown'
apiver = '2017-04-01'
}
'Microsoft.ContainerService/managedClusters' = @{
action = 'stop'
apiver = '2022-04-01'
}
}
do {
$Error.Clear()
$retry++
Connect-AzAccount -Identity
$all = @()
$all += Search-AzGraph -Query "resources | where tags contains 'schedule'" | ForEach-Object {
$PSItem | ForEach-Object {
[pscustomobject]@{
Type = $_.type
Id = $_.id
Schedule = $_.Tags.schedule
}
}
}
$all += Search-AzGraph -Query "ResourceContainers | where type =~ 'microsoft.resources/subscriptions/resourcegroups' | where tags contains 'classicschedule'" | ForEach-Object {
$tag = $PSItem.tags.classicSchedule
$name = $PSItem.name.ToLower()
Search-AzGraph -Query "resources | where resourceGroup == '$name' and type == 'microsoft.classiccompute/virtualmachines'" | ForEach-Object {
$PSItem | ForEach-Object {
[pscustomobject]@{
Type = $_.type
Id = $_.id
Schedule = $tag
}
}
}
}
} while ( $Error.Count -gt 0 -and $maxRetry -ge $retry )
if ( $Error ) {
"FATAL: runbook failed"
$Error
exit 1
}
try {
# .foreach{} method is not a loop => 'continue' skips everything => foreach() is required
$all = $all.where{ $_.Type -in $mapper.keys }
foreach ( $entity in $all ) { parse-entity $entity }
$processed.where{ $_.Id -notmatch "virtualMachineScaleSets" -or $_.Action -ne "start" }.foreach{ process-entity $PSItem }
Start-Sleep -Seconds 600 # start scalesets after databases\application gateways are up
$processed.where{ $_.Id -match "virtualMachineScaleSets" -and $_.Action -eq "start" }.foreach{ process-entity $PSItem }
}
catch {
# Possibly restart runbook and\or determine state and restart failed
"bad place to be"
Write-Error ( $PSItem | Out-String )
}
$retries.foreach{ process-entity $PSItem $header }
if ( $messages ) {
"Error messages:"
$messages
"examine start\stop job state"
$Error
exit 1
}
All the other moving parts are the same as in the previous post. Except maybe permissions need adjusting to allow ARG queries.
Cheerios
Written on October 13, 2022