I have a number of DEV/TEST Virtual Machines (VMs) deployed to Azure Regions in Southeast Asia (Singapore) and West US as these were the closet to those of us living in Australia. Now that the new Azure Regions in Australia have been launched, it’s time to start migrating those VMs closer to home. Manually moving VMs between Regions is pretty straight forward and a number of articles already exist outlining the manual steps.

To migrate an Azure VM to another Region

  1. Shutdown the VM in the source Region
  2. Copy the underlying VHDs to storage accounts in the new Region
  3. Create OS and Data disks in the new Region
  4. Re-create the VM in the new Region.

Simple enough but tedious manual configuration, switching between tools and long waits while tens or hundreds of GBs are transferred between Regions.

What’s missing is the automation…

Automating the Migration

In this post I will share a Windows PowerShell script that automates the migration of Azure Virtual Machines between Regions. I have made the full script available via GitHub.

Here is what we are looking to automate:


  1. Shutdown and Export the VM configuration
  2. Setup async copy jobs for all attached disks and wait for them to complete
  3. Restore the VM using the saved configuration.

The Migrate-AzureVM.ps1 script assumes the following:

  • Azure Service Management certificates are installed on the machine running the script for both source and destination Subscriptions (same Subscription for both is allowed)
  • Azure Subscription profiles have been created on the machine running the script. Use Get-AzureSubscription to check.
  • Destination Storage accounts, Cloud Services, VNets etc. already have been created.

The script accepts the following input parameters:

[code language=”powershell” gutter=”false”]
.\Migrate-AzureVM.ps1 -SourceSubscription "MySub" `
-SourceServiceName "MyCloudService" `
-VMName "MyVM" `
-DestSubscription "AnotherSub" `
-DestStorageAccountName "mydeststorage" `
-DestServiceName "MyDestCloudService" `
-DestVNETName "MyRegionalVNet" `
-IsReadOnlySecondary $false `
-Overwrite $false `
-RemoveDestAzureDisk $false

SourceSubscription Name of the source Azure Subscription
SourceServiceName Name of the source Cloud Service
VMName Name of the VM to migrate
DestSubscription Name of the destination Azure Subscription
DestStorageAccountName Name of the destination Storage Account
DestServiceName Name of the destination Cloud Service
DestVNETName Name of the destination VNet – blank if none used
IsReadOnlySecondary Indicates if we are copying from the source storage accounts read-only secondary location
Overwrite Indicates if we are overwriting if the VHD already exists in the destination storage account
RemoveDestAzureDisk Indicates if we remove an Azure Disk if it already exists in the destination disk repository

To ensure that the Virtual Machine configuration is not lost (and avoid us have to re-create by hand) we must first shutdown the VM and export the configuration as shown in the PowerShell snippet below.

[code language=”powershell” gutter=”false”]
# Set source subscription context
Select-AzureSubscription -SubscriptionName $SourceSubscription -Current

# Stop VM
Stop-AzureVMAndWait -ServiceName $SourceServiceName -VMName $VMName

# Export VM config to temporary file
$exportPath = "{0}\{1}-{2}-State.xml" -f $ScriptPath, $SourceServiceName, $VMName
Export-AzureVM -ServiceName $SourceServiceName -Name $VMName -Path $exportPath

Once the VM configuration is safely exported and the machine shutdown we can commence copying the underlying VHDs for the OS and any data disks attached to the VM. We’ll want to queue these up as jobs and kick them off asynchronously as they will take some time to copy across.

[code language=”powershell” gutter=”false”]
Get list of azure disks that are currently attached to the VM
$disks = Get-AzureDisk | ? { $_.AttachedTo.RoleName -eq $VMName }

# Loop through each disk
foreach($disk in $disks)
# Start the async copy of the underlying VHD to
# the corresponding destination storage account
$copyTasks += Copy-AzureDiskAsync -SourceDisk $disk
catch {} # Support for existing VHD in destination storage account

# Monitor async copy tasks and wait for all to complete

Tip: You’ll probably want to run this overnight. If you are copying between Storage Accounts within the same Region copy times can vary between 15 mins and a few hours. It all depends on which storage cluster the accounts reside. Michael Washam provides a good explanation of this and shows how you can check if your accounts live on the same cluster. Between Regions will always take a longer time (and incur data egress charges don’t forget!)… see below for a nice work-around that could save you heaps of time if you happen to be migrating within the same Geo.

You’ll notice the script also supports being re-run as you’ll have times when you can’t leave the script running during the async copy operation. A number of switches are also provided to assist when things might go wrong after the copy has completed.

Now that we have our VHDs in our destination Storage Account we can begin putting our VM back together again.

We start by re-creating the logical OS and Azure Data disks that take a lease on our underlying VHDs. So we don’t get clashes, I use a convention based on Cloud Service name (which must be globally unique), VM name and disk number.

[code language=”powershell” gutter=”false”]
# Set destination subscription context
Select-AzureSubscription -SubscriptionName $DestSubscription -Current

# Load VM config
$vmConfig = Import-AzureVM -Path $exportPath

# Loop through each disk again
$diskNum = 0
foreach($disk in $disks)
# Construct new Azure disk name as [DestServiceName]-[VMName]-[Index]
$destDiskName = "{0}-{1}-{2}" -f $DestServiceName,$VMName,$diskNum

Write-Log "Checking if $destDiskName exists…"

# Check if an Azure Disk already exists in the destination subscription
$azureDisk = Get-AzureDisk -DiskName $destDiskName `
-ErrorAction SilentlyContinue `
-ErrorVariable LastError
if ($azureDisk -ne $null)
Write-Log "$destDiskName already exists"

if ($RemoveDisk -eq $true)
# Remove the disk from the repository
Remove-AzureDisk -DiskName $destDiskName

Write-Log "Removed AzureDisk $destDiskName"
$azureDisk = $null
# else keep the disk and continue

# Determine media location
$container = ($disk.MediaLink.Segments[1]).Replace("/","")
$blobName = $disk.MediaLink.Segments | Where-Object { $_ -like "*.vhd" }
$destMediaLocation = "http://{0}.blob.core.windows.net/{1}/{2}" -f $DestStorageAccountName,$container,$blobName

# Attempt to add the azure OS or data disk
if ($disk.OS -ne $null -and $disk.OS.Length -ne 0)
# OS disk
if ($azureDisk -eq $null)
$azureDisk = Add-AzureDisk -DiskName $destDiskName `
-MediaLocation $destMediaLocation `
-Label $destDiskName `
-OS $disk.OS `
-ErrorAction SilentlyContinue `
-ErrorVariable LastError

# Update VM config
$vmConfig.OSVirtualHardDisk.DiskName = $azureDisk.DiskName
# Data disk
if ($azureDisk -eq $null)
$azureDisk = Add-AzureDisk -DiskName $destDiskName `
-MediaLocation $destMediaLocation `
-Label $destDiskName `
-ErrorAction SilentlyContinue `
-ErrorVariable LastError

# Update VM config
# Match on source disk name and update with dest disk name
$vmConfig.DataVirtualHardDisks.DataVirtualHardDisk | ? { $_.DiskName -eq $disk.DiskName } | ForEach-Object {
$_.DiskName = $azureDisk.DiskName

# Next disk number
$diskNum = $diskNum + 1

[code language=”powershell” gutter=”false”]
# Restore VM
$existingVMs = Get-AzureService -ServiceName $DestServiceName | Get-AzureVM
if ($existingVMs -eq $null -and $DestVNETName.Length -gt 0)
# Restore first VM to the cloud service specifying VNet
$vmConfig | New-AzureVM -ServiceName $DestServiceName -VNetName $DestVNETName -WaitForBoot
# Restore VM to the cloud service
$vmConfig | New-AzureVM -ServiceName $DestServiceName -WaitForBoot

# Startup VM
Start-AzureVMAndWait -ServiceName $DestServiceName -VMName $VMName

For those of you looking at migrating VMs between Regions within the same Geo and have GRS enabled, I have also provided an option to use the secondary storage location of the source storage account.

To support this you will need to enable RA-GRS (read access) and wait a few minutes for access to be made available by the storage service. Copying your VHDs will be very quick (in comparison to egress traffic) as the copy operation will use the secondary copy in the same region as the destination. Nice!

Enabling RA-GRS can be done at any time but you will be charged for a minimum of 30 days at the RA-GRS rate even if you turn it off after the migration.

[code language=”powershell” gutter=”false”]
# Check if we are copying from a RA-GRS secondary storage account
if ($IsReadOnlySecondary -eq $true)
# Append "-secondary" to the media location URI to reference the RA-GRS copy
$sourceUri = $sourceUri.Replace($srcStorageAccount, "$srcStorageAccount-secondary")

Don’t forget to clean up your source Cloud Services and VHDs once you have tested the migrated VMs are running fine so you don’t incur ongoing charges.


In this post I have walked through the main sections of a Windows PowerShell script I have developed that automates the migration of an Azure Virtual Machine to another Azure data centre. The full script has been made available in GitHub. The script also supports a number of other migration scenarios (e.g. cross Subscription, cross Storage Account, etc.) and will be handy addition to your Microsoft Azure DevOps Toolkit.

Azure Infrastructure, Azure Platform, PowerShell
, , , ,

Join the conversation! 11 Comments

  1. did you shift the VHD files via azure site-to-site connection, or across internet? how fast did they transfer?

    • When we setup an async copy job between Regions, the Azure storage service copies the VHD across the internet (storage service to storage service). It does not traverse any VPN connections we might have in place. During my migration, I found that an OS disk took somewhere between 1.5 – 2 hrs to copy from the Singapore DC to the Sydney DC.

  2. Thank you for this post, very handy and relevant I would imagine to a lot of Australian IT folk. We attempted to follow this exactly as described however did get errors thrown up during the process…

    Get-AzureStorageBlobCopyState : The input object cannot be bound because it did not contain the information required to bind all mandatory parameters: ICloudBlob
    At MigrateAzure.ps1:440 char:18
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (Microsoft.Windo…torageContainer:PSObject) [Get-AzureStorageBlobCopyS
    tate], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectMissingMandatory,Microsoft.WindowsAzure.Commands.Storage.Blob.Cmdlet.GetAzure


    WARNING: No deployment found in service: ‘newstorage’.
    New-AzureVM : CurrentStorageAccountName is not accessible. Ensure the current storage account is accessible and in the same location or affinity group as your cloud service.
    At MigrateAzure.ps1:577 char:21
    + $vmConfig | New-AzureVM -ServiceName $DestServiceName -VNetName $DestVNE …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : CloseError: (:) [New-AzureVM], ArgumentException
    + FullyQualifiedErrorId : Microsoft.WindowsAzure.Commands.ServiceManagement.IaaS.PersistentVMs.NewAzureVMCommand

    Perhaps we missed something obvious in the process?

    • I have come across this error before Mustafa. Check your current Azure Subscription profile (Get-AzureSubscription cmdlet) and verify the storage account you have set as the “CurrentStorageAccountName”. Even though you may not be referencing that storage account during the copy, the New-AzureVM cmdlet appears to validate this setting when executing. Try setting this profile property to a valid storage account before running the script.

  3. Thanks for this script! I have a feeling it’s going to come in handy for me 🙂

    Quick question: if I was moving a VM within the same subscription, but to a different date center, would $SourceSubscription and $DestSubscription be the same value?

  4. Have you successfully copied from the Read Access Secondary? I can’t get it to work, and can only seem to copy from secondary using AZCopy.

    The error I get is: WARNING: Ignore mismatch source storage context.. The source uri is https://account-secondary….., the end point is https://account….

  5. FYI

    I had to comment out the part about copying form the secondary site.
    I do not have read-only sencondary storage, but the script thought so, even through I specified -isreadablesecondary $false.

    Also when copying to the same subscription, I had to put in a pause, to enable me to delete the instance from the azure portal, otherwise it would fail to import the machine obviously.
    This would also to allow me to modify the xml config before importing, to enable upgrading or downgrading of the role size.
    (PS. you will have to create a new cloud service if you are moving VM’s in an availability set to a different role size.)

  6. Hello,

    The script is good for “Classic” VM’s, but do you any recommendations for “Resource Manager” or ARM VM’s? The APS Export-AzureVM and Import-AzureVM cmdlets are not available for ARM mode.


Leave a Reply