Azure Automation — Runbooks, State Configuration, and Update Management
You are doing the same manual tasks every week. Stopping dev VMs on Friday evening. Checking for orphaned disks. Rotating storage account keys. Verifying that all servers have the right software installed. Patching 50 VMs during the maintenance window and praying nothing breaks. Azure Automation turns all of these repetitive operations into scheduled, auditable, self-service workflows — and it does it for about the cost of a coffee per month.
Azure Automation Account
An Automation account is the container for all your runbooks, schedules, credentials, variables, and configurations. Create one per region or per team depending on your operational model.
# Create an Automation account
az automation account create \
--name aa-operations-eastus \
--resource-group rg-automation \
--location eastus \
--sku Free
# List existing automation accounts
az automation account list \
--query "[].{Name:name, RG:resourceGroup, Location:location, SKU:sku.name}" \
--output table
The pricing tiers:
| Tier | Runbook Minutes/Month | DSC Nodes | Price |
|---|---|---|---|
| Free | 500 minutes | 5 nodes | Free |
| Basic | Unlimited | Not included | ~$0.002/minute |
The Free tier is generous enough for most small to mid-size operations. You only upgrade to Basic when you exceed 500 minutes of runbook execution per month or need more DSC-managed nodes.
PowerShell Runbooks
PowerShell runbooks are the bread and butter of Azure Automation. They run on Azure-hosted sandbox workers and have access to Az PowerShell modules out of the box.
# Runbook: Stop-DevVMs.ps1
# Stops all VMs in dev resource groups tagged with AutoShutdown=true
param(
[string]$SubscriptionId = "your-subscription-id"
)
# Authenticate using the Automation account's system-assigned managed identity
Connect-AzAccount -Identity
Set-AzContext -SubscriptionId $SubscriptionId
# Find all VMs tagged for auto-shutdown
$vms = Get-AzVM -Status | Where-Object {
$_.Tags["AutoShutdown"] -eq "true" -and
$_.PowerState -eq "VM running"
}
Write-Output "Found $($vms.Count) running VMs tagged for shutdown"
foreach ($vm in $vms) {
Write-Output "Stopping $($vm.Name) in $($vm.ResourceGroupName)..."
Stop-AzVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Force -NoWait
}
Write-Output "Shutdown commands sent for all tagged VMs"
# Import the runbook
az automation runbook create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name Stop-DevVMs \
--type PowerShell \
--location eastus
# Upload the script content
az automation runbook replace-content \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name Stop-DevVMs \
--content @Stop-DevVMs.ps1
# Publish the runbook (required before it can be scheduled or triggered)
az automation runbook publish \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name Stop-DevVMs
# Test run the runbook
az automation runbook start \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name Stop-DevVMs \
--parameters SubscriptionId="<sub-id>"
Another practical example — finding and reporting orphaned resources:
# Runbook: Find-OrphanedResources.ps1
# Finds unattached disks, public IPs, and NICs and sends a report
Connect-AzAccount -Identity
$report = @()
# Unattached managed disks
$orphanedDisks = Get-AzDisk | Where-Object { $_.ManagedBy -eq $null -and $_.DiskState -eq "Unattached" }
foreach ($disk in $orphanedDisks) {
$report += [PSCustomObject]@{
Type = "Disk"
Name = $disk.Name
ResourceGroup = $disk.ResourceGroupName
SizeGB = $disk.DiskSizeGB
MonthlyCost = [math]::Round($disk.DiskSizeGB * 0.05, 2)
}
}
# Unattached public IPs
$orphanedIPs = Get-AzPublicIpAddress | Where-Object { $_.IpConfiguration -eq $null }
foreach ($ip in $orphanedIPs) {
$report += [PSCustomObject]@{
Type = "PublicIP"
Name = $ip.Name
ResourceGroup = $ip.ResourceGroupName
SizeGB = "N/A"
MonthlyCost = if ($ip.Sku.Name -eq "Standard") { 3.65 } else { 0 }
}
}
# Unattached NICs
$orphanedNICs = Get-AzNetworkInterface | Where-Object { $_.VirtualMachine -eq $null }
foreach ($nic in $orphanedNICs) {
$report += [PSCustomObject]@{
Type = "NIC"
Name = $nic.Name
ResourceGroup = $nic.ResourceGroupName
SizeGB = "N/A"
MonthlyCost = 0
}
}
$totalWaste = ($report | Measure-Object -Property MonthlyCost -Sum).Sum
Write-Output "=== Orphaned Resources Report ==="
Write-Output ($report | Format-Table -AutoSize | Out-String)
Write-Output "Total estimated monthly waste: `$$totalWaste"
Python Runbooks
Azure Automation also supports Python 3 runbooks. Use these when your team prefers Python or when you need specific Python libraries.
# Runbook: cleanup_old_snapshots.py
# Deletes VM snapshots older than 30 days
import datetime
import automationassets
from azure.identity import ManagedIdentityCredential
from azure.mgmt.compute import ComputeManagementClient
credential = ManagedIdentityCredential()
subscription_id = automationassets.get_automation_variable("SubscriptionId")
compute_client = ComputeManagementClient(credential, subscription_id)
cutoff_date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)
deleted_count = 0
for snapshot in compute_client.snapshots.list():
if snapshot.time_created < cutoff_date:
print(f"Deleting snapshot: {snapshot.name} (created {snapshot.time_created.date()})")
compute_client.snapshots.begin_delete(
resource_group_name=snapshot.id.split("/")[4],
snapshot_name=snapshot.name
)
deleted_count += 1
print(f"Deleted {deleted_count} snapshots older than 30 days")
# Create a Python runbook
az automation runbook create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name Cleanup-OldSnapshots \
--type Python3 \
--location eastus
Graphical Runbooks
Graphical runbooks provide a visual drag-and-drop canvas for building workflows. They are based on PowerShell Workflow but use a visual editor in the Azure portal. Each activity is a cmdlet, and you connect them with links that control flow and data.
Best for: teams with limited scripting experience or simple linear workflows. Not recommended for complex logic — the visual editor becomes unwieldy with conditionals and loops.
Webhooks for Triggering
Webhooks let external systems trigger runbooks via HTTP POST. This is how you integrate Azure Automation with monitoring alerts, CI/CD pipelines, or custom applications.
# Create a webhook for a runbook (valid for 1 year)
az automation webhook create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name wh-stop-dev-vms \
--runbook-name Stop-DevVMs \
--expiry-time "2026-11-15T00:00:00Z" \
--parameters SubscriptionId="<sub-id>"
The webhook URL is shown only at creation time — save it immediately. You cannot retrieve it later.
# Trigger a runbook via webhook
curl -X POST "https://s1events.azure-automation.net/webhooks?token=<token>" \
-H "Content-Type: application/json" \
-d '{"SubscriptionId": "<sub-id>"}'
A common pattern: Azure Monitor fires an alert when CPU exceeds 90% on a VM. The alert's Action Group calls a webhook that triggers a runbook to scale up the VM or add instances to a scale set.
Schedules
Schedules run runbooks at specific times or intervals:
# Create a daily schedule (runs at 7 PM every weekday)
az automation schedule create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name "weekday-evening-shutdown" \
--frequency Week \
--interval 1 \
--start-time "2025-11-15T19:00:00-05:00" \
--time-zone "America/New_York" \
--description "Stop dev VMs every weekday evening"
# Link the schedule to a runbook
az automation job-schedule create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--runbook-name Stop-DevVMs \
--schedule-name "weekday-evening-shutdown" \
--parameters SubscriptionId="<sub-id>"
# Create a monthly schedule for orphaned resource reports
az automation schedule create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name "monthly-orphan-scan" \
--frequency Month \
--interval 1 \
--start-time "2025-12-01T09:00:00-05:00" \
--time-zone "America/New_York"
Hybrid Worker Groups
Azure Automation runbooks normally execute on Azure-hosted sandboxes. But what if you need to run scripts on on-premises servers, in other clouds, or on Azure VMs that need local network access? Hybrid Runbook Workers solve this.
# Install the Hybrid Worker extension on an Azure VM
az vm extension set \
--resource-group rg-production \
--vm-name vm-worker-01 \
--name HybridWorkerForLinux \
--publisher Microsoft.Azure.Automation.HybridWorker \
--type HybridWorkerForLinux \
--settings '{
"AutomationAccountURL": "https://eastus.azure-automation.net/accounts/<account-id>"
}'
# Create a hybrid worker group
az automation hrwg create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name "on-prem-workers" \
--credential-name "worker-credential"
Use cases for hybrid workers:
- Running scripts that need access to on-premises databases or file shares
- Executing commands on VMs in locked-down VNets without public endpoints
- Managing non-Azure resources (VMware, other clouds)
- Running long-duration jobs that exceed the 3-hour sandbox limit
State Configuration (DSC)
Azure Automation State Configuration uses PowerShell Desired State Configuration (DSC) to enforce server configurations. You define what the server should look like, and DSC continuously checks and corrects drift.
# DSC Configuration: Ensure IIS is installed and specific services are running
Configuration WebServerConfig {
param(
[string[]]$NodeName = "localhost"
)
Import-DscResource -ModuleName PSDesiredStateConfiguration
Node $NodeName {
WindowsFeature IIS {
Name = "Web-Server"
Ensure = "Present"
}
WindowsFeature IISManagement {
Name = "Web-Mgmt-Console"
Ensure = "Present"
DependsOn = "[WindowsFeature]IIS"
}
Service W3SVC {
Name = "W3SVC"
StartupType = "Automatic"
State = "Running"
DependsOn = "[WindowsFeature]IIS"
}
File WebContent {
DestinationPath = "C:\inetpub\wwwroot\health.html"
Contents = "<html><body>OK</body></html>"
Ensure = "Present"
Type = "File"
DependsOn = "[WindowsFeature]IIS"
}
}
}
# Upload and compile the DSC configuration
az automation dsc configuration create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name WebServerConfig \
--location eastus \
--source @WebServerConfig.ps1
# Register a VM node with the configuration
az automation dsc node register \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--vm-name vm-web-01 \
--node-configuration "WebServerConfig.localhost" \
--configuration-mode ApplyAndAutoCorrect \
--reboot-if-needed true
Configuration modes:
- ApplyOnly — Apply once and do not check again
- ApplyAndMonitor — Apply once and report drift (no auto-fix)
- ApplyAndAutoCorrect — Apply and continuously fix drift (recommended)
Update Management
Update Management assesses and deploys OS patches for Windows and Linux VMs — both Azure VMs and Arc-connected on-premises servers.
# Enable Update Management on an Automation account
az automation update-management enable \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--workspace "/subscriptions/<sub-id>/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/law-ops"
# Check update compliance for all managed VMs
az rest \
--method POST \
--url "https://management.azure.com/subscriptions/<sub-id>/resourceGroups/rg-automation/providers/Microsoft.Automation/automationAccounts/aa-operations-eastus/softwareUpdateConfigurations?api-version=2019-06-01"
Create update deployment schedules to patch VMs during maintenance windows:
# Create a weekly patch schedule for production Linux VMs
az automation software-update-configuration create \
--automation-account-name aa-operations-eastus \
--resource-group rg-automation \
--name "weekly-linux-patches" \
--operating-system Linux \
--included-update-classifications "Critical,Security" \
--azure-virtual-machines "['/subscriptions/<sub-id>/resourceGroups/rg-prod/providers/Microsoft.Compute/virtualMachines/vm-app-01', '/subscriptions/<sub-id>/resourceGroups/rg-prod/providers/Microsoft.Compute/virtualMachines/vm-app-02']" \
--frequency Week \
--interval 1 \
--start-time "2025-11-16T02:00:00-05:00" \
--time-zone "America/New_York" \
--duration "PT2H" \
--reboot-setting IfRequired
The update assessment shows you which VMs are missing critical patches, how many updates are pending, and the last assessment time. No more guessing which servers are behind on patches.
Inventory and Change Tracking
Change Tracking records changes to files, Windows registry, services, Linux daemons, and installed software across your VMs. Inventory collects data about what is installed.
Together, they answer questions like:
- Who installed Python 3.12 on the production web server last Tuesday?
- Which VMs had their SSH configuration changed this week?
- What software is running on each VM that we do not manage?
This integrates with Log Analytics for querying and alerting.
Comparison with Ansible and Puppet
| Capability | Azure Automation | Ansible | Puppet |
|---|---|---|---|
| Agent | Azure VM extension or hybrid worker | Agentless (SSH/WinRM) | Agent-based |
| Language | PowerShell, Python | YAML (playbooks) | Puppet DSL |
| State enforcement | DSC (continuous drift correction) | Idempotent playbook runs | Continuous enforcement |
| Cloud scope | Azure-native (Azure VMs + Arc) | Any cloud, any OS | Any cloud, any OS |
| Update mgmt | Built-in Update Management | Via modules/roles | Via modules |
| Cost | Free tier (500 min/month) | Free (community), paid (Tower/AAP) | Free (open source), paid (Enterprise) |
| Best for | Azure-centric shops | Multi-cloud, Linux-heavy | Large enterprises, complex configs |
Azure Automation is the obvious choice if you are all-in on Azure. It integrates natively with Azure Monitor, Azure Policy, and RBAC. If you manage multi-cloud or heavily on-premises infrastructure, Ansible or Puppet gives you broader reach.
Pricing and Limits
| Resource | Free Tier | Basic Tier |
|---|---|---|
| Runbook job minutes | 500/month | Unlimited ($0.002/min) |
| DSC nodes | 5 | $6/node/month |
| Watchers | 744 hours/month | Same |
| Update Management | Free (Log Analytics costs apply) | Same |
| Variables/Credentials | 200 | 200 |
| Schedules | 200 | 200 |
| Sandbox execution limit | 3 hours max | 3 hours max |
| Hybrid worker execution | No time limit | No time limit |
Wrapping Up
Azure Automation eliminates the "I'll just SSH in and do it manually" pattern that creates undocumented, unreproducible changes. Start with the obvious wins: schedule dev VM shutdowns, automate orphaned resource cleanup, and set up Update Management for patch compliance. Then move to State Configuration for enforcing server baselines and hybrid workers for on-premises operations. Every manual operational task you automate is one less 2 AM page and one more auditable, repeatable process.
This post wraps up our Azure Automation coverage. In the next series, we will explore Azure Container Apps and serverless containers — deploying containerized workloads without managing Kubernetes clusters or infrastructure.
