Marta Ríos did the exercise of writing down where her week went. The result is uncomfortable: shutting down and starting up the development environments, deleting disk snapshots nobody remembers any more, checking which certificates are about to expire, rotating credentials, exporting a report of untagged resources and patching the machines. None of it is hard, none of it requires judgment, and all of it has to be done. Those are hours of a senior profile spent on tasks a machine would do better, the same way every time and without forgetting.

With the platform now observable after the previous three lessons, it is time to automate the operations. Azure Automation is the service that runs scripts on a schedule or triggered by events, with an identity of its own, with no server to maintain and with a record of everything it does. In this lesson you will build Contoso's Automation account, write the runbook that shuts down the development environments at night, connect it to the alerts from 07-01, and see where Automation ends and Azure Update Manager and Machine Configuration begin.

Contents

  1. What Azure Automation is and its components
  2. Runbook types and which to choose
  3. Creating aa-contoso-operaciones with a managed identity
  4. The flagship runbook: shut down and start up by tag
  5. Schedules, testing, source control and publishing
  6. Firing a runbook from an alert: runbook, Logic App or function
  7. Update management with Azure Update Manager
  8. Desired State Configuration and Machine Configuration
  9. Azure Resource Graph: inventory at scale
  10. Costs and runbook best practices
  11. Common Mistakes and Tips
  12. Exercises
  13. Conclusion

  1. What Azure Automation is and its components

Azure Automation is a managed service that runs scripts — called runbooks — on Microsoft's infrastructure, under an identity of its own and with a complete record of every run. There is nothing to provision or patch. Its pieces:

Component What it is Use at Contoso
Automation account The container: identity, assets, runbooks and jobs aa-contoso-operaciones
Runbook The script and its logic Detener-IniciarEntornosDev
Schedule When it runs, with its time zone Every weekday at 20:00 and at 07:30
Job A specific run, with its output and its status Queryable, and queried, with KQL
Variables Configuration values, with optional encryption Subscription identifiers, exclusion lists
Credentials An encrypted username and password Access to the office's legacy systems
Certificates and connections Reusable authentication material Integration with the billing system
Modules The PowerShell or Python libraries available Az.Accounts, Az.Compute, Az.Resources
Hybrid Runbook Worker An agent that runs the runbook outside Azure On-premises servers in the Barcelona office

The Hybrid Runbook Worker deserves a note. A normal runbook runs in the Azure cloud and only reaches whatever is accessible from the internet or over a private network. The check-in desk billing servers Contoso still keeps in Barcelona are not. By installing the agent on one of those machines, the same runbook runs inside the local network, with access to internal resources, but scheduled and audited from Azure. It is the way to automate on-premises work without opening anything outwards, and it fits with the hybrid connectivity from module 2.

  1. Runbook types and which to choose

Type Language Checkpoints When to choose it
PowerShell PowerShell 7.x No The default choice for tasks on Azure
PowerShell Workflow Workflow syntax Yes, with parallelism Very long, interruptible processes; on its way out
Python Python 3.x No Teams with a Python background, or data libraries
Graphical A visual, codeless editor No Demos; hard to version and review

The recommendation is straightforward: PowerShell unless there is a compelling reason otherwise. The Az modules cover the whole of Azure, the syntax is the same as in Cloud Shell (module 1) and the result is a text file that gets reviewed in a pull request. Graphical runbooks look convenient and are not: they cannot be diffed in Git, they do not review well and they are not reusable.

  1. Creating aa-contoso-operaciones with a managed identity

The account needs permissions to act on the resources. The correct way — and the only acceptable one in production — is a system-assigned managed identity, with the minimum permissions at the minimum scope, applying the RBAC from module 4.

# 1. Automation account with a system-assigned managed identity
az automation account create \
  --name aa-contoso-operaciones \
  --resource-group rg-contoso-seguridad-pro \
  --location westeurope --sku Free \
  --assign-identity \
  --tags entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042 propietario=marta.rios

# 2. The principal identifier of that identity
PRINCIPAL=$(az automation account show \
  --name aa-contoso-operaciones --resource-group rg-contoso-seguridad-pro \
  --query identity.principalId -o tsv)

# 3. MINIMUM and SCOPED permission: only start/stop VMs, and only in development
az role assignment create \
  --assignee "$PRINCIPAL" \
  --role "Virtual Machine Contributor" \
  --scope "/subscriptions/<id-desarrollo>/resourceGroups/rg-contoso-reservas-dev"

# 4. Global read access so it can inventory, with no ability to modify
az role assignment create --assignee "$PRINCIPAL" --role "Reader" \
  --scope "/subscriptions/<id-desarrollo>"

Three decisions worth reasoning through. The scope is rg-contoso-reservas-dev, not the subscription: a bug or an abuse of the runbook cannot touch production, by construction. The role is the most specific one that works, never Contributor at subscription level, and still less Owner — which would additionally let the identity grant itself more permissions. And the identity is managed, with no secrets to rotate or leak; the classic certificate-based Run As accounts have been retired.

After that you have to import the modules you need (Az.Accounts, Az.Compute, Az.Resources) and pin the runtime version. An automatic module update can change the behavior of a runbook that had been working for a year; in production, the version is pinned and updated deliberately.

  1. The flagship runbook: shut down and start up by tag

This is the runbook that saves the most money and the one that best illustrates good practice. Development resources tagged horario=laborable have no reason to be running at night or at the weekend: that is around 128 of the 168 hours in a week, and that is straight off the bill.

<#
.SYNOPSIS
    Starts or stops the development resources tagged horario=laborable.
.DESCRIPTION
    Idempotent: it checks the state before acting and does not fail if it is already correct.
    Logs every decision so it can be audited from log-contoso-pro.
#>
param(
    [Parameter(Mandatory = $true)]
    [ValidateSet("Iniciar", "Detener")]
    [string]$Action,

    [string]$ResourceGroup = "rg-contoso-reservas-dev",
    [string]$TagKey        = "horario",
    [string]$TagValue      = "laborable",

    # Dry run: logs what it would do, without doing it. Essential on first use.
    [bool]$DryRun = $false
)

$ErrorActionPreference = "Stop"      # any unhandled error stops the runbook
$start = Get-Date
Write-Output "=== $Action | group=$ResourceGroup | dryrun=$DryRun | $start ==="

try {
    # 1. Authentication with the managed identity: no secrets in the code
    Connect-AzAccount -Identity | Out-Null
    Write-Output "Authenticated with the managed identity of aa-contoso-operaciones."

    # 2. Find the machines by tag. -Status brings back the real power state.
    $vms = Get-AzVM -ResourceGroupName $ResourceGroup -Status |
        Where-Object { $_.Tags[$TagKey] -eq $TagValue }

    if (-not $vms) {
        Write-Warning "No machine with $TagKey=$TagValue in $ResourceGroup."
        return
    }
    Write-Output "Found $($vms.Count) candidate machines."

    $acted = 0; $skipped = 0; $failed = 0

    foreach ($vm in $vms) {
        $state = ($vm.Statuses | Where-Object Code -like "PowerState/*").Code

        # 3. Idempotency: if it is already in the desired state, leave it alone
        $alreadyCorrect = ($Action -eq "Iniciar" -and $state -eq "PowerState/running") -or
                          ($Action -eq "Detener" -and $state -eq "PowerState/deallocated")
        if ($alreadyCorrect) {
            Write-Output "SKIPPED $($vm.Name): already in $state."
            $skipped++; continue
        }

        if ($DryRun) {
            Write-Output "DRY RUN $($vm.Name): would run '$Action' (current state $state)."
            $acted++; continue
        }

        # 4. Each machine in its own try: one failure must not abort the rest
        try {
            if ($Action -eq "Iniciar") {
                Start-AzVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -NoWait | Out-Null
            } else {
                # -Force avoids the interactive confirmation; stopping releases the compute cost
                Stop-AzVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Force -NoWait | Out-Null
            }
            Write-Output "DONE    $($vm.Name): '$Action' launched (previous state $state)."
            $acted++
        }
        catch {
            Write-Error "FAILED  $($vm.Name): $($_.Exception.Message)"
            $failed++
        }
    }

    $seconds = [int]((Get-Date) - $start).TotalSeconds
    Write-Output "=== Summary: acted=$acted skipped=$skipped failed=$failed in ${seconds}s ==="

    # 5. Structured output: queryable with KQL, and you can alert on it
    if ($failed -gt 0) { throw "The runbook finished with $failed failed machines." }
}
catch {
    Write-Error "General runbook error: $($_.Exception.Message)"
    throw            # rethrowing marks the job as Failed and allows alerting
}

The five numbered points are the good practices that make the difference between a script and a production runbook. Connect-AzAccount -Identity authenticates without a single secret. Selecting by tag instead of by a list of names means a new machine joins in on its own as soon as it is tagged — and it reinforces the tagging policy from module 1. Idempotency lets you run it twice with no strange effects, which matters a great deal when a retry triggers it. The per-machine try stops one isolated failure leaving the other fifteen running. And the final throw makes the job show up as failed, a condition you can alert on. The $DryRun parameter is the safety net on first use: you run it dry first and review the output.

An important cost warning: stopping a machine from inside the operating system does not stop the billing. Only the deallocated state, which is what Stop-AzVM produces, releases the compute. It is a classic mistake that makes the automation save nothing at all.

  1. Schedules, testing, source control and publishing

A runbook's lifecycle has a detail that surprises people: it exists in draft and in published form, and schedules always run the published version. Editing without publishing changes nothing in production, which is a protection, not an annoyance.

The correct order of work is: write, test in the test pane — which runs the draft interactively and shows the output live, ideal with $DryRun = $true — publish, and only then schedule.

# Import the runbook from the contoso-infra repository
az automation runbook create --automation-account-name aa-contoso-operaciones \
  --resource-group rg-contoso-seguridad-pro --name Detener-IniciarEntornosDev \
  --type PowerShell --runbook-type PowerShell72

az automation runbook replace-content --automation-account-name aa-contoso-operaciones \
  --resource-group rg-contoso-seguridad-pro --name Detener-IniciarEntornosDev \
  --content @./runbooks/Detener-IniciarEntornosDev.ps1

az automation runbook publish --automation-account-name aa-contoso-operaciones \
  --resource-group rg-contoso-seguridad-pro --name Detener-IniciarEntornosDev

Contoso's two schedules are daily, Monday to Friday, in the W. Europe Standard Time time zone: sch-apagado-dev at 20:00 with the parameter Action=Detener, and sch-encendido-dev at 07:30 with Action=Iniciar. Parameters are attached to the link between the schedule and the runbook, so the same runbook serves both with a single code base. The time zone is not a detail: a schedule in UTC shifts by an hour in summer and the team arrives at 8:00 to find the machines switched off.

Source control closes the loop with module 5. The account is connected to the contoso-infra repository in Azure Repos, folder /runbooks, and every push to the main branch synchronizes the runbooks. The consequences are the ones you already know: pull request review, a history of who changed what, and the ability to roll back. With synchronization active, the portal stops being the place where you edit; editing there creates divergences that the next sync overwrites.

  1. Firing a runbook from an alert: runbook, Logic App or function

In 07-01 you created ag-guardia-contoso and left it announced that an action group can run a runbook. Here the loop closes: the alert detects, the runbook remediates.

flowchart LR
  A["Metric: cola-emision-tarjetas<br/>depth > 500"] --> B["Azure Monitor alert"]
  B --> C["Action group<br/>ag-equipo-contoso"]
  C --> D["Runbook webhook<br/>Escalar-ProcesadoTarjetas"]
  C --> E["Email to the team"]
  D --> F["Increases instances<br/>and logs the action"]
  F --> G["New run visible<br/>in AzureDiagnostics"]

The trigger is a webhook: a URL with a one-time token that is generated when you create it and cannot be looked up again, so it has to be saved in kv-contoso-pro immediately. The action group sends the common alert schema to that URL, and the runbook receives it in the $WebhookData parameter, from which it extracts which resource fired the alert:

param([object]$WebhookData)

if (-not $WebhookData) { throw "This runbook must be invoked from an alert webhook." }

$alert    = (ConvertFrom-Json $WebhookData.RequestBody).data.essentials
$resource = $alert.alertTargetIDs[0]
Write-Output "Alert '$($alert.alertRule)' with severity $($alert.severity) on $resource"

Connect-AzAccount -Identity | Out-Null
# ... remediation logic, idempotent and logged ...

Runbook, Logic App or function? All three can react to an alert, and the choice matters:

Automation runbook Logic App Azure Function
Strong at Operating Azure infrastructure Integrating systems and notifying Custom logic and performance
Language PowerShell / Python Visual designer, connectors Whichever you choose (06-03)
Startup latency Seconds to a minute or so Seconds Milliseconds (or a cold start)
Long runs Yes, hours Yes, with limits Limited depending on the plan
Who maintains it Operations Operations and the business Development
Cost Minutes, with a free allowance Per action executed Per execution and consumption
Case at Contoso Shutting down environments, cleaning up snapshots Notifying in Teams and opening an incident (06-04) GenerarTarjetaEmbarque (06-03)

The rule Contoso applies: if the task manipulates Azure infrastructure and an administrator would have written it, it is a runbook. If it consists of chaining systems together and notifying people, it is a Logic App. If it is business logic with latency requirements, it is a function. And nothing stops you combining them: the queue alert simultaneously fires a runbook that scales out and a Logic App that posts in Teams.

  1. Update management with Azure Update Manager

The old update management solution built into Automation has been replaced by Azure Update Manager, an independent service that no longer requires an Automation account or a workspace, and that works the same way for Azure machines and for Arc-enabled servers.

It has three pieces: assessment of each machine's patch status, on-demand deployment, and maintenance configurations, which are recurring windows with their own scope and reboot policy.

# Monthly maintenance window for the development environment
az maintenance configuration create \
  --resource-group rg-contoso-seguridad-pro \
  --resource-name mc-contoso-dev-mensual \
  --location westeurope \
  --maintenance-scope InGuestPatch \
  --duration 03:00 \
  --recur-every "Month Second Saturday" \
  --start-date-time "2026-09-12 02:00" --time-zone "W. Europe Standard Time" \
  --reboot-setting IfRequired \
  --tags entorno=desarrollo proyecto=contoso-reservas centro-coste=CC-1042 propietario=marta.rios

Contoso keeps two configurations: mc-contoso-dev-mensual, which patches vm-motor-disponibilidad-dev in the small hours of the second Saturday, and mc-contoso-pro-mensual, more conservative, for the base operating system of vmss-api-disponibilidad-pro. On a scale set the preferred strategy is still a different one: replacing instances with a new, already patched image rather than patching in place, which is what is consistent with cattle versus pets from module 2. And remember to coordinate the window with an alert processing rule from 07-01 so nobody gets woken up by planned reboots.

  1. Desired State Configuration and Machine Configuration

Automation used to include State Configuration (DSC), which defines in code the state a machine must be in — which services are running, which packages are installed, which registry keys are set — and corrects it when it drifts. That capability has moved to Azure Machine Configuration, an extension of Azure Policy: instead of living in the Automation account, it is assigned as one more policy inside the "Base de gobernanza de Contoso" initiative from module 4, and its compliance shows up alongside everything else in the governance view.

The conceptual difference from Automation is the one worth holding on to: a runbook performs an action at a given moment; Machine Configuration watches and maintains a state continuously. Contoso uses it to require that the Azure Monitor Agent is installed on every server and that certain services are stopped, without writing a single runbook.

  1. Azure Resource Graph: inventory at scale

Before automating you need to know what you are automating over. Azure Resource Graph uses KQL to query the metadata of every resource across every subscription in seconds, something that with az resource list walking through resource groups would take minutes and exhaust the API limits. It is Automation's natural complement: the runbook acts, Resource Graph decides what it acts on.

Resources
| where type =~ "microsoft.compute/virtualmachines"
| extend entorno   = tostring(tags["entorno"]),
         horario   = tostring(tags["horario"]),
         propietario = tostring(tags["propietario"]),
         centroCoste = tostring(tags["centro-coste"])
| where entorno == "desarrollo"
| extend Tagged = iif(isnotempty(horario), "yes", "NO")
| project name, resourceGroup, location, Tagged, propietario, centroCoste,
          size = tostring(properties.hardwareProfile.vmSize)
| order by Tagged asc, name asc

The query reads the same way as the KQL from 07-02, with two differences: the Resources table holds the configuration of the resources, not their logs, and there is no TimeGenerated because it describes the current state, not a time series. Its value here is concrete: the Tagged column lists exactly the development machines that do not carry horario and that the runbook is therefore ignoring, and which stay switched on at night. You run it with az graph query -q "<query>" or from a runbook with Search-AzGraph.

  1. Costs and runbook best practices

The pricing model is one of the kindest in Azure: you pay for the job execution minutes, with a generous monthly free allowance — of the order of 500 minutes — that more than covers usage like Contoso's, plus the managed nodes for state configuration, if you use them. The Automation account itself costs nothing. Put in perspective: the shutdown runbook consumes a few minutes a day and saves 75% of the compute cost of a whole environment. It is probably the best ratio on the entire platform, and you will come back to it in module 8.

The good practices that separate a reliable runbook from a time bomb:

  • Idempotent: running it twice produces the same result. Check the state before acting.
  • Parameterized, with nothing hard-coded: names, groups and scopes as parameters or as account variables.
  • With a dry-run mode: to use it for the first time without risk, and for debugging.
  • With structured logging: Write-Output in a consistent format, so it can be queried with KQL.
  • With per-item error handling: an isolated failure must not abort the batch, but it must be reflected in the final status.
  • Bounded by scope and by tag: never at whole-subscription level "for convenience".
  • Versioned in Git, reviewed by pull request and published from the repository.

And the monitoring, which closes the loop with what you have learned: Automation jobs send their status and their output to log-contoso-pro through a diagnostic setting (categories JobLogs and JobStreams), so you can alert on JobStatus == "Failed" with a log search alert. A runbook that fails silently is worse than having no runbook, because the team believes the task is getting done.

Common Mistakes and Tips

  • Editing and not publishing. The schedule runs the published version; the draft changes nothing.
  • Stopping the VM from inside the operating system. It carries on billing: only Stop-AzVM releases it (deallocated).
  • Excessive permissions. Contributor on the subscription for convenience turns a runbook bug into a production incident.
  • Scheduling in UTC. The daylight saving change shifts the start-up and the team finds the machines switched off.
  • Unmonitored runbooks. With no alert on failed jobs, the task stops getting done and nobody notices.
  • Automatic module updates in production. They can break a stable runbook; pin the versions.
  • Tip: run every new runbook with $DryRun = $true first and review the output before letting it act.
  • Tip: select by tag, never by a list of names. Lists go stale; tags maintain themselves if policy requires them.
  • Tip: save the webhook token in kv-contoso-pro the moment it is generated. There is no way to retrieve it afterwards.

Exercises

Exercise 1. Design a runbook that deletes disk snapshots more than 90 days old in rg-contoso-reservas-dev, except those tagged conservar=si. Describe the parameters, the safeguards, the permissions, the schedule and the monitoring. The full code is not required.

Exercise 2. The shutdown runbook has been running "correctly" for three weeks, but the development bill has not gone down. List five possible causes and how to check each one.

Exercise 3. Contoso Miles (centro-coste=CC-2077) needs to export each day's redemptions to a file every night, drop it in stlagocontosopro and notify the finance team. Decide between a runbook, a Logic App and a function — or a combination — and justify the architecture.

Solutions

Solution 1: parameters $ResourceGroup, $AgeInDays = 90, $ExclusionTag = "conservar", $DryRun = $true by default, because this is a destructive operation and the safe value must be the default. Safeguards: filter by age with TimeCreated and exclude the tagged ones; cap the number of deletions per run — 50, for example — so a filter bug does not wipe everything out; a try per snapshot; and log the name, the date and the size of each one before deleting it, so there is a record of what was removed. Permissions: a role scoped to rg-contoso-reservas-dev with permission to read and delete snapshots, never subscription Contributor. Schedule: weekly, at the weekend, outside the maintenance window so they do not overlap. Monitoring: a log search alert on JobStatus == "Failed" and, additionally, on an abnormally high number of deletions, which is the signal that the filter has broken. And the underlying safeguard: check that current backups exist before considering a snapshot expendable, which is what 07-05 covers.

Solution 2: (a) The machines are being stopped from inside the operating system, or the state is stopped and not deallocated, so the compute carries on being billed; you check it with Get-AzVM -Status or with Resource Graph looking at the power state. (b) Tags are missing: there are development machines without horario=laborable that the runbook ignores; you detect it with the Resource Graph query from section 9. (c) The cost is not compute: managed disks, public IPs and reserved addresses are billed even when the machine is deallocated, and they can be most of the spend on a small environment. (d) Somebody switches the machines on and does not switch them off, or there is a process starting them; you verify it with AzureActivity, filtering on the start operation and looking at which identity runs it. (e) The spend is somewhere else: App Service, databases or log-contoso-pro itself, which the runbook does not touch; you confirm it with the per-resource cost analysis from module 8. A cross-cutting check: review the job logs in log-contoso-pro, because "it runs correctly" may mean it acts on zero machines every night.

Solution 3: a combined architecture, with each piece doing what it does best. A PowerShell runbook scheduled at 23:30 runs the redemptions KQL query over log-contoso-pro with Invoke-AzOperationalInsightsQuery, generates the CSV file and uploads it to stlagocontosopro, authenticating with the managed identity of aa-contoso-operaciones; it is the right option because it is an infrastructure task, it can take minutes and an administrator would have written it. The notification is handled by a Logic App reacting to a blob created event — Event Grid, module 6 — sending the notice to Teams and by email with the link: that is integration with people, not infrastructure, and the finance team can ask for changes to the message without touching code. No function is needed: there is no complex business logic and no latency requirement. Minimum permissions: Log Analytics Reader on the workspace and Storage Blob Data Contributor only on the destination container. Monitoring: an alert on failed jobs and a second alert if the file has not appeared by 00:30, because a runbook that does not run generates no error at all. Tags: entorno, proyecto=contoso-millas, centro-coste=CC-2077 and propietario.

Conclusion

You now know what Azure Automation is and what it is for: running the repetitive work that was consuming Marta Ríos' weeks, with no servers, with an identity of its own and with a record of everything. You know its components — account, runbooks, schedules, jobs, variables, credentials, certificates, connections and modules — and the Hybrid Runbook Worker that takes the same automation to the on-premises servers in the Barcelona office. You know how to choose the runbook type, with PowerShell as the default and graphical ones ruled out for not being versionable. You have created aa-contoso-operaciones with a managed identity and minimum permissions scoped to rg-contoso-reservas-dev, applying the RBAC from module 4 instead of handing out Contributor for convenience.

The flagship runbook has left you a complete, reusable pattern: authentication with no secrets, selection by tag instead of by list, idempotency by checking the state before acting, a dry-run mode to use it for the first time without risk, per-machine error handling, structured logging and a final throw so the job is marked as failed and can be alerted on — plus the detail that decides whether the automation saves anything at all: only deallocated stops the billing. You know how to test it in the test pane, publish it — because the schedule runs the published version, not the draft — schedule it with the correct time zone and version it in contoso-infra with source control, closing the loop with module 5.

You have also closed the loop with 07-01: an Azure Monitor alert fires a runbook through a webhook, with its token saved in kv-contoso-pro, and you have the decision table for choosing between runbook, Logic App and function depending on whether you are manipulating infrastructure, integrating systems or running business logic. You place Azure Update Manager as the successor to update management, with its maintenance configurations mc-contoso-dev-mensual and mc-contoso-pro-mensual, and Machine Configuration as DSC's heir inside Azure Policy, with the key distinction: the runbook acts once, the configuration maintains a state. And you carry Azure Resource Graph as the instrument for inventory at scale so you know what to automate over, plus the per-minute costs with their free allowance and the good practices that make a runbook reliable.

One last piece remains, and it is the one that decides whether all the rest is worth anything. Contoso now has a platform that sees what is happening and that largely operates itself, but none of that answers the question of the bad day: an accidental deletion, ransomware encryption, an entire region that stops responding. Lesson 07-05, Backup and Disaster Recovery, closes the module with the two numbers that govern that conversation — RPO and RTO — Azure Backup and its vaults, protection against malicious deletion, what each PaaS service backs up on its own and what remains yours, Azure Site Recovery, the strategy between West Europe and North Europe, and the written plan that has to be tested, because an untested plan does not exist.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved