Azure VM Restore from Backup – Part 1: Automated Restore with PowerShell
This article is Part 1 of a two-part series on automating Azure VM restore from Recovery Services Vault (RSV) backups, enabling Business Continuity/Disaster Recovery (BCP/DR) across regions. Here, we focus on the main restore process using the restorevms.ps1
script. In Part 2, we cover NIC re-creation and IP assignment for a true like-for-like DR.
Overview
- Goal: Restore VMs from backup in one Azure region (e.g., East US) to another (e.g., West US) for BCP/DR.
- Approach: Use PowerShell and Azure CLI to automate finding the latest restore point and restoring the VM, including all required network and resource group settings.
- Why: This approach ensures your DR VM is as close as possible to the original, with minimal manual effort.
The Script: restorevms.ps1
Below is the full script used to automate the restore process:
# Boolean variable to indicate whether to restore disks only
# If it is True then VM will also be created so be careful
param (
[string] $restorediskonly = "false",
[int] $numberofhours2wait = 2,
[string] $vmlist = './scripts/bcpdr/vm/vmlist.json'
)
########################################################
# Install Azure CLI manually if not installed already
# curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
########################################################
function Get-Latest-Recovery-Point {
param (
[string]$vmname,
[string]$vmcontainername,
[string]$vaultname,
[string]$vaultresourcegroupname
)
$latest_recovery_point = az backup recoverypoint list `
--container-name $vmcontainername `
--backup-management-type AzureIaasVM `
--item-name $vmname `
--resource-group $vaultresourcegroupname `
--vault-name $vaultname `
--query "[0]" `
--use-secondary-region `
--output json `
| ConvertFrom-Json
| Select-Object -ExpandProperty name
return $latest_recovery_point
}
function Restore-Disks {
param (
[string]$vmname,
[string]$vmcontainername,
[string]$vaultname,
[string]$vaulresourcegrouptname,
[string]$storageaccountname,
[string]$recoverypointname,
[string]$targetresourcegroupname,
[string]$targetvmname = '',
[string]$targetvnetname = '',
[string]$targetvnetresourcegroup = '',
[string]$targetsubnetname = '',
[bool]$restoretodisk = $false
)
if ($restoretodisk) {
$restore_operation = az backup restore restore-disks `
--resource-group $vaulresourcegrouptname `
--vault-name $vaultname `
--restore-mode AlternateLocation `
--container-name $vmcontainername `
--item-name $vmname `
--storage-account $storageaccountname `
--rp-name $recoverypointname `
--target-resource-group $targetresourcegroupname `
--restore-to-staging-storage-account $true `
--use-secondary-region
}
else {
$restore_operation = az backup restore restore-disks `
--resource-group $vaulresourcegrouptname `
--vault-name $vaultname `
--container-name $vmcontainername `
--item-name $vmname `
--storage-account $storageaccountname `
--rp-name $recoverypointname `
--target-resource-group $targetresourcegroupname `
--target-vm-name $targetvmname `
--target-vnet-name $targetvnetname `
--target-vnet-resource-group $targetvnetresourcegroup `
--target-subnet-name $targetsubnetname `
--use-secondary-region
}
return $restore_operation
}
function Invoke-AzureCLICommand {
param (
[string]$command,
[string]$message = ""
)
if (-not [string]::IsNullOrEmpty($message)) {
Write-CustomLog ("About to run message {0}" -f $message)
Write-CustomLog ("About to run command {0}" -f $command)
}
$executionResult = Invoke-Expression $command
return $executionResult
}
# logging function which should work on both local dev container and Git Hub action
function Write-CustomLog {
param (
[string]$message
)
Write-Host "::notice::$message"
}
$booleanRestorediskonly = $false
switch ($restorediskonly.ToLower()) {
"true" {
$restorediskonly = [bool]$true
$booleanRestorediskonly = $true
}
"false" {
$restorediskonly = [bool]$false
$booleanRestorediskonly = $false
}
default {
throw "Invalid value for restorediskonly: $restorediskonly. Must be 'true' or 'false'."
}
}
# Log values with type information
Write-CustomLog ("BooleanRestorediskonly variable value: $booleanRestorediskonly (Type: $($booleanRestorediskonly.GetType().Name))")
Write-CustomLog ("Numberofhours2wait variable value: $numberofhours2wait (Type: $($numberofhours2wait.GetType().Name))")
# Use the boolean variable in further logic
if ($booleanRestorediskonly) {
Write-CustomLog "Restoring disk only."
} else {
Write-CustomLog "Performing full restore."
}
############################################################
# Variables, note the staging SA we created in earler steps
############################################################
$gdep_rsv_vault_name = "rsv-prod-eus-01"
$gdep_rsv_vault_resource_group = "rg-gdep-peus-backup"
$gdep_staging_storage_account = az storage account show --name storegdeppwusstaging --query 'id' --output tsv
Write-CustomLog ("Staging storage account is {0}" -f $gdep_staging_storage_account)
<#
gdep_virtual_machine = Specifies the name of the backed-up VM item to restore.
gdep_virtual_machine_containername = Specifies the name of the container within the vault that holds the backed-up VM
gdep_target_resource_group_name = Specifies the resource group where the restored VM will be created.
gdep_target_vm_name = Specifies the name to assign to the newly restored VM
gdep_target_vnet_name = Specifies the name of the virtual network (VNet) to which the restored VM will be connected
gdep_target_vnet_resource_group = Specifies the resource group name of the VNet where the restored VM will be connected
gdep_target_subnet_name = Specifies the name of the subnet within the target VNet where the restored VM will be placed
vaultname = Specifies the name of the Recovery Services vault from which the backup will be restored
vaulresourcegrouptname = Specifies the resource group name where the vault is located.
storageaccountname = Specifies the name of the storage account where the VM disks will be restored.
recoverypointname = Specifies the name of the recovery point to restore from.
gdep_virtual_machine_nic_name = Name of the NIC resource
gdep_virtual_machine_nic_ip = Final IP desired for this NIC
gdep_virtual_machine_nic_subnet_resource_group =
gdep_virtual_machine_nic_vnet_name =
gdep_virtual_machine_nic_snet_name =
#>
$vm_list = Get-Content -Path $vmlist | ConvertFrom-Json
# Start of script
# Lets add restore Job related properties to keep track of each job
foreach ($gdepvm in $vm_list) {
$gdepvm | Add-Member -MemberType NoteProperty -Name "gdep_restore_job_name" -Value ""
$gdepvm | Add-Member -MemberType NoteProperty -Name "gdep_restore_job_status" -Value "Not Started"
$gdepvm | Add-Member -MemberType NoteProperty -Name "gdep_nic_operation_completed" -Value $false
}
foreach ($gdepvm in $vm_list) {
try {
# Extract parameters from the hashtable
$gdep_virtual_machine = $gdepvm.gdep_virtual_machine
$gdep_virtual_machine_container_name = $gdepvm.gdep_virtual_machine_containername
$gdep_target_resource_group_name = $gdepvm.gdep_target_resource_group_name
$gdep_target_vm_name = $gdepvm.gdep_target_vm_name
$gdep_target_vnet_name = $gdepvm.gdep_target_vnet_name
$gdep_target_vnet_resource_group = $gdepvm.gdep_target_vnet_resource_group
$gdep_target_subnet_name = $gdepvm.gdep_target_subnet_name
Write-CustomLog ("About to obtain recovery point for VM {0}" -f $gdep_virtual_machine)
$latest_recovery_point = Get-Latest-Recovery-Point `
-vmname $gdep_virtual_machine `
-vmcontainername $gdep_virtual_machine_container_name `
-vaultname $gdep_rsv_vault_name `
-vaultresourcegroupname $gdep_rsv_vault_resource_group
if ($booleanRestorediskonly) {
Write-CustomLog ("Sould not have come into this loop {0}" -f $gdep_virtual_machine)
if (-not [string]::IsNullOrEmpty($latest_recovery_point)) {
$restore_operation = Restore-Disks `
-vmname $gdep_virtual_machine `
-vmcontainername $gdep_virtual_machine_container_name `
-vaultname $gdep_rsv_vault_name `
-vaulresourcegrouptname $gdep_rsv_vault_resource_group `
-storageaccountname $gdep_staging_storage_account `
-recoverypointname $latest_recovery_point `
-targetresourcegroupname $gdep_target_resource_group_name `
-restoretodisk $true
# ($restore_operation | ConvertFrom-Json).properties.activityId
$gdepvm.gdep_restore_job_name = ($restore_operation | ConvertFrom-Json).name
Write-CustomLog ("Working on Restoring Disk for job name {0}" -f $gdepvm.gdep_restore_job_name)
}
else {
Write-CustomLog ("No disk(s) recovery point found for VM specfified {0}" -f $gdep_virtual_machine)
}
}
else {
if (-not [string]::IsNullOrEmpty($latest_recovery_point)) {
Write-CustomLog ("This is the correct condition for VM {0}" -f $gdep_virtual_machine)
$restore_operation = Restore-Disks `
-vmname $gdep_virtual_machine `
-vmcontainername $gdep_virtual_machine_container_name `
-vaultname $gdep_rsv_vault_name `
-vaulresourcegrouptname $gdep_rsv_vault_resource_group `
-storageaccountname $gdep_staging_storage_account `
-recoverypointname $latest_recovery_point `
-targetresourcegroupname $gdep_target_resource_group_name `
-targetvmname $gdep_target_vm_name `
-targetvnetname $gdep_target_vnet_name `
-targetvnetresourcegroup $gdep_target_vnet_resource_group `
-targetsubnetname $gdep_target_subnet_name `
-restoretodisk $false
$gdepvm.gdep_restore_job_name = ($restore_operation | ConvertFrom-Json).name
Write-CustomLog ("Working on Restoring Virtual Machine for job name {0}" -f $gdepvm.gdep_restore_job_name)
}
else {
Write-CustomLog ("No virtual machine recovery point found for VM {0}" -f $gdep_virtual_machine)
}
}
}
catch {
Write-Error "An error occurred: $_"
}
}
Step-by-Step Explanation
1. Parameters and Setup
restorediskonly
: Iftrue
, only disks are restored; iffalse
, a full VM is created.numberofhours2wait
: How long to wait for restore jobs (not used directly in this script, but can be used for polling/wait logic).vmlist
: Path to the JSON file listing VMs to restore (see example below).
2. Helper Functions
- Get-Latest-Recovery-Point: Finds the most recent backup recovery point for a VM in the secondary region.
- Restore-Disks: Runs the Azure CLI command to restore either disks or a full VM, depending on parameters.
- Invoke-AzureCLICommand: Utility to run arbitrary Azure CLI commands and log them.
- Write-CustomLog: Standardized logging for both local and CI environments.
3. Main Logic
- Loads the VM list from the provided JSON file.
- For each VM, finds the latest recovery point and triggers a restore (either disk or full VM).
- Tracks job names and statuses for each VM.
4. VM List JSON Example
The script expects a JSON file like this:
[
{
"gdep_virtual_machine": "MDCAVDPRDAE02",
"gdep_virtual_machine_containername": "MDCAVDPRDAE02",
"gdep_target_resource_group_name": "dr-rg-gdep-pwus-infrastructure",
"gdep_target_vm_name": "DRMDCAVDPRDAE02",
"gdep_target_vnet_name": "vnet-gdep-pwus-management",
"gdep_target_vnet_resource_group": "dr-rg-gdep-pwus-vnets",
"gdep_target_subnet_name": "snet-gdep-pwus-management-restore"
// ...other NIC properties...
}
]
Summary
- This script automates the restore of VMs from Azure RSV backups to a new region.
- It ensures you always restore from the latest available backup.
- For full DR, combine with the NIC re-creation process in Part 2.
Continue to Part 2: NIC Re-Creation and IP Assignment