Post

Adopting AzLocal VMs

Adopting AzLocal VMs

This post is all about taking an existing Hyper-V VM and making it an AzLocal VM - so you can manage it from Azure Local like it’s always belonged there.
It’s not quite a migration (nothing’s being migrated cross-platform), and it’s not a full “hydration” either.
Think of it as adopting a VM into Azure Local - keep your data, get the AzLocal VM benefits.

VMs are VMs… unless they’re running in IaaS … or in a hybrid space where a control plane kinda makes them look like IaaS, but they’re actually just good ol’ fashioned virtualization - kinda.

Confused yet? Yeah. Let’s break this down …

The Basics: What Makes a VM a VM

At their core, all VMs have a few key aspects:

  • Runtime – the VM itself, happily running an OS of your choice, doing whatever job you gave it (serving web pages, crunching numbers, hoarding cat GIFs … no judgment).
  • CRUD operations – your ability to create, read, update, and delete the VM-resources. Think power on/off, add disks, tweak NICs, and so on.
  • Management layer – seeing CPU usage, monitoring health, collecting logs, and all that “are you still alive?” stuff.

Where Azure Local Fits In

With Azure Local, the runtime stays the same - it’s still your VM running on a local hypervisor (“the” local Hyper-V). But the management pieces get enhanced by having them projected into Azure via Azure Arc components, same for the AzLocal part itself (but that’s a different story - more info).

That means your on-prem VM becomes an AzLocal VM - a resource that Azure Local can manage like it was born in the cloud (even if it was very much born in your server rack).

The magic sauce behind this includes:

  • Azure Arc Resource Bridge
  • Custom Location enabled in your subscription
  • Kubernetes extension for VM operators (the on-prem twin of Azure Resource Manager’s VM provider)
  • Arc for Servers agent (aka Azure Connected Machine agent) for guest management

More details here:
Azure Arc VM Management Overview

Once this stack is in place, you can create new Azure Local VMs, ooooor… you can onboard existing Hyper-V VMs through either a migration process or using Azure CLI to “somewhat create” the right resources, in the right order, and turn the unmanaged Hyper-V VM into an AzLocal VM (this is what we’ll do in this article).


When You Already Have a Hyper-V VM

If you’ve got an existing Hyper-V VM - maybe you created it locally, maybe you moved it from another Hyper-V environment - you’ll need to prep it a bit, for it to be an AzLocal VM. The process might look complex, but it’s fairly direct and scriptable (once you understand it):

process overview

Multiple VMs? You’re going to want a script.
Gen1 VMs (like from Azure Stack Hub)? That’s a different beast entirely - we’ll tackle that in a future post (soon™).

For now, let’s walk through a single VM example.


Example VM

We’ve got a Windows Server 2025 VM running IIS, with:

  • 1 OS disk
  • 8 GB RAM
  • 6 vCPUs

vm overview

Running unmanaged on Azure Local host ..E01 and running an important, production type, application:

snake gif

First, you can pull the VM’s details (run this on the host where the VM is running) - these will help shape the decisions later on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$vm = Get-VM -Name "win2025"
$cpu = Get-VMProcessor -VMName $vm.Name
$mem = Get-VMMemory -VMName $vm.Name

# Disks
$rows = @()
$hdds = Get-VMHardDiskDrive -VMName $vm.Name

foreach ($d in $hdds) {
  $vhd = Get-VHD -Path $d.Path
  $diskBase = [System.IO.Path]::GetFileNameWithoutExtension($d.Path)
  $diskExt  = [System.IO.Path]::GetExtension($d.Path)
  $swapName = "${diskBase}-swap${diskExt}"
  $sizeGB   = [int][math]::Ceiling($vhd.Size / 1GB)

  $rows += [pscustomobject]@{
    swapName   = $swapName
    sizeGB     = $sizeGB
    memStartup = [int][math]::Ceiling($mem.Startup / 1GB)
    memDynamic = $mem.DynamicMemoryEnabled
    cpuCount   = $cpu.Count
  }
}

$rows | Format-Table swapName, sizeGB, memStartup, memDynamic, cpuCount -AutoSize

We’ll need this info shortly.


SVCCM - Steps for Virtualizing, Configuring, Connecting, and Managing your AzLocal VM

Create the resources which will be used by your AzLocal VM:

  1. Create vNICs
  2. Create placeholder disks
  3. Swap the placeholders with your real disks
  4. Create the AzLocal VM

Everything is done in Azure, through CLI, but before you start - shut down your source Hyper-V VM. (Yes, even if it’s running Snake. Or especially if it’s running Snake.)

Easiest option is to use Azure CloudShell, but other options will work fine as well. Make sure you are connected to the right subscription and in the right context.


1. Create NIC(s)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$lnetName = "lnetname"
$nicName ="targetVM-NIC1"
$subscription = "subID"
$resource_group = "RGname"
$customLocationID = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.ExtendedLocation/customLocations/<s-cluster-customlocation>"
$location = "AzLocalRegion"

az stack-hci-vm network nic create `
  --subscription $subscription `
  --resource-group $resource_group `
  --custom-location $customLocationID `
  --location $location `
  --name $nicName `
  --subnet-id $lnetName

More info around working with AzLocal NICs.


2. Create a Placeholder OS Disk

This will be a disk, which we will use only temporarily, to swap with our source VM disk. Only the name of the disk will matter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$resourceGroup = "RGname"
$customLocation = "/subscriptions/<SubID>/resourcegroups/<RGname>/providers/microsoft.extendedlocation/customlocations/<s-cluster-customlocation>"
$location = "AzLocalRegion"
$sizeGb = 127
$diskName = "VM1diskOS"
$storagePathId = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.AzureStackHCI/storageContainers/<yourStoragePath>"
$diskFileFormat = "vhdx"

az stack-hci-vm disk create `
  --resource-group $resourceGroup `
  --custom-location $customLocation `
  --location $location `
  --size-gb $sizeGb `
  --name $diskName `
  --storage-path-id $storagePathId `
  --disk-file-format $diskFileFormat

Note: Once created, save the disk’s full ID (/subscriptions/.../virtualHardDisks/<name>). You’ll need it when creating the VM.


3. Swap the Placeholder Disk

Find your placeholder disk in $storagePathId. It’ll be in a GUID-named folder (together with the rest of the disks from the AzLocal VMs created). Replace it with your real disk, keeping the exact same filename (which will be the name you’ve selected, the SubID, and RG name).

placeholder disk swap

You can either rename your source disk and use something like:

1
Move-Item .\pathToSourceDisk .\pathToClusterStorage\placeholderDiskName -Force

Or:

  • Copy your source disk into the folder
  • Rename the placeholder to something else
  • Rename your source disk to match the placeholder’s original name

4. Create the Azure Local VM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$resourceGroup = "RGname"
$customLocation = "/subscriptions/<subID>/resourcegroups/<RGname>/providers/microsoft.extendedlocation/customlocations/<s-cluster-customlocation>"
$location = "AzLocalRegion"
$size = "Default"
$storagePathId = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.AzureStackHCI/storageContainers/<yourStoragePath>"
$vmName = "VMname"
$osDiskName = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.AzureStackHCI/virtualHardDisks/<diskname>"
$osType = "windows"
$nic1 = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.AzureStackHCI/networkInterfaces/<nicName>"

$enableAgent = "false"
$enableVmConfigAgent = "false"
$enableVtpm = "true"
$enableSecureBoot = "true"

az stack-hci-vm create `
  --resource-group $resourceGroup `
  --custom-location $customLocation `
  --location $location `
  --size $size `
  --storage-path-id $storagePathId `
  --name $vmName `
  --os-disk-name $osDiskName `
  --os-type $osType `
  --enable-agent $enableAgent `
  --enable-vm-config-agent $enableVmConfigAgent `
  --enable-vtpm $enableVtpm `
  --enable-secure-boot $enableSecureBoot `
  --nics $nic1

Once accepted, the AzLocal VM creation will take a little time, but you should start seeing the VM in the Azure Local blade, under VMs.

VM creation progress


Wrapping Up

After the creation actions complete, the new AzLocal VM will appear in the Azure Local blade. Once it hits Running, you can enable Guest Management - which involves running a script inside the VM.

guest management script

Wait a few minutes after enabling it, and you’ll have full Azure Local-powered management over your previously “unmanaged/just Hyper-V” VM. Update the IP config, check that you can connect and everything is running as expected and …

AzLocal VM in portal

And that’s it - one small step for you, one giant leap for your hybrid cloud setup. (Also one giant leap in the number of PS and GUIDs in your clipboard.)

The AzLocal VM can now be managed from the Azure Portal, turned on or off, and you can add additional disks from the portal.

portal disk management

And the important production apps can now resume:

apps running


What’s Next

This was the single-VM path - good for learning the ropes and proving it out. Next up (soon(tm)), few more things around:

  • Bulk onboarding – script this out so you can adopt dozens of VMs in one go. (Nobody wants to hand-swap disks forever.)
  • Gen1 VMs – if you’ve got older workloads (Azure Stack Hub, wink wink), the flow is different.
  • Guest management deep dive – enabling extensions, policy, and patching through Azure Local for your onboarded VMs.
  • Disks and networking – working with multiple NICs, data disks, and attaching/detaching storage on the fly.
  • Automation – ARM templates, Bicep, or Terraform to standardize AzLocal VM adoption.

Think of this post as the “hello world” of AzLocal VM adoption.

AzLocal VM adoption illustration

```


This post is licensed under CC BY 4.0 by the author.