Запуск и остановка Azure серверов по расписанию v2

21.05.2020

Эта заметка описывает модификацию решения Запуск и остановка Azure серверов по расписанию от апреля 2019 года.

В процессе работы с инфраструктурой Azure обнаружился не очень приятный факт, что команды на включение виртуальных машин могут не исполняться, в результате чего сервер остается в выключенном состоянии. Например, на картинке ниже видно, как несколько раз подавались команды на запуск сервера и от скрипта, и непосредственно от пользователя через портал Azure, но облако выдавало ошибку "Allocation failed. If you are trying to add a new VM to an Availability Set or update/resize an existing VM in an Availability Set, please note that such Availability Set allocation is scoped to a single cluster, and it is possible that the cluster is out of capacity..." А бот из центра поддержки центра отвечал: "The hardware cluster where the VM is currently deployed did not have enough capacity of this size to support your allocation request." Инженер из центра поддержки Microsoft подтвердил проблему в Дата Центре Azure и посоветовал модифицировать сценарий запуска сервера - проверять через некоторое время, запустился ли сервер и присылать отчет администратору.

Servers - Start-stop Azure instances by a schedule v2 - 01

Это сподвигло меня проверить скрипты, используемые в решении, дополнить их несколькими улучшениями, а так же написать еще один скрипт, который бы проверял состояние серверов и отправлял письма об ошибках запуска виртуальных машин администраторам системы, что им необходимо подключиться к порталу Azure и проконтролировать процесс.

Основные отличия в обновленном сценарии - это

  • добавление переменной $Location, чтобы назначать регион в котором отрабатывать скрипт,
  • добавление штампа даты и времени к событиям в логе скрипта - команда Get-Date -Format "HH:mm:ss" в каждой строке вывода, что помогает в расследовании инцидентов с запуском виртуальных машин,
  • обработка машин только из назначенного региона, то есть серверов с одними и теми же настройками часового пояса (это сделано, чтоб избежать ошибок вычисления времени запуска серверов в разных регионах) - опция -Location $Location в команде выборки виртуальных машин,

В остальном же сценарий остался без изменений.

Ниже представлены сами модифицированные скрипты, а в заметке Проверка запуска Azure серверов и уведомление администраторов рассказывается о контролирующем запуск скрипте.

run-StartSchedule - скрипт запуска серверов
$connectionName = "AzureRunAsConnection"
$TagsTimeZone = "Central Europe Standard Time"
$TagsTimeZoneAbbreviation = "CET"
$Location = "westeurope"

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "----- The script started -----")
Try {
    # Get the connection "AzureRunAsConnection "
    $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Logging in to Azure...")
    Login-AzureRmAccount `
        -ServicePrincipal `
        -TenantId $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint | Out-Null
}
catch {
    if (!$servicePrincipalConnection) {
        $ErrorMessage = "Connection $connectionName not found"
        throw $ErrorMessage
    } else {
        Write-Error -Message $_.Exception
        throw $_.Exception
    }
}

# Making time filter
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "System time zone = " + ([TimeZoneInfo]::Local).Id)
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Current system time = " + (Get-Date -Format "dd.MM.yyyy HH:mm, ddd") )
$TagsTime = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm, ddd")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Tags time zone = $($TagsTimeZone)")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Tags time = $($TagsTime)")

$START_TIME = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('H:00')
$START_DAY = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('ddd')
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Adapting time to search for ""$($START_TIME)"" in tags")

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Looking for instances where ""Schedule Start"" tag = ""$($START_TIME)"" ...")

[array]$VMs = Get-AzureRMVm -Location $Location | Where-Object {$PSItem.Tags["Schedule Start"] -eq $START_TIME}

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VMs.Count) instances found")

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Processing the instances...")
ForEach ($VM in $VMs) {
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VM.Name) instance in $($VM.ResourceGroupName) resource group:")
    $VMTags = $VM.Tags
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Checking the ""Schedule Days"" tag ...")
    If ( -not($VMTags.Keys -contains "Schedule Days") -or $VMTags["Schedule Days"].Split(',').Trim() -contains $START_DAY ) {
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is allowed to be processed today" )
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Checking the instance status ...")
        $VMStatus = (Get-AzureRMVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
        If ($VMStatus -eq "VM deallocated") {
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is in ""$($VMStatus)"" state")
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Starting the instance")
            Start-AzureRMVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -AsJob | Out-Null

            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Updating COMMENT tag for the instance")
            $VMTagCommentText = ("Started by Start-Stop Scheduler at " + `
              ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm") + `
              " $($TagsTimeZoneAbbreviation)")

            If (-not($VMTags.ContainsKey("Comment"))) {
                $VMTags.Add("Comment", $VMTagCommentText)
            } Else {
                $VMTags["Comment"] = $VMTagCommentText
            }
            Set-AzureRMResource -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -ResourceType "Microsoft.Compute/VirtualMachines" `
            -Tag $VMTags -Force -AsJob | Out-Null
        } Else {
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is in ""$($VMStatus)"" state")
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  No action needed")
        }
    } Else {
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is not allowed to be processed today" )
    }
}
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "----- The script stopped -----")
Write-Output (Get-Job)
run-StopSchedule - скрипт отсановки серверов
$connectionName = "AzureRunAsConnection"
$TagsTimeZone = "Central Europe Standard Time"
$TagsTimeZoneAbbreviation = "CET"
$Location = "westeurope"

Write-Output ("----- The script started -----")
Try {
    # Get the connection "AzureRunAsConnection "
    $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Logging in to Azure...")
    Login-AzureRmAccount `
        -ServicePrincipal `
        -TenantId $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint | Out-Null
}
catch {
    if (!$servicePrincipalConnection) {
        $ErrorMessage = "Connection $connectionName not found"
        throw $ErrorMessage
    } else {
        Write-Error -Message $_.Exception
        throw $_.Exception
    }
}

# Making time filter
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "System time zone = " + ([TimeZoneInfo]::Local).Id)
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Current system time = " + (Get-Date -Format "dd.MM.yyyy HH:mm, ddd") )
$TagsTime = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm, ddd")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Tags time zone = $($TagsTimeZone)")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Tags time = $($TagsTime)")

$STOP_TIME = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('H:00')
$STOP_DAY = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('ddd')
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Adapting time to search for ""$($STOP_TIME)"" in tags")

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Looking for instances where ""Schedule Stop"" tag = ""$($STOP_TIME)"" ...")

[array]$VMs = Get-AzureRMVm -Location $Location | Where-Object {$PSItem.Tags["Schedule Stop"] -eq $STOP_TIME}

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VMs.Count) instances found")

Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Processing the instances...")
ForEach ($VM in $VMs) {
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VM.Name) instance in $($VM.ResourceGroupName) resource group:")
    $VMTags = $VM.Tags
    Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Checking the ""Schedule Days"" tag ...")
    IF ( -not($VMTags.Keys -contains "Schedule Days") -or $VMTags["Schedule Days"].Split(',').Trim() -contains $STOP_DAY ) {
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is allowed to be processed today" )
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Checking the instance status ...")
        $VMStatus = (Get-AzureRMVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
        If ($VMStatus -eq "VM running") {
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is in ""$($VMStatus)"" state")
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Stopping the instance")
            Stop-AzureRMVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -Force -AsJob | Out-Null

            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  Updating COMMENT tag for the instance")
            $VMTagCommentText = ("Stopped by Start-Stop Scheduler at " + `
              ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm") + `
              " $($TagsTimeZoneAbbreviation)")
            
            If (-not($VMTags.ContainsKey("Comment"))) {
                $VMTags.Add("Comment", $VMTagCommentText)
            } Else {
                $VMTags["Comment"] = $VMTagCommentText
            }
            Set-AzureRMResource -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -ResourceType "Microsoft.Compute/VirtualMachines" `
            -Tag $VMTags -Force -AsJob | Out-Null
        } Else {
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is in ""$($VMStatus)"" state")
            Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  No action needed")
        }
    } Else {
        Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "  The instance is not allowed to be processed today")
    }
}
Write-Output ("----- The script stopped -----")