Skip to content

Instantly share code, notes, and snippets.

@matejskubic
Last active April 24, 2026 19:33
Show Gist options
  • Select an option

  • Save matejskubic/8ab5617dd7586fee4f0b8e25c537e8cd to your computer and use it in GitHub Desktop.

Select an option

Save matejskubic/8ab5617dd7586fee4f0b8e25c537e8cd to your computer and use it in GitHub Desktop.
Azuve VM Eviciton shutdown

Azure Spot VM Eviction Monitor

Overview

This directory contains scripts that detect an Azure Spot VM eviction and trigger a graceful forced OS shutdown before Azure terminates the machine.

File Purpose
Notify-AzureVMEviction.ps1 Production monitor - polls IMDS every 2 s; shuts down on eviction
Shutdown VM on eviction.xml Task Scheduler definition example for recurring monitor execution

How It Works

Azure Spot VMs receive a 30-second eviction notice via the Instance Metadata Service (IMDS) Scheduled Events API:

GET http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01
Headers: Metadata: true

When an eviction is imminent the API returns a Preempt event:

{
  "DocumentIncarnation": 2,
  "Events": [
    {
      "EventId": "...",
      "EventType": "Preempt",
      "ResourceType": "VirtualMachine",
      "EventStatus": "Scheduled",
      "NotBefore": "2026-04-15T12:00:00Z"
    }
  ]
}

Notify-AzureVMEviction.ps1 polls this endpoint every 2 seconds. On detecting a Preempt event it:

  1. Logs activity and event payloads to a text log file.
  2. Calls shutdown.exe /s /f /t 8 – forces all apps to close and shuts Windows down after 8 seconds.

Requirements

  • Windows Server (runs on the Azure VM itself)
  • PowerShell 5.1 or PowerShell 7+
  • The VM must be an Azure Spot (or Standard) VM – IMDS is available on all Azure VMs
  • The log directory (C:\Logs\ by default) is created automatically

Parameters – Notify-AzureVMEviction.ps1

Parameter Default Description
-MetadataEndpoint IMDS link-local URL Override for testing
-TranscriptOutputDirectory C:\Logs\ Directory for PowerShell transcript log files
-PollIntervalSeconds 2 IMDS poll frequency
-ShutdownTimeoutSeconds 8 Seconds before OS shutdown executes

Manual Usage

# Run directly (blocks until eviction or Ctrl+C)
.\Notify-AzureVMEviction.ps1

# Custom transcript output directory
.\Notify-AzureVMEviction.ps1 -TranscriptOutputDirectory 'D:\Logs\'

Register as a Windows Task Scheduler Task

The monitor should start automatically on the VM and run under SYSTEM.

Option A - Import the provided XML task

The included file Shutdown VM on eviction.xml is configured to:

  • Run every 5 minutes
  • Run as SYSTEM with highest privileges
  • Launch pwsh.exe
  • Execute C:\Logs\Notify-AzureVMEviction.ps1

Run once in an elevated PowerShell session:

$taskName = 'Shutdown VM on eviction'
$xmlPath = '.\Shutdown VM on eviction.xml'

# Ensure the script path referenced in XML exists, or edit XML first.
# Default in XML: C:\Logs\Notify-AzureVMEviction.ps1

Register-ScheduledTask -TaskName $taskName -Xml (Get-Content $xmlPath -Raw) -Force

If you prefer schtasks.exe:

schtasks /Create /TN "Shutdown VM on eviction" /XML "Shutdown VM on eviction.xml" /F

Option B - Create task with PowerShell cmdlets

Run once with Administrator privileges on the VM:

$scriptPath = 'C:\Scripts\Notify-AzureVMEviction.ps1'

# Copy script to a stable location first
Copy-Item .\Notify-AzureVMEviction.ps1 $scriptPath -Force

$action = New-ScheduledTaskAction `
    -Execute 'pwsh.exe' `
    -Argument "-NonInteractive -WindowStyle Hidden -File `"$scriptPath`""

# Trigger: run at system startup
$trigger = New-ScheduledTaskTrigger -AtStartup

# Run as SYSTEM, highest privileges
$principal = New-ScheduledTaskPrincipal `
    -UserId    'NT AUTHORITY\SYSTEM' `
    -LogonType ServiceAccount `
    -RunLevel  Highest

$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit      ([TimeSpan]::Zero) `   # no time limit
    -RestartCount            5 `
    -RestartInterval         ([TimeSpan]::FromMinutes(1)) `
    -StartWhenAvailable

Register-ScheduledTask `
    -TaskName  'AzureSpotVmEvictionMonitor' `
    -TaskPath  '\Azure\' `
    -Action    $action `
    -Trigger   $trigger `
    -Principal $principal `
    -Settings  $settings `
    -Force

Option C - Create task with schtasks.exe

schtasks /Create /F /RU "SYSTEM" /RL HIGHEST /SC ONSTART /DELAY 0000:30 ^
  /TN "\Azure\AzureSpotVmEvictionMonitor" ^
  /TR "pwsh.exe -NonInteractive -WindowStyle Hidden -File \"C:\Scripts\Notify-AzureVMEviction.ps1\""

Verify the task

Get-ScheduledTask -TaskName 'AzureSpotVmEvictionMonitor' | Select-Object TaskName, State

# Start manually to verify it runs
Start-ScheduledTask -TaskName '\Azure\AzureSpotVmEvictionMonitor'

# Check the transcript log (filename is generated by Start-Transcript)
Get-ChildItem 'C:\Logs\' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content -Tail 20 -Wait

Remove the task

Unregister-ScheduledTask -TaskName 'AzureSpotVmEvictionMonitor' -Confirm:$false
<#
.SYNOPSIS
Monitors the Azure Instance Metadata Service (IMDS) for Spot VM eviction events
and initiates a graceful forced shutdown.
.DESCRIPTION
Polls the Azure IMDS Scheduled Events endpoint every 2 seconds (configurable).
When a Preempt event (Spot VM eviction) is detected, logs the event and starts a forced
Windows shutdown with an 8-second countdown, allowing any registered shutdown hooks to run.
Must be run on the Azure VM itself. The IMDS endpoint (169.254.169.254) is only
reachable from within the VM.
.PARAMETER TranscriptOutputDirectory
Full path to the log file. Defaults to C:\Logs\
.PARAMETER PollIntervalSeconds
How often (in seconds) to poll the IMDS endpoint. Defaults to 2.
.PARAMETER ShutdownTimeoutSeconds
Seconds to wait before the forced shutdown executes after the eviction is detected.
Defaults to 8. Use this window to flush buffers, notify external systems, etc.
.EXAMPLE
# Normal operation on the VM
.\Notify-AzureVMEviction.ps1
#>
[CmdletBinding()]
param(
[int]
$PollIntervalSeconds = 2
,
[int]
$ShutdownTimeoutSeconds = 8
,
[string]
$TranscriptOutputDirectory = 'C:\Logs\'
,
[string]
$MetadataEndpoint = 'http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01'
)
Set-StrictMode -Version 1.0
$ErrorActionPreference = 'Continue'
$InformationPreference = 'Continue'
Start-Transcript -IncludeInvocationHeader -OutputDirectory $TranscriptOutputDirectory -Append -NoClobber -UseMinimalHeader
# warm up the IMDS endpoint with a single call to reduce latency on the first real query
Invoke-RestMethod `
-Uri $MetadataEndpoint `
-Headers @{ Metadata = 'true' } `
-Method GET `
-TimeoutSec 90 `
-ErrorAction Continue
while ($true) {
Start-Sleep -Seconds $PollIntervalSeconds
$payload = Invoke-RestMethod `
-Uri $MetadataEndpoint `
-Headers @{ Metadata = 'true' } `
-Method GET `
-TimeoutSec 3 `
-ErrorAction Continue
Write-Information "$([System.DateTimeOffset]::Now.ToString('o')): Events: $($payload.Events.Count)"
if (!$payload -or !$payload.Events) {
continue
}
foreach ($azEvent in $payload.Events) {
$azEvent | ConvertTo-Json -Depth 10
if ($azEvent.EventType -eq 'Preempt') {
Write-Information "EVICTION DETECTED - forced shutdown in $ShutdownTimeoutSeconds seconds."
# Issue the OS shutdown: /s = shutdown /f = force close apps /t = timeout
& shutdown.exe /s /f /t $ShutdownTimeoutSeconds
if ($LASTEXITCODE -ne 0) {
Write-Warning "shutdown.exe returned exit code $LASTEXITCODE"
}
}
}
}
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2026-04-15T21:37:50.7018327</Date>
<Author>ms-dev-vs2022-1\matejsk</Author>
<URI>\Shutdown VM on eviction</URI>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<Repetition>
<Interval>PT5M</Interval>
<Duration>P1D</Duration>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>2026-01-01T00:00:00</StartBoundary>
<ExecutionTimeLimit>P1D</ExecutionTimeLimit>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P1D</ExecutionTimeLimit>
<Priority>7</Priority>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>999</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"%ProgramFiles%\PowerShell\7\pwsh.exe"</Command>
<Arguments>-NonInteractive -NoProfile -NoLogo C:\Logs\Notify-AzureVMEviction.ps1</Arguments>
</Exec>
</Actions>
</Task>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment