Azure Automation – backup solution for old operating systems – Windows Server 2003 / 2008

During one of my projects, I worked on scripting a procedure for backing up virtual machines with the Windows Server 2003 / 2008 operating system. Microsoft provides Azure Backup Services for protecting virtual machines hosted in Azure. However, that service does not officially support legacy operating systems older than Windows Server 2008 R2. The main goal was to write a script that uses PowerShell parallelism and Azure Automation functionality.

The following piece of code contains the automation of copying disks in parallel for many virtual machines and their disks. The script uses asynchronous data transfer within the storage account and deletes backups older than the specified amount in script parameters. Each virtual machine has a tag that defines maintenance group called ‘BackupGroupName’.

Runbooks performs the following tasks:
• Stop virtual machine
• Copy all virtual machine VHD files to defined storage account
• Start virtual machine
• Check and clean-up old backup files – only keeping last backup files defined in runbook configuration


workflow Start-BackupLegacyVM
# Define output data type

#Input parameters
param (
# Maintenace group parameters
[string]$BackupGroupName = “01”,
[string]$BackupStorageAccount = “saeunstlrsbbvmbackup01”,
[int]$NumberOfBackups = 3

# Define time zone
$timeZone = “GMT Standard Time”

# Specify Action Preferences
$ErrorActionPreference = ‘Stop’
$PSPersistPreference = $true

# Logging to Azure
write-output “————————`nLogging to Azure`n————————”
$connectionName = “AzureRunAsConnection”

$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName

$Output = Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint

# Get all virtual machines objects tagged with BackupMG
$virtualMachines = Get-AzureRMVM | Where-Object -FilterScript {$_.Tags.Keys -eq “BackupGroupName” -and $_.Tags.Values -eq “$BackupGroupName”}

$VMs = $($virtualMachines | measure-object).Count

# For each virtual machine start in parrallel copy process
If ($VMs -gt 0) {
write-output “————————`nVirtual machines log`n————————”
$VMDetails = $virtualMachines | Select-Object Name, ResourceGroupName
write-output $VMDetails
write-output “`n”

foreach -parallel ($VM in $virtualMachines) {
# Main code
# —————————————————————————-
function Get-LocalTime {
param ($timeZone)
$UTCTime = (Get-Date).ToUniversalTime()
$TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($timeZone)
$localTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ)
return $localTime
try {
# Get variables outside inlinescript
$storageAccount = $Using:BackupStorageAccount
$numberofBackups = $Using:NumberOfBackups
$timeZone = $Using:timeZone
$vm = Get-AzureRMVM -Name ($Using:VM.Name) -ResourceGroupName ($Using:VM.ResourceGroupName)

# Define static variables
$CopyStatus = @{}
$CopyIndex = 0

# Get virtual machine status
$vmStatus = $($VM | Get-AzureRmVM -Status).Statuses[1].Code

# Stop virtual machine if it is not in stopped / deallocated state
# ————————-
If ($vmStatus -ne “PowerState/deallocated”) {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Stop virtual machine”
$output = Stop-AzureRmVM -Name $($vm.Name) -ResourceGroupName $($vm.ResourceGroupName) -Force
else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine is in stopped (deallocated) state”

# Set destination storage settings before copy process start (storage object, key and context)
$dstStorageAccountObject = Get-AzureRmStorageAccount | Where-Object {$_.StorageAccountName -eq $storageAccount}
$dstStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $dstStorageAccountObject.ResourceGroupName -StorageAccountName $dstStorageAccountObject.StorageAccountName).Key1
$dstContext = New-AzureStorageContext -StorageAccountName $dstStorageAccountObject.StorageAccountName -StorageAccountKey $dstStorageKey
$dstDiskDate = “_” + $(Get-Date -Format yyyyMMddHHmm)
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Set destination storage settings before copy process start (storage object, key and context)”

# Get information about virtual machine disks
$vmOSDisks = $vm.StorageProfile.osDisk
$vmDataDisks = $vm.StorageProfile.dataDisks

# Start copy process for OS disk
# ————————-
$srcDiskUri = $vmOSDisks.Vhd.Uri
$srcStorageAccount = $srcDiskUri.Substring(8,$srcDiskUri.IndexOf(“.”)-8)
$srcStorageAccountObject = Get-AzureRmStorageAccount | Where-Object -FilterScript {$_.StorageAccountName -eq $srcStorageAccount}
$srcStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $srcStorageAccountObject.ResourceGroupName -StorageAccountName $srcStorageAccountObject.StorageAccountName).Key1
$srcContext = New-AzureStorageContext -StorageAccountName $srcStorageAccountObject.StorageAccountName -StorageAccountKey $srcStorageKey
$dstDiskName = $vmOSDisks.Name + $dstDiskDate

# If destination container (virtual machine name) doesn’t exist create new one / ErrorAction set to SilentlyContinue in case of existing container
$Output = New-AzureStorageContainer -Name $vm.Name.ToLower() -Permission Container -Context $dstContext -ErrorAction SilentlyContinue

# Start copy process
$blob = Start-AzureStorageBlobCopy -SrcUri $srcDiskUri -SrcContext $srcContext -DestContainer $vm.Name.ToLower() -DestBlob “$dstDiskName.vhd”.ToLower() -DestContext $dstContext -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Start copy process for OS disk – $($vmOSDisks.Name) – file name: $dstDiskName.vhd”

$CopyStatus[$CopyIndex] = $blob

# Start copy process for DATA disks if there is more than zero
# ————————-
If ($vmDataDisks.Count -gt 0) {
foreach ($dataDisk in $vmDataDisks) {
# Set source storage settings and destination disk name
$srcDiskUri = $dataDisk.Vhd.Uri
$srcStorageAccount = $srcDiskUri.Substring(8, $srcDiskUri.IndexOf(“.”) – 8)
$srcStorageAccountObject = Get-AzureRmStorageAccount | Where-Object -FilterScript {$_.StorageAccountName -eq $srcStorageAccount}
$srcStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $srcStorageAccountObject.ResourceGroupName -StorageAccountName $srcStorageAccountObject.StorageAccountName).Key1
$srcContext = New-AzureStorageContext -StorageAccountName $srcStorageAccountObject.StorageAccountName -StorageAccountKey $srcStorageKey
$dstDiskName = $dataDisk.Name + $dstDiskDate
# Start copy process
$blob = Start-AzureStorageBlobCopy -SrcUri $srcDiskUri -SrcContext $srcContext -DestContainer $vm.Name.ToLower() -DestBlob “$dstDiskName.vhd”.ToLower() -DestContext $dstContext -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Start copy process for DATA disk – $($dataDisk.Name) – file name: $dstDiskName.vhd”

$CopyStatus[$CopyIndex] = $blob
Else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine doesn’t have any DATA disks”

# Check all copies status
# ————————-
for($i=0;$i -lt $CopyIndex; $i++){
$status = $CopyStatus[$i] | Get-AzureStorageBlobCopyState
While($status.Status -eq “Pending”){
Start-Sleep -Seconds 60
$status = $CopyStatus[$i] | Get-AzureStorageBlobCopyState
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – All copies finished”

# Start virtual machine
# ————————-
If ($vmStatus -ne “PowerState/deallocated”) {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine is starting.”
$output = Start-AzureRmVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual is running.”

# Delete old backups on retention policy
# —————————————
$blobs = Get-AzureStorageBlob -Context $dstContext -Container $vm.Name.ToLower()
$backupGUIDs = @()
# Get timestamp which is uniq ID of backup instance
foreach ($blob in $blobs) {
If ($backupGUIDs -notcontains $($blob.Name.Substring($blob.Name.Length-16,12))) {
$backupGUIDs += $blob.Name.Substring($blob.Name.Length-16,12)
# Sort all backup instances
$backupGUIDs = $backupGUIDs | Sort-Object

# If there is more backup instances than specified, remove old one
If ($backupGUIDs.Count -gt $numberOfBackups) {
# Count number of backups to remove
$items = $backupGUIDs.Count – $numberOfBackups
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Number of stored backups to delete – $items”
$backupItemsToRemove = @()
# Get all backup GUIDs (timestamp yyyyMMddHHmm) to remove
for ($i = 0; $i -lt $items; $i++) {
$backupItemsToRemove += $backupGUIDs[$i]
# Remove old backups
foreach ($blob in $blobs) {
if ($backupItemsToRemove -contains $($blob.Name.Substring($blob.Name.Length-16,12))) {
$blob | Remove-AzureStorageBlob -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Deleted old backup – file: $($blob.Name)”
else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Number of stored backups is less than specified – files have not been removed.”
write-output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Found error: $($_.Exception.Message)”
# —————————————————————————-
ElseIf ($VMs -eq 0) {
write-output “Virtual machines with tag BackupGroupName – $BackupGroupName haven’t been defined”
Else {
write-output “Return information are in unknown format.”

2 thoughts on “Azure Automation – backup solution for old operating systems – Windows Server 2003 / 2008

  1. Hi Bartosz Kubiak. Your post is very interesting, but i have a question. Actually, i have 1 Windows Server 2003 vm in the Resource Group Name “PRD”. In this RG I also have VM with Windows Server 2016 that are backed up with azure backups.

    The question I have is whether it is necessary to create another RG and move my virtual machine with windows server 2003 to that new rg so that only that virtual machine is backed up with your script, without affecting the others that are backed up from Azure.

    What do you recommend?


  2. I need a mower for my small yard. I’m reading
    how in order to locate the best cord electric lawn mower on agreenhand.
    Unfortunately I do donrrrt you have a garage, shed, porch, basement,
    actually crawl space I’m able to keep it in and my house has been robbed twice so I understand
    it would be stolen unless I should disable and lock
    it somehow.
    So my two big questions are:
    Would it be possible safe enable keep an electric lawn mower outside with a tarp for cover?

    Would it’s possible to disable and lock a corded garden tractor?

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.