Azure VM Custom Script Extensions: Post-Boot Automation

The Custom Script Extension lets you run any shell or PowerShell script on an Azure VM after it provisions. It is the bridge between “bare OS image” and “configured, ready-to-run server” — useful for installing packages, writing configuration files, joining a domain, or registering with a configuration management system. This page shows how to use it, what the limitations are, and when you should switch to a different approach.

What VM extensions are

Azure VM extensions are small agent-managed programs that run inside a VM in response to commands from the Azure control plane. The Azure VM Agent (installed by default on all Azure Marketplace images) listens for extension commands and executes them. Extensions are available for common tasks: running scripts (Custom Script Extension), enrolling in antivirus, deploying monitoring agents, and joining Azure Active Directory.

The Custom Script Extension specifically allows you to point at a script file or specify inline commands, and Azure executes them inside the VM as root (Linux) or SYSTEM (Windows). The script runs once unless you update the extension configuration to trigger a new run.

Running a script on a Linux VM

The simplest usage: run an inline command on an already-provisioned VM.

az vm extension set \
  --resource-group my-rg \
  --vm-name my-vm \
  --name customScript \
  --publisher Microsoft.Azure.Extensions \
  --settings '{"commandToExecute": "apt-get update && apt-get install -y nginx"}'

For longer scripts, store the script in Azure Blob Storage and reference it by URI:

# Upload script to blob storage first
az storage blob upload \
  --account-name mystorageaccount \
  --container-name scripts \
  --name setup.sh \
  --file ./setup.sh

# Run the script on the VM
az vm extension set \
  --resource-group my-rg \
  --vm-name my-vm \
  --name customScript \
  --publisher Microsoft.Azure.Extensions \
  --settings '{
    "fileUris": ["https://mystorageaccount.blob.core.windows.net/scripts/setup.sh"],
    "commandToExecute": "bash setup.sh"
  }'
Note

If your blob storage container is private (which it should be for production scripts), pass a SAS token in the protected settings, not the public settings. Protected settings are encrypted and not visible in the Azure portal or deployment history. Use —protected-settings for any value you do not want logged.

Running a script on a Windows VM

Windows VMs use a different extension name and publisher:

az vm extension set \
  --resource-group my-rg \
  --vm-name my-windows-vm \
  --name CustomScriptExtension \
  --publisher Microsoft.Compute \
  --settings '{
    "fileUris": ["https://mystorageaccount.blob.core.windows.net/scripts/setup.ps1"],
    "commandToExecute": "powershell -ExecutionPolicy Unrestricted -File setup.ps1"
  }'

PowerShell scripts on Windows have the same pattern: reference a file in Blob Storage, execute it with PowerShell. For simple inline commands, use commandToExecute directly with the PowerShell invocation inline.

Checking extension status and logs

After running an extension, check whether it succeeded:

# Check extension status on the VM
az vm extension list \
  --resource-group my-rg \
  --vm-name my-vm \
  --output table

# Get detailed status including error messages
az vm extension show \
  --resource-group my-rg \
  --vm-name my-vm \
  --name customScript

For deeper diagnostics, connect to the VM and check the log file. On Linux:

# Script output and error logs
cat /var/log/azure/custom-script/handler.log

# See the actual stdout/stderr from the last script run
ls /var/lib/waagent/custom-script/download/
cat /var/lib/waagent/custom-script/download/0/stdout
cat /var/lib/waagent/custom-script/download/0/stderr

Running a script at VM creation time

You can attach a Custom Script Extension at the same time you create the VM, so the script runs immediately after the OS boots:

az vm create \
  --resource-group my-rg \
  --name my-vm \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --custom-data cloud-init.yaml

Note the shift to —custom-data here: for at-creation bootstrapping on Linux, cloud-init is generally preferred over Custom Script Extensions. Cloud-init is a widely supported Linux initialisation system that runs before the Azure agent processes extensions, supports multi-step configurations, and has better logging. Custom Script Extension is more flexible for ad hoc post-boot automation and works equally well on Linux and Windows.

Tip

For Scale Sets where every instance needs the same bootstrap, consider whether a custom VM image (baked with your software pre-installed) would be better than a bootstrap script. Images boot faster, reduce the risk of script failures at scale, and avoid dependency on external script storage being available at boot time.

When to use Custom Script Extension vs alternatives

ApproachBest forLimitations
Custom Script ExtensionOne-off post-boot tasks, Windows or Linux, ad hoc changes to running VMsNo idempotency guarantees, runs once per configuration change
Cloud-initInitial Linux VM setup — packages, users, files, servicesLinux only, at-creation time only
Custom VM imageConsistent baseline across many VMs, fast Scale Set bootImage rebuild needed for updates, more complex pipeline
Ansible / Chef / PuppetOngoing configuration management, drift detection, complex multi-step configRequires a configuration management setup, steeper learning curve
Terraform provisionersSimple commands run once as part of a Terraform deploymentAnti-pattern in Terraform — avoid for anything complex

Common mistakes

  1. Putting credentials in the public settings. Extension settings appear in the Azure portal and in deployment logs. API keys, passwords, and SAS tokens belong in —protected-settings, which are encrypted and never displayed in the portal after submission.
  2. Not testing the script locally before running it as an extension. An extension failure leaves the VM in an inconsistent state. Test your scripts on a local VM or test VM first. Check syntax, verify package names, and make sure all file paths exist before deploying to production VMs via an extension.
  3. Using extensions for ongoing configuration management. Extensions are not designed for idempotent drift correction. If you need to enforce ongoing configuration state across many VMs, invest in a proper configuration management tool. Extensions are for initial setup and targeted one-off tasks.

Frequently asked questions

Can I run a Custom Script Extension on a VM that is already running?

Yes. Extensions can be applied to running VMs at any time, not just at creation. The Azure agent inside the VM receives the extension request and executes the script. You can also run a new script by updating the extension — change the file name or URI to trigger a new execution, since Azure only re-runs an extension if its configuration changes.

Where can the script file be stored?

Azure Blob Storage (public or private with a SAS token or managed identity), GitHub raw URLs (public), or a base64-encoded inline command in the extension settings. For production use, store scripts in Azure Blob Storage with access controlled through a managed identity rather than embedding credentials in the extension settings or using publicly accessible storage.

What happens if the script fails?

The extension reports a failure status that you can see in the portal or via az vm extension list. The VM itself continues running — a failed extension does not roll back the VM. Check the extension log at /var/log/azure/custom-script/handler.log (Linux) or C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\... (Windows) for the script output and error details.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.