Use PowerShell script to monitor and start a deallocated Azure VM
Jan 25, 2021

My Azure VM sometimes gets deallocated for some reason, so I created a PowerShell script and configure it as a scheduled task to monitor the VM status and start the VM once it is deallocated. The script uses a precreated Azure service principal to automatically authenticate Azure and Azure PowerShell cmdlets to detect the VM status, if the status is deallocated then it calls Start-AzVm to start the VM, the script execution result is recorded to a log file.

Install Azure PowerShell cmdlets

Azure PowerShell works with PowerShell 6.2.4 and later on all platforms. It is also supported with PowerShell 5.1 on Windows.

Install for Current User for PowerShell core:

Install-Module -Name Az -AllowClobber -Scope CurrentUser

Install for All Users for PowerShell core:

Install-Module -Name Az -AllowClobber -Scope AllUsers

Install for Windows built-in PowerShell 5.1:

Install-Module -Name PowerShellGet -Force

After installation, you can use command Connect-AzAccount to test whether the installation succeeds.

Create an Azure service principal

Connect-AzAccount provides interactive sign in experience by default which is not suitable for an automation script. We can create an Azure service principal and do non-interactive sign in using Connect-AzAccount -ServicePrincipal.

  1. First, run Connect-AzAccount to sign in your Azure account interactively.

  2. Connect-AzAccount connects to the default tenant of your Azure account, use Get-AzVm -Name to test whether it can get the target VM status. If not, you can use Set-AzContext -SubscriptionId to switch to the tenant of the subscription that contains the target VM.

  3. After that, use New-AzADServicePrincipal to create an Azure service principal, you can just specify a displayName parameter. Without any other authentication parameters, password-based authentication is used and a random password is created for you. You can use following commands to display the random password.

    $sp = New-AzADServicePrincipal -DisplayName "vm-powershell-script-svc-principal"
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sp.Secret)
    $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
    $UnsecureSecret
    

    • ApplicationId can be used as username to sign in Azure
    • UnsecureSecret can be used as password to sign in Azure
    • Id can be used as ObjectId parameter to delete this service principal using command: Remove-AzADServicePrincipal -ObjectId
  4. Sign out current Azure context using Disconnect-AzAccount and then use following commands to test whether the Azure service principal we just created can successfully sign in and get the target VM status:

    $password = ConvertTo-SecureString -String "cf0d6408-6e25-4685-b4b9-84548821e7d1" -AsPlainText -Force
    $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "970b5ce1-4dee-47f6-b317-9fb4c925e13c", $password
    Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant "6e30ec6d-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    Get-AzVM -Name "joji-ea"
    

Set username and password as environment variables

It is unsafe to save the username and password in your script, so let's set them as system environment variables.

PowerShell script content

The script logic is quite simple, it just checks whether the VM status is deallocated, if yes then it starts the VM using Start-AzVm, it also records the execution result to a log file.

function log($message) {
    $global:logContent += "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") $message`r`n"
}

$logFile = "c:\temp\vm.log"
$global:logContent = ""
if (!(Test-Path $logFile)) {
    New-Item $logFile -Force
}
else {
    $global:logContent = Get-Content $logFile -Raw
}
$password = ConvertTo-SecureString -String $Env:VM_SCRIPT_PWD -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:VM_SCRIPT_APPID, $password
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant "6e30ec6d-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -WarningAction SilentlyContinue

$vm = Get-AzVm -Name "joji-ea" -Status
log("VM PowerState: $($vm.PowerState)");

if ($vm.PowerState -match "deallocated") {
    $startvm = Start-AzVm -Name "joji-ea" -ResourceGroupName "linux-rg"
    log("Starting VM...`r`nOperationId: $($startvm.OperationId)`r`nStatus: $($startvm.Status)`r`nStartTime: $($startvm.StartTime.ToString())`r`nEndTime: $($startvm.EndTime.ToString())")
    $newVMstatus = Get-AzVm -Name "joji-ea" -Status
    log("Refreshing... VM PowerState: $($newVMstatus.PowerState)");
}
Set-Content $logFile $global:logContent.TrimEnd()

Configure as a scheduled task

Finally, let's create a scheduled task to run the PowerShell script repeatedly. Assuming the script is saved at "C:\temp\vm-monitor.ps1", let's open PowerShell first and run C:\temp\vm-monitor.ps1 to ensure the script is executable. The default ExecutionPolicy of PowerShell script on Windows is Restricted, running scripts is disabled if you did not set the ExecutionPolicy before. You need to run PowerShell as administrator and run command Set-ExecutionPolicy -ExecutionPolicy RemoteSigned to allow running scripts.

Set the task trigger as: At startup and repeat task every 1 minute indefinitely.

The task command is: pwsh -File c:\temp\vm-monitor.ps1, if you want to use Windows built-in PowerShell, you can change to powershell -File c:\temp\vm-monitor.ps1.

Some other task settings like: Stop the task if it runs longer than an hour, run a new instance in parallel if previous task is still running.

References