Automating Azure VM Start/Stop from Local Scripts

Introduction

Manually starting and stopping Azure VMs is time-consuming.
With Azure Automation you can use Runbooks and Webhooks to control VMs automatically, and trigger them from your local PC.


Step 1. Configure Automation Account and Runbook

1.1 Create Automation Account

  1. In Azure Portal, search Automation AccountsCreate.
  2. Fill in:
  • Resource Group: auto-rg
  • Name: vm-auto-control
  • Region: choose same as VM if possible

1.2 Create a Runbook

  • Go to the Automation Account → Runbooks → Create a runbook.
  • Name: Start-Stop-VM
  • Type: PowerShell
  • Runtime version : 7.2
  • Create – Edit in portal
  • Paste the script below:
param([object]$WebhookData)

$ErrorActionPreference = 'Stop'

# Parse JSON body
$body = if($WebhookData.RequestBody){$WebhookData.RequestBody}else{$WebhookData}
$p = $body | ConvertFrom-Json
$op = ($p.operation+"").ToLower().Trim()
if($op -notin @('start','stop')){throw "operation must be start|stop"}

$t = if($p.target){$p.target}elseif($p.targets){$p.targets[0]}else{throw "target required"}
$sub,$rg,$vm = $t.subscriptionId,$t.resourceGroup,$t.vmName
if(!$sub -or !$rg -or !$vm){throw "subscriptionId, resourceGroup, vmName required"}

Connect-AzAccount -Identity | Out-Null
Set-AzContext -Subscription $sub | Out-Null

if($op -eq 'start'){Start-AzVM -Name $vm -ResourceGroupName $rg -NoWait:$false | Out-Null}
if($op -eq 'stop'){Stop-AzVM  -Name $vm -ResourceGroupName $rg -Force -NoWait:$false | Out-Null}
  • Save → Publish the runbook.

1.3 Create a Webhook

  1. Open the Runbook → Webhook → Create new webhook.
  2. Copy the Webhook URL (keep it safe, it’s the trigger endpoint).
  3. Set expiry as required → Enable.

Step 2. Assign Permissions to the Runbook Identity

Runbook runs under the Automation Account’s Managed Identity.
We need to grant Virtual Machine Contributor role to this identity.

  • Scope: Single VM
    1. Go to the VM → Access control (IAM).
    2. Add role assignment:
      • Role = Virtual Machine Contributor
      • Assign access to = Managed identity
      • Select your Automation Account
  • Scope: Subscription (all VMs)
    1. Go to Subscriptions → Access control (IAM).
    2. Add role assignment with the same settings.

Step 3. Manage VM Information with vms.json

For better flexibility, store VM metadata in a local JSON file (vms.json).
This allows you to manage multiple VMs (Windows, Linux, servers, clients) in one place, without hardcoding values in scripts.

vms.json Example

{
    "vms": [
        {
            "vmName": "Windows-01",
            "deviceType": "Windows Client",
            "subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "resourceGroup": "VSRG",
            "webhookUrl": "https://prod-00.westus.logic.azure.com/.../webhook"
        },
        {
            "vmName": "Linux-01",
            "deviceType": "Linux Server",
            "subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "resourceGroup": "DevRG",
            "PublicIP": "",
            "webhookUrl": "https://prod-00.westus.logic.azure.com/.../webhook"
        }
    ]
}

Step 4. Build Local Trigger Scripts

Now we can call the Runbook via webhook.
We’ll create a PowerShell script and a .bat wrapper for convenience.

4.1 PowerShell Script (vm_webhook.ps1)

param([string]$Config="vms.json")
$ErrorActionPreference="Stop"

if(-not(Test-Path $Config)){Write-Host "Config not found: $Config";exit 1}
try{$cfg=(Get-Content -LiteralPath $Config -Raw -Encoding UTF8)|ConvertFrom-Json}catch{Write-Host "Bad JSON: $_";exit 1}
$vms = if($cfg.vms){$cfg.vms}else{$cfg}; if(-not $vms -or $vms.Count -eq 0){Write-Host "No VMs in config.";exit 1}

while($true){
  Write-Host "`n==== VM List ===="
  for($i=0;$i -lt $vms.Count;$i++){
    $vm=$vms[$i]
    $line="[{0}] {1}" -f ($i+1),$vm.vmName
    if($vm.publicIp){$line+="  (PublicIP: {0})" -f $vm.publicIp}
    Write-Host $line
  }
  Write-Host "[0] Exit"
  $n=Read-Host "Pick VM number"
  if($n -eq "0" -or $n -eq "exit"){break}
  if($n -notmatch '^\d+$' -or [int]$n -lt 1 -or [int]$n -gt $vms.Count){Write-Host "Invalid VM.";continue}
  $dev=$vms[[int]$n-1]

  Write-Host "Operation: [1] start (default), [2] stop"
  $opSel=Read-Host "Pick op number"
  $op = if($opSel -eq "2"){"stop"}else{"start"}

  if(-not $dev.webhookUrl){Write-Host "Missing webhookUrl on VM.";continue}

  $payload=@{
    operation=$op
    targets=@(@{subscriptionId=$dev.subscriptionId;resourceGroup=$dev.resourceGroup;vmName=$dev.vmName})
  }

  try{
    Invoke-RestMethod -Method POST -Uri $dev.webhookUrl -Body ($payload|ConvertTo-Json -Compress) -ContentType "application/json" -TimeoutSec 10 | Out-Null
    $ipNote=if($dev.publicIp){" ($($dev.publicIp))"}else{""}
    Write-Host ("OK: {0} -> {1}{2}" -f $op,$dev.vmName,$ipNote)
  }catch{
    Write-Host ("FAIL: {0} -> {1} : {2}" -f $op,$dev.vmName,$_.Exception.Message)
  }
}

4.2 Batch Wrapper (run.bat)

@echo off
setlocal
set PS=powershell -ExecutionPolicy Bypass -NoProfile
%PS% -File "%~dp0vm_webhook.ps1" -Config "%~dp0vms.json"
endlocal
  • Double-click run.bat.
  • Select an option → script will send webhook request.
  • Runbook executes in Azure → VM starts/stops accordingly.

Leave a Reply

Your email address will not be published. Required fields are marked *