Customize your Azure VMs with Custom Script Extensions

Custom Script Extensions are a possibility to customize ARM VMs without using ARM templates. All you need is PowerShell. Sounds great? Then give it a try!

Howdy folks and a happy new year to all of you,

today I’m gonna show you a way to automatically deploy and customize virtual machines in Azure Resource Manager without using ARM templates. Custom Script Extensions can help you to achieve your goal by only leveraging PowerShell.

Azure Custom Script Extensions (CSE) are a great way to customize your VM’s operating system without having to log on or do it manually. You can use CSE on Linux and Windows VMs and write them for the OS’s native scripting environment, e.g. PowerShell or bash. Let me give you an example:

I’ve deployed a new ARM VM using the following PowerShell script:

Import-Module Azure
$location = 'NorthEurope'
$ResourceGroupName= 'myRG'
$LocalAdminUser ='myAdmin'
$ResourceGroup = New-AzureRmResourceGroup -Name $ResourceGroupName -Location $Location
$StorageAccountName = 'letstalkazure'
$Test = Get-AzureRmStorageAccountNameAvailability $StorageAccountName
If ($Test) {
$StorageAccount =New-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName -SkuName 'Standard_GRS' -Kind 'Storage' -Location $location
$mySubnet = New-AzureRmVirtualNetworkSubnetConfig -Name 'mySubnetConfig' -AddressPrefix
$myVnet = New-AzureRmVirtualNetwork -Name 'myVnet' -ResourceGroupName $ResourceGroupName -Location $location -AddressPrefix -Subnet $mySubnet
$myPublicIp = New-AzureRmPublicIpAddress -Name 'myPIP' -ResourceGroupName $ResourceGroupName -Location $location -AllocationMethod Static
$myNIC = New-AzureRmNetworkInterface -Name 'myNIC' -ResourceGroupName $ResourceGroupName -Location $location -SubnetId $myVnet.Subnets[0].Id -PublicIpAddressId $myPublicIp.Id

## Now get the secret from your Azure Key Vault that you've created before and create your local admin credentials

$Secret = Get-AzureKeyVaultSecret -VaultName 'myKeyVault' -Name 'mySecret'
$Cred = [PSCredential]::new($LocalAdminUser, $Secret.SecretValue)

$myVm = New-AzureRmVMConfig -VMName 'myVM' -VMSize 'Standard_A2'
$myVM = Set-AzureRmVMOperatingSystem -VM $myVM -Windows -ComputerName 'myVM' -Credential $cred -ProvisionVMAgent -EnableAutoUpdate
$myVM = Set-AzureRmVMSourceImage -VM $myVM -PublisherName 'MicrosoftWindowsServer' -Offer 'WindowsServer' -Skus '2016-Datacenter' -Version 'latest'
$myVM = Add-AzureRmVMNetworkInterface -VM $myVM -Id $myNIC.Id
$blobPath0 = 'vhds/myOsDisk1.vhd'
$blobPath1 = 'vhds/myDataDisk1.vhd'
$blobPath2 = 'vhds/myDataDisk2.vhd'
$osDiskUri = $StorageAccount.PrimaryEndpoints.Blob.ToString() + $blobPath0
$DataDiskVhdUri1 = $StorageAccount.PrimaryEndpoints.Blob.ToString() + $blobPath1
$DataDiskVhdUri2 = $StorageAccount.PrimaryEndpoints.Blob.ToString() + $blobPath2
$myVM = Set-AzureRmVMOSDisk -VM $myVM -Name 'myOsDisk1' -VhdUri $osDiskUri -CreateOption fromImage
$myVM = Add-AzureRmVMDataDisk -VM $myVM -Name 'myDataDisk1' -VhdUri $DataDiskVhdUri1 -Caching 'ReadOnly' -DiskSizeInGB 10 -Lun 0 -CreateOption Empty
$myVM = Add-AzureRmVMDataDisk -VM $myVM -Name 'myDataDisk2' -VhdUri $DataDiskVhdUri2 -Caching 'ReadOnly' -DiskSizeInGB 10 -Lun 1 -CreateOption Empty

New-AzureRmVM -ResourceGroupName $ResourceGroupName -Location $location -VM $myVM

This PowerShell script creates a new ARM VM with an OS disk and two data disks. Be aware of the passage I use to create the local admin credentials from an Azure Key Vault secret. If you are not familiar with Azure Key Vault and secrets that are stored within read my blogpost from November 2016.
After VM creation those data disks are neither initialized nor formatted so if you’re up to use them later on you will have further tasks to do. This is when Custom Script Extensions come into play. I’ve created a PowerShell script that performs the following tasks:

  1. all raw disks are initialized using GPT partition style
  2. all of the initialized disks are formatted with NTFS file system
  3. all of the formatted disks are assigned a drive letter starting with F:\ and ending with Y:\
$Disks = Get-Disk | where {$_.PartitionStyle -like "Raw"}
$i = 0
$accessPath = @()
$accessPath += 102..121 | foreach {"$([char]$_):\" } ## add drive letters f:\ to y:\ to the array
Foreach ($Disk in $Disks) {
    Initialize-Disk -PartitionStyle GPT -Number $Disk.Number
    New-Volume -Disk $Disk -FileSystem NTFS -FriendlyName Data -AccessPath $accessPath[$i]

You can run the script manually on a Windows operating system after logging in or you use Azure CSE. All you need to do is log in into Azure Primary Portal, choose your new VM and select the Extensions setting.

Select Extensions setting on your ARM VM

Then you click “+ Add” and select Custom Script Extension and create. Now you can upload your CSE script.

Of course you can also manage those tasks using PowerShell 🙂

First of all you need to upload your script file to Azure storage.

### Upload .ps1 script to Azure storage account
$context = New-AzureStorageContext -StorageAccountName "MySAName" -StorageAccountKey "MySAKey"
Set-AzureRmCurrentStorageAccount -Context $Context
New-AzureStorageContainer -name 'myContainerName'
Set-AzureStorageBlobContent -File 'localpath\myFilename.ps1' -container 'myContainerName'

then you can configure your VM to run the CSE.

### Run CSE on VM
Set-AzureRmVMCustomScriptExtension -Name 'myCSE' `
-ContainerName 'myContainer' `
-FileName Azure_CustomScriptExtension.ps1 `
-StorageAccountName 'mySA' `
-ResourceGroupName 'myRG' `
-VMName 'myVM' `
-Run Azure_CustomScriptExtension.ps1 `
-Location NorthEurope

After your CSE has finished you can remove it again using the following command.

### Remove CSE after successful run
remove-AzureRmVMCustomScriptExtension -Name 'myCSE' -ResourceGroupName 'myRG' -VMName 'myVM' -Force

That’s all for now.

Stay tuned and kind regards,

Author: Tom Janetscheck

Cloud Security Enthusiast | Security Advocate

3 thoughts on “Customize your Azure VMs with Custom Script Extensions”

  1. i had to change line 6 of your script to:
    Initialize-Disk -PartitionStyle GPT -Number $Disk.Number

    in order to get it working.
    Thanks for this blog!

    Liked by 1 person

Leave a Reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: