Azure VM: Standard to Premium Storage, Unmanaged to Managed

Hello folks and welcome back. I recently had cause to convert the OS disk of an Azure VM from Standard (HDD) to Premium (SSD). This turned out to be a little more interesting than I expected, so I thought I’d blog the process I decided on and for once this will actually be quite a short post as it’s all PowerShell.

Before we get into it, let’s have a look at the existing VM and what we’re looking to achieve:

The VM We’re Starting With

Here are the particulars of our test VM, post change:

  • VM Size:  Standard D1 v2 (1 CPU, 3.5 GB RAM)
  • 128 GB OS disk on standard storage (HDD)
  • 2 x 128 GB data disks on standard storage (HDD)
  • All disks are in the same storage account
  • All disks are unmanaged
  • VM has a dynamic Public IP assigned

Supporting Screenshots


What Are we Looking to Achieve?

By the end of this process, we aim to have the following:

  • The VM size will have been changed to “Standard_D2s_v3” to support premium disks
  • The OS disk will be running on premium storage (changed from standard)
  • The data disks will remain on standard storage
  • The VM storage will be converted from “Unmanaged” to “Managed”
  • VM will retain the same internal and public IP addresses (this will be explained later)

NOTE:  I’ve chosen the VM size “Standard_D2s_v3” as an example, any SKU that supports premium disks will do

The Process

So what steps are involved in getting us to our end goal? Well the first thing I want to call out is that you’ll be deleting and recreating your VM. Now I know this might seem a little heavy handed, but remember a VM in Azure is just an object made up of other objects…and all of the important objects will survive the deletion of the VM and will be re-used 🙂

Let’s clear things up a little by listing all the steps involved, and why they’re required:

  • Download and install AzCopy. We’ll be using this to migrate (copy) our VM OS disk between a standard and premium storage account
  • Set our public IP address “Allocation Method” to “Static” so we can retain it throughout the process.

NOTE:  Dynamically assigned public IP addresses will change when a VM is powered down/deallocated or deleted.

  • Delete our existing VM. We’re going to recreate our VM as a size that supports premium disks otherwise we’re stuck in a kind of chicken and egg situation.

We can’t change our existing VM to a SKU that supports premium disks as our OS disk is HDD and we can’t convert our current OS disk to premium as the VM size doesn’t support it…hence the need to recreate the VM.

  • Create a premium storage account and use AzCopy to copy our OS disk to it from its current home, the standard storage account
  • Create a new VM using the existing OS disk from its new home in the premium storage account, the data disks from their original home in the standard storage account and the existing network adapter object from it’s home in….cyprus? …who knows where NICs live.
  • Stop the new VM and convert its storage from “Unmanaged” to “Managed”
  • Clean up by deleting the two storage accounts we’ve left behind (standard and premium)

The Process in Action

I’ll be doing this entire process from PowerShell as a few of the tasks can’t be carried out within the portal and it saves jumping back and forth between them.

Before you continue, make sure you’re logged into your Azure tenant and have set the correct context (the subscription that contains your VM)

All PowerShell from this point onwards should be run within the same session.

Download and Install AzCopy

As mentioned above, we’ll be using AzCopy to copy our OS disk between storage accounts, so let’s download and install it.

  • Launch an elevated PowerShell ISE console, paste in the code below, edit as required and run it
# Check C:\Temp\ exists and create it if not
if (!(Get-Item -Path "C:\Temp\" -ErrorAction SilentlyContinue)) {
	New-Item -ItemType Directory -Path "C:\Temp\"

# Check if AzCopy is installed, if not, install it
if (!(Get-Item -Path "C:\Program Files (x86)\Microsoft SDKs\Azure\AzCopy\AzCopy.exe" -ErrorAction SilentlyContinue)) {
	Write-Verbose -Message "Downloading and installing AzCopy" -Verbose
	Invoke-WebRequest `
		-OutFile C:\Temp\MicrosoftAzureStorageTools.msi
	msiexec.exe /i C:\Temp\MicrosoftAzureStorageTools.msi /QN

Grab Resource Group and Existing VM

We’ll be making heavy use of our resource group and existing VM as we work through this process, so the code below will grab both objects and store in as variables

  • Paste the following code into your ISE editor, edit the Resource Group and VM Name and run it
# Get resource group. Script assumes all resources are in the same RG
$ResourceGroup = Get-AzureRmResourceGroup -Name "DF-Test-RG"

# Get Current VM Information
$CurrentVMName = "DF-Test-VM"
$CurrentVM = Get-AzureRMVM -Name $CurrentVMName `
-ResourceGroupName $ResourceGroup.ResourceGroupName

Grab Existing Storage Account Information

Later we’ll be copying our OS disk between storage accounts and to do that we’ll need the following information:

  • Path to the existing VM OS disk
  • Name of the existing VM OS disk
  • Standard storage account access key
  • With that in mind, paste the following code into your ISE editor and run it

NOTE:  If the version of Azure Module you’re running is lower than 1.4, follow the instructions in the code and swap out the command that grabs the storage account key

# Get current VM storage account specifics
$CurrentSAName = ($CurrentVM.StorageProfile.OsDisk.Vhd.Uri).Split('.')[0].Replace('https://', '')

$CurrentSAKey = (Get-AzureRmStorageAccountKey `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	-AccountName $CurrentSAName).Value[0]

<# If you're using a version of Azure PowerShell lower than 1.4, swap out the above command with the following: $CurrentSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroup.ResourceGroupName -AccountName $CurrentSAName).Key1 #>

$SourceVHDPath = ($CurrentVM.StorageProfile.OsDisk.Vhd.Uri).Substring(0, ($CurrentVM.StorageProfile.OsDisk.Vhd.Uri).lastIndexOf('/'))
$VHDName = ($CurrentVM.StorageProfile.OsDisk.Vhd.Uri).Split('/')[4]

Create a Premium Storage Account

One of the main points of this process it to make sure our VM OS disk is being served from SSD and not HDD, before we can make this happen, we’ll need to create a premium storage account. The code below will do the following:

  • Create a premium storage account
  • Create a container in the new storage account called “vhds”
  • Grab an access key to our new storage account
  • Grab the destination path we’ll be copying our existing OS disk to
  • Paste the following code into your ISE editor and run it

NOTE:  If the version of Azure Module you’re running is lower than 1.4, follow the instructions in the code and swap out the command that grabs the storage account key

# Create a new premium storage account with a container named 'vhds' and grab the first key
$PremiumSA = New-AzureRmStorageAccount `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	-AccountName ($ResourceGroup.ResourceGroupName.replace('-', '').ToLower() + "temppremiumsa") `
	-Location $ResourceGroup.Location -Type "Premium_LRS"

$PremiumSAContainer = New-AzureStorageContainer `
	-Name "vhds" `
	-Permission Off `
	-Context $PremiumSA.Context

$PremiumSAKey = (Get-AzureRmStorageAccountKey `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	-AccountName $PremiumSA.StorageAccountName).Value[0]

$DestinationVHDPath = $PremiumSAContainer.CloudBlobContainer.Uri.AbsoluteUri

<# If you're using a version of Azure PowerShell lower than 1.4, swap out the above command with the following: $PremiumSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroup.ResourceGroupName -AccountName $PremiumSA.StorageAccountName).Key1 #>

Change Public IP Allocation Method

As the public IP address assigned for our VMs use has a “Dynamic” allocation, it won’t survive the process we’re about to kick off. However, by changing the allocation method to “Static” we can make sure it does.

NOTE:  Each subscription by default can have a maximum of 5 statically assigned public IP addresses. This number can be increased by contacting Microsoft support.

  • Paste the following code into your ISE editor and run it
# Used when grabbing the existing NICs public IP address below
$VMNic = Get-AzureRmNetworkInterface `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	| Where-Object Name -Like $CurrentVMName*

# Set VM public IP address to static, if not already
# Cannot be changed back to dynamic without powering down the VM, which would totally defeat the purpose
$PublicIP = Get-AzureRmPublicIpAddress `
	-Name ($VMNic.IpConfigurations[0].PublicIpAddress.Id).Split('/')[8] `
	-ResourceGroupName $ResourceGroup.ResourceGroupName

if ($PublicIP.PublicIpAllocationMethod -eq "Dynamic") {
	$PublicIP.PublicIpAllocationMethod = "Static"
	Set-AzureRmPublicIpAddress -PublicIpAddress $PublicIP

Stop and Delete Existing VM

Now this task seems like the scary one, but I promise you it’s not. Everything that makes our VM our VM is contained within the disks and network adapter, all of which will survive the process.

NOTE:  Before running the code below, take a note of any VM extensions installed on the VM. Although these will technically still be running on the new VM we’re creating it, they will likely need to be redeployed to work correctly as Azure won’t be aware of them (as it didn’t install them).

You can check for existing VM extensions by echoing the contents of $CurrentVM


With that done, let’s stop our VM and delete it

  • Paste the following code into your ISE editor and run it (you can add -Force to the end of both commands to bypass confirmation)
# Stop and Remove existing VM keeping disks and NIC
Stop-AzureRmVM -Name $CurrentVMName `
	-ResourceGroupName $ResourceGroup.ResourceGroupName
Remove-AzureRmVM -Name $CurrentVMName `
-ResourceGroupName $ResourceGroup.ResourceGroupName

Grab VM Network Adapter

We’ll be using the existing network adapter in the creation of our new VM, this way we’ll retain the original IP configuration

  • Paste the following code into your ISE editor and run it
# Store VM NIC for creation of new VM (overwriting object in existing variable as it now won't contain a reference to the deleted VM)
$VMNic = Get-AzureRmNetworkInterface `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
| Where-Object Name -Like $CurrentVMName*

Copy VM OS disk Between Storage Accounts

One of our desired outcomes here is to have our OS disk being served from premium  storage (SSD), so let’s go ahead and copy it to the premium storage account we deployed earlier

  • Paste the following code into your ISE editor and run it
# Copy VM OS disk to premium storage
& 'C:\Program Files (x86)\Microsoft SDKs\Azure\AzCopy\AzCopy.exe' /Source:$SourceVHDPath /Dest:$DestinationVHDPath /SourceKey:$CurrentSAKey /DestKey:$PremiumSAKey /Pattern:$VHDName

Deploy Our New VM

Now that our OS disk has been copied to premium storage, we can go ahead and create our new VM. The following code creates a VM config object made up of our existing OS disk, data disks and network adapter. It then creates a new VM using that object. 

The code is written to handle two data disks, if your VM has only one or even none, comment out the relevant code blocks.

  • Paste the following code into your ISE editor and run it (modify the VM SKU ($NewVMSize) as desired, just make sure it’s able to support premium disks)
# Create new VM using existing disks (assumes 2 data disks) and NIC
$NewVMSize = "Standard_D2s_v3"

$NewVM = New-AzureRmVMConfig `
	-VMName $CurrentVMName `
	-VMSize $NewVMSize

$NewVM = Add-AzureRmVMNetworkInterface `
	-VM $NewVM `
	-Id $VMNic.Id

$NewVM = Set-AzureRmVMOSDisk `
	-VM $NewVM `
	-Name $VHDName.Replace('.vhd', '') `
	-VhdUri ($DestinationVHDPath + '/' + $VHDName) `
	-Caching ReadWrite `
	-CreateOption Attach `

### Comment out the block of code if VM has no data disks
$NewVM = Add-AzureRmVMDataDisk `
	-VM $NewVM `
	-Name $CurrentVM.StorageProfile.DataDisks[0].Name `
	-VhdUri $CurrentVM.StorageProfile.DataDisks[0].Vhd.Uri `
	-Caching ReadOnly `
	-DiskSizeInGB $null `
	-Lun 0 `
	-CreateOption Attach

### Comment out this block of code if VM has 0 or only 1 data disk
$NewVM = Add-AzureRmVMDataDisk `
	-VM $NewVM `
	-Name $CurrentVM.StorageProfile.DataDisks[1].Name `
	-VhdUri $CurrentVM.StorageProfile.DataDisks[1].Vhd.Uri `
	-Caching ReadOnly `
	-DiskSizeInGB $null `
	-Lun 1 `
	-CreateOption Attach

# Create new VM using the above config
New-AzureRmVM -ResourceGroupName $ResourceGroup.ResourceGroupName `
	-Location $ResourceGroup.Location `
	-VM $NewVM `

Remove BGInfo VM Extension

While deploying my new VM, it decided to install the “BGInfo” VM extension too…cheeky little bugger. Now BGInfo is something I tend to use anyway depending on the purpose of the VM but I thought it would be good to see exactly how easy/difficult it is to remove it using PowerShell…turns out it’s pretty easy.

  • Paste the following code into your ISE editor and run it
### Uninstall BGInfo VM Extension
Get-AzureRmVMExtension -Name "BGInfo" `
	-VMName $CurrentVMName `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	| Remove-AzureRmVMExtension `

Convert VM Storage From Unmanaged to Managed

The last remaining task (other than checking everything worked as planned) is to convert our VM storage to “Managed”, this is a fairly straightforward task but does require that we power down the VM one last time.

  • Paste the following code into your ISE editor and run it

NOTE:  The “ConvertTo-AzureRmVMManagedDisk” cmdlet will power up the VM again when it’s finished…how helpful 🙂

# Change VM disks from unmanaged to managed
Stop-AzureRmVM -Name $CurrentVMName `
	-ResourceGroupName $ResourceGroup.ResourceGroupName

# It may be easier to run this command from the Azure Shell...depends on the version of the AzureRM module you're running
# If running in the Azure shell, make sure to substitute the variables for their static values before running
ConvertTo-AzureRmVMManagedDisk `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
-VMName $CurrentVMName

Check Our Working

So…all going well, we should have a VM set up as follows:

  • Our VM should now support premium disks
  • Our OS disk should be running on SSD
  • Our data disks should still be running on HDD
  • Our VM storage should be “Managed”, changed from “Unmanaged”
  • Our public IP address should be the same
  • Our internal IP address should be the same

Here are some supporting screenshots to confirm:


NOTE:  The “Computer name” field in the Azure portal is now blank. This is because during the VM deployment, the “OSProfile” settings are blank. This is a limitation of Azure and has been acknowledged by Microsoft. More of a pain in the hoop than an actual issue. If someone has found a way round this, if it even exists, please post a comment as this chaffs my OCD 🙂


When we look in the Resource Group that holds our VM, we’ll see 3 disk objects that weren’t there before. This is because they’re now managed, so we see the disks instead of the storage accounts.


So it looks like everything has worked exactly as we’d planned…nice. Now all that’s left to do is tidy up after ourselves a little.

Clean Up

Now that our VM disks are managed, we’ve got no need for the two storage accounts that have been left behind. These accounts still hold a copy of our disks, so if for no other reason, we’ll want to delete them to save us paying for them twice 🙂

  • Paste the following code into your ISE editor and run it
### Tidy up by deleting left over storage accounts
Remove-AzureRmStorageAccount `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
	-AccountName $CurrentSAName
Remove-AzureRmStorageAccount `
	-ResourceGroupName $ResourceGroup.ResourceGroupName `
-AccountName $PremiumSA.StorageAccountName

That’s it folks, I hope you found this guide helpful, if not, then as always, it’ll be my brain dump 🙂

Hope to see you in the next one.

4 Replies to “Azure VM: Standard to Premium Storage, Unmanaged to Managed”

  1. Hello thank you for this great discription.
    why do you assign –> $CurrentSA = Get-AzureRmStorageAccount `
    -ResourceGroupName $ResourceGroup.ResourceGroupName `
    -Name $CurrentSAName
    It is never used.
    Best regards

    1. Hi Philip,

      Thanks for pointing that out. It’s likely I WAS using it at some point when writing the code and just neglected to remove it. Must have been before I made the move to VSCode 😛
      Page updated 🙂

  2. Hi DAVID,

    Excellent Code, Actually i was searching for code or procedure for migration standard HDD to Premium on production, Your code help me lots i tried and executed first in lab environment, it went successful., Your code is Awesome thanks for sharing on net.. appreciate your powershell knowledge. thanks again. 🙂

Leave a Reply

Your email address will not be published.

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