Moving VHDs from one Storage Account to Another (Part 2) - Updated 2017 08 18


This article will show how to automatically copy VHDs from a source storage account to a new one, without hardcoding values. Secondly how to create a new VM with the disks in the new Storage Account, reusing the same value of the original VM. The first thing is to create a PowerShell module file where keeps all functions that will be invoked by the main script.

Ideally, this module could be reused for other purposes and new functions should be added according to your needs.

Open your preferred PowerShell editor and creates a new file called "Module-Azure.ps1"

Note: all function will be declared as global in order to be available to others script

The first function to be added is called Connect-Azure and it will simplify Azure connection activities.

[powershell] function global:Connect-Azure { Login-AzureRmAccount $global:subName = (Get-AzureRmSubscription | select SubscriptionName | Out-GridView -Title "Select a subscription" -OutputMode Single).SubscriptionName Select-AzureRmSubscription -SubscriptionName $subName } [/powershell]

Above function, using Out-GridView cmdlets, will show all Azure subscriptions associated with your account and allow you to select the one against which execute script

The second function to be added is called CopyVHDs. It will take care of copy all VHDs from the selected source Storage Account to the selected destination Storage Account


function global:CopyVHDs { param ( $sourceSAItem, $destinationSAItem


$sourceSA = Get-AzureRmStorageAccount -ResourceGroupName $sourceSAItem.ResourceGroupName -Name $sourceSAItem.StorageAccountName

$sourceSAContainerName = "vhds"

$sourceSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $sourceSAItem.ResourceGroupName -Name $sourceSAItem.StorageAccountName)[0].Value

$sourceSAContext = New-AzureStorageContext -StorageAccountName $sourceSAItem.StorageAccountName -StorageAccountKey $sourceSAKey

$blobItems = Get-AzureStorageBlob -Context $sourceSAContext -Container $sourceSAContainerName

$destinationSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $destinationSAItem.ResourceGroupName -Name $destinationSAItem.StorageAccountName)[0].Value

$destinationContainerName = "vhds"

$destinationSAContext = New-AzureStorageContext -StorageAccountName $destinationSAItem.StorageAccountName -StorageAccountKey $destinationSAKey

foreach ( $blobItem in $blobItems) {

# Copy the blob Write-Host "Copying " $blobItem.Name " from " $sourceSAItem.StorageAccountName " to " $destinationSAItem.StorageAccountName

$blobCopy = Start-AzureStorageBlobCopy -DestContainer $destinationContainerName -DestContext $destinationSAContext -SrcBlob $blobItem.Name -Context $sourceSAContext -SrcContainer $sourceSAContainerName

$blobCopyStatus = Get-AzureStorageBlob -Blob $blobItem.Name -Container $destinationContainerName -Context $destinationSAContext | Get-AzureStorageBlobCopyState

[int] $i = 0;

while ( $blobCopyStatus.Status -ne "Success") { Start-Sleep -Seconds 180

$i = $i + 1

$blobCopyStatus = Get-AzureStorageBlob -Blob $blobItem.Name -Container $destinationContainerName -Context $destinationSAContext | Get-AzureStorageBlobCopyState

Write-Host "Blob copy status is " $blobCopyStatus.Status Write-Host "Bytes Copied: " $blobCopyStatus.BytesCopied Write-Host "Total Bytes: " $blobCopyStatus.TotalBytes

Write-Host "Cycle Number $i" }

Write-Host "Blob " $blobItem.Name " copied"


return $true }



This function is basically executing the same commands that were showed in the first article. Of course the difference is the it takes as input two objects which contains required information to copy VHDs between the two Storage Account. A couple of notes:

  • Because it is unknown how many VHDs should be copied, there is foreach that will iterate over all VHDs that will copied
  • In order to minimize any side effects, aforementioned for each contains a while that will ensure that copy activity is really completed before return control

The third function to be added is called Create-AzureVMFromVHDs. It will take care of create a new VM using existing VHDs. In order to provide a PoC about what could be achieved, following assumptions have been made:

  • New VM will be deployed in an existing vnet / subnet
  • New VM will have the same size of the original VM
  • New VM will be deployed in a new Resource Group
  • New VM will be deployed in the same location of the (destination) Azure Storage Account where VHDs have been copied
  • New VM will have the same credentials of the source one
  • New VM will have assigned a new dynamic public IP
  • All VHDs copied from source Storage Account (which were attached to the source VM) will be attached to the new VM

[powershell] function global:Create-AzureVMFromVHDs { param ( $destinationVNETItem, $destinationSubnetItem, $destinationSAItem, $sourceVMItem )

$destinationSA = Get-AzureRmStorageAccount -Name $destinationSAItem.StorageAccountName -ResourceGroupName $destinationSAItem.ResourceGroupName

$Location = $destinationSA.PrimaryLocation

$destinationVMItem = '' | select name,ResourceGroupName

$ = ($sourceVMItem.Name + "02").ToLower()

$destinationVMItem.ResourceGroupName = ($sourceVMItem.ResourceGroupName + "02").ToLower()

$InterfaceName = $ + "-nic"

$destinationResourceGroup = New-AzureRmResourceGroup -location $Location -Name $destinationVMItem.ResourceGroupName

$sourceVM = get-azurermvm -Name $sourceVMItem.Name -ResourceGroupName $sourceVMItem.ResourceGroupName

$VMSize = $sourceVM.HardwareProfile.VmSize

$sourceVHDs = $sourceVM.StorageProfile.DataDisks

$OSDiskName = $sourceVM.StorageProfile.OsDisk.Name

$publicIPName = $ + "-pip"

$sourceVMOSDiskUri = $sourceVM.StorageProfile.OsDisk.Vhd.Uri

$OSDiskUri = $sourceVMOSDiskUri.Replace($sourceSAItem.StorageAccountName,$destinationSAItem.StorageAccountName)

# Network Script $VNet = Get-AzureRMVirtualNetwork -Name $destinationVNETItem.Name -ResourceGroupName $destinationVNETItem.ResourceGroupName $Subnet = Get-AzureRMVirtualNetworkSubnetConfig -Name $destinationSubnetItem.Name -VirtualNetwork $VNet

#Public IP script $publicIP = New-AzureRmPublicIpAddress -Name $publicIPName -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $location -AllocationMethod Dynamic

# Create the Interface $Interface = New-AzureRMNetworkInterface -Name $InterfaceName -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $Location -SubnetId $Subnet.Id -PublicIpAddressId $publicIP.Id

#Compute script $VirtualMachine = New-AzureRMVMConfig -VMName $ -VMSize $VMSize

$VirtualMachine = Add-AzureRMVMNetworkInterface -VM $VirtualMachine -Id $Interface.Id $VirtualMachine = Set-AzureRMVMOSDisk -VM $VirtualMachine -Name $OSDiskName -VhdUri $OSDiskUri -CreateOption Attach -Windows

$VirtualMachine = Set-AzureRmVMBootDiagnostics -VM $VirtualMachine -Disable

#Adding Data disk

if ( $sourceVHDs.Length -gt 0) { Write-Host "Found Data disks"

foreach ($sourceVHD in $sourceVHDs) { $destinationDataDiskUri = ($sourceVHD.Vhd.Uri).Replace($sourceSAItem.StorageAccountName,$destinationSAItem.StorageAccountName)

$VirtualMachine = Add-AzureRmVMDataDisk -VM $VirtualMachine -Name $sourceVHD.Name -VhdUri $destinationDataDiskUri -Lun $sourceVHD.Lun -Caching $sourceVHD.Caching -CreateOption Attach


} else { Write-Host "No Data disk found" }

# Create the VM in Azure New-AzureRMVM -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $Location -VM $VirtualMachine

Write-Host "VM created. Well Done !!"



A couple of note:

  • The URI of the VHDs copied in the destination Storage Account has been calculated replacing the source Storage Account name with destination Storage Account name in URI
  • destination VHDs will be attached in the same order (LUN) of source VHDs

Module-Azure.ps1 should have a structure like this:

Now it's time to create another file called Move-VM.ps1 which should be stored in the same folder of Module-Azure

Note: if you want to store in a different folder, then update line 7

Paste following code:

[powershell] $ScriptDir = $PSScriptRoot

Write-Host "Current script directory is $ScriptDir"

Set-Location -Path $ScriptDir



$vmItem = Get-AzureRmVM | select ResourceGroupName,Name | Out-GridView -Title "Select VM" -OutputMode Single

$sourceSAItem = Get-AzureRmStorageAccount | select StorageAccountName,ResourceGroupName | Out-GridView -Title "Select Source Storage Account" -OutputMode Single

$destinationSAItem = Get-AzureRmStorageAccount | select StorageAccountName,ResourceGroupName | Out-GridView -Title "Select Destination Storage Account" -OutputMode Single

# Stop VM

Write-Host "Stopping VM " $vmItem.Name

get-azurermvm -name $vmItem.Name -ResourceGroupName $vmItem.ResourceGroupName | stop-azurermvm

Write-Host "Stopped VM " $vmItem.Name

CopyVHDs -sourceSAItem $sourceSAItem -destinationSAItem $destinationSAItem

$destinationVNETItem = Get-AzureRmVirtualNetwork | select Name,ResourceGroupName | Out-GridView -Title "Select Destination VNET" -OutputMode Single

$destinationVNET = Get-AzureRmVirtualNetwork -Name $destinationVNETItem.Name -ResourceGroupName $destinationVNETItem.ResourceGroupName

$destinationSubnetItem = Get-AzureRmVirtualNetworkSubnetConfig -VirtualNetwork $destinationVNET | select Name,AddressPrefix | Out-GridView -Title "Select Destination Subnet" -OutputMode Single

Create-AzureVMFromVHDs -destinationVNETItem $destinationVNETItem -destinationSubnetItem $destinationSubnetItem -destinationSAItem $destinationSAItem -sourceVMItem $vmItem



  • Line 7: Module-Azure function is invoked
  • Line 9: Connect-Azure function (declared in Module-Azure) is invoked. This is possible because it has been declared as global
  • From Line 11 to Line 15: a subset of source VM, source Storage Account and destination Storage Account info are retrieved. They will be used later
  • Line 19-23: source VM is stopped
  • Line 25: Copy-VHDs function (declared in Module-Azure) is invoked. This is possible because it has been declared as global. Note that we're just passing three previously retrieved parameters
  • From Line 27 to Line 31: VNET and subnet where new VM will be attached are retrieved
  • Line 33:  Create-AzureVMFromVHDs function (declared in Module-Azure) is invoked. This is possible because it has been declared as global. Note that we're just passing already retrieved parameters

Following screenshots shows an execution of Move-VM script:

Select Azure subscription

Select source VM

Select source Storage Account

Select Destination Storage Account

Confirm to stop VM

Select destination VNET

Select destination Subnet

Output sample #1

Output sample #2

Source VM Resource Group

Destination VM RG

Destination Storage Account RG

Source VHDs

Destination VHDs

Thanks for your patience.  Any feedback is  appreciated

Note: Above script has been tested with Azure PS 3.7.0 (March 2017).

Starting from Azure PS 4.x, this cmdlets returns an array of objects with the following properties: Name, Id, TenantId and State.

The function Connect-Azure is using the value SubscriptionName that is no more available. This is the reason why some people saw an empty Window.

Connect-Azure function should be modified as follows to work with Azure PS 4.x:


function global:Connect-Azure { Login-AzureRmAccount

$global:subName = (Get-AzureRmSubscription | select Name | Out-GridView -Title "Select a subscription" -OutputMode Single).Name

Select-AzureRmSubscription -SubscriptionName $subName }