ClusterTriage / Blog / Storage

Aligning VMs With Their Storage: Building on Darryl van der Peijl's Script

Years ago a former colleague of mine, Darryl van der Peijl, wrote a small, sharp PowerShell script that did one thing well: it found every VM that was running on the wrong node relative to its storage and Live Migrated it to the node that owned its Cluster Shared Volume. The idea is published on his site under align VMs with storage, and it has quietly saved a lot of clusters from redirected I/O over the years. Darryl has since founded Splitbrain, but the script lives on in countless toolboxes, including mine.

This is the story of what happens when you take that good idea into a modern production engagement, hit the places where the simple version is too blunt, and grow it into a balancer that a customer can actually run on a six-node stretch cluster without holding their breath. The credit for the core insight is Darryl's. What follows is what we built on top of it.

By Hans Vredevoort · 18 June 2026 · 10 minute read · Storage

1. The original idea, and why it is so good

The insight behind Darryl's script is one of those things that is obvious the moment someone says it and easy to miss for years before that. A Cluster Shared Volume has an owner node, the coordinator, and all metadata operations on that volume run through it. If a VM lives on one node but the CSV holding its disks is owned by another, every metadata operation, and any I/O that cannot go direct, crosses the cluster network to the coordinator and back. Put the VM on the node that owns its storage and that traffic stays local.

The original script automated the whole loop. Enumerate the VMs, find each one's CSV, find that CSV's owner, and where the VM is not already there, Live Migrate it to the owner. A handful of lines, no agents, no products, just cluster cmdlets doing exactly what a careful admin would do by hand but across a hundred VMs in one pass. It is the kind of utility you keep, because it solves a real problem with no ceremony.

We have used it, recommended it, and learned from it. So when a recent engagement put a badly misaligned stretch cluster in front of us, the script was the obvious starting point. And then the cluster taught us where the simple version stops being enough.

2. Where the simple version bites back

On the cluster in question, the analysis pass reported that something like three quarters of the VMs were misaligned. The naive fix was clear: Live Migrate them all to their CSV owners, exactly what the original script does. We did not do that, and the reason is the whole point of this article.

First, the misalignment was not random, it was systematic. VM placement and CSV ownership had been swapped between two nodes, in both sites. The VMs were sensibly distributed across the hosts. It was the storage ownership that had drifted to the wrong nodes. If you "fix" that by moving every VM to the storage, you take a balanced VM distribution and concentrate it onto the few nodes that happen to own the CSVs. You trade a storage-locality problem for a load and memory problem.

Second, mass Live Migration wrecked role spread. When we modelled moving all of those VMs, the count of same-function VMs colliding on a single node went up, not down. You would align the storage and simultaneously make the cluster less resilient to a node failure, because now more identical roles sit together.

Third, this was a SAN cluster, Pure FlashArray over iSCSI, where every node has a direct path to the storage. The payoff of CSV-owner alignment is real but smaller on a SAN than on Storage Spaces Direct, where the owner relationship maps to where the data physically lives. Moving ninety VMs to chase a smaller benefit, while creating two new problems, is a bad trade.

None of this is a criticism of the original script. It is what happens when a clean idea meets a specific production reality. The script asks the right question. The environment demanded a different answer.

3. The key reframe: move ownership, not VMs

Here is the reframe that made everything click. The goal is to get each VM and its CSV owner onto the same node. Darryl's script achieves that by moving the VM. But there are two movable objects in that sentence, and on this cluster the cheaper one to move was the other one.

So the balancer's primary action became CSV-ownership rebalancing. For each CSV, find the node where most of its VMs already run, and if that is not the current owner, move the ownership there with Move-ClusterSharedVolume. That single call is near-instant and non-disruptive. The VMs do not move, their memory does not move, the load distribution stays exactly as the customer arranged it. And in one pass it aligns dozens of VMs at once, because aligning the owner to the crowd fixes every VM in that crowd simultaneously.

# The reframe, in one cmdlet, per CSV:
#   find the node hosting the most VMs of this CSV,
#   and hand it the coordinator role.
Move-ClusterSharedVolume -Name $csv -Node $nodeWithMostVMsHere

On the real run, the CSV-ownership phase aligned the great majority of the misaligned VMs without moving a single VM. Only a small remainder still needed a genuine Live Migration, and that is where the original technique comes back in: VM moves are kept as the fallback for the VMs that ownership moves cannot fix. You can still force the classic behaviour with a switch when you want it. The default just stopped leading with the expensive action.

The deeper background on why CSV ownership drifts in the first place, and what redirected I/O costs, is in CSV Ownership Imbalance in Hyper-V Clusters: Causes and Fixes.

4. Site-aware role spread

The second thing we added has nothing to do with storage and everything to do with surviving a node failure. Once you are already enumerating every VM, you can also check whether VMs with the same function are spread across nodes or piled onto one. Two domain controllers on the same host is the textbook example you never want.

The naive version of this check is wrong on a stretched cluster, though. A pool of remote-desktop hosts that is deliberately pinned to one site is not "badly spread", it is correctly placed. So the balancer judges spread per site: a role group is spreadable only when its member count is at most the number of usable nodes in the sites where its members actually run. Smaller groups get spread and get an AntiAffinityClassNames value set so the cluster keeps them apart on future placement and failover. Larger groups are reported as a pool with their imbalance, not forced apart against the site design. That distinction is what stops the tool from "fixing" things that were never broken.

Setting the anti-affinity is a few lines per role group. Give every member of a spreadable group the same class name, and the cluster keeps them on different nodes during placement and failover:

# Same-function VMs that should never share a node
$roleClass = 'RDP-Frontend'
$members   = 'RDP01','RDP02','RDP03'

foreach ($name in $members) {
    $group = Get-ClusterGroup -Name $name
    $aac   = New-Object System.Collections.Specialized.StringCollection
    [void]$aac.Add($roleClass)
    $group.AntiAffinityClassNames = $aac
}

# Verify the class is set on each member
Get-ClusterGroup |
    Where-Object { $_.AntiAffinityClassNames -contains $roleClass } |
    Select-Object Name, OwnerNode, AntiAffinityClassNames

5. Built-in protections for production

A script that moves storage ownership and Live Migrates VMs on a production cluster has to be trustworthy before it is clever. The protections we wrapped around it:

  • Read-only by default. With no switches, the script only analyses and projects. It changes nothing. Remediation is gated behind an explicit -Balance switch, and you are told to run that first with -WhatIf, which prints every ownership move, every VM move and every anti-affinity change without touching the cluster.
  • It never powers a VM off. Some VMs cannot Live Migrate, for known reasons. A running VM on that list is reported as needing a manual shutdown, never moved by force and never shut down by the script. A VM only gets moved while off if it was already off.
  • Nothing disappears silently. Excluded and out-of-scope VMs are still listed, with owner, state, alignment and the reason they were skipped, labelled as a planned exclusion. A report you can trust is one that shows you what it did not touch as clearly as what it did.
  • It reads on a blue screen. The console and log output use white for information, green for aligned and for non-disruptive wins, and red for misalignment, collisions and anything that needs attention. On a typical dark admin console you can see the state at a glance without squinting at near-identical greys.
The one assumption we flagged The list of VMs excluded from Live Migration had been carried over from an older script and had not been re-measured on this cluster. We did not trust that silently. We marked it in the report as an assumption the customer still has to confirm, because a remediation tool that hides its own uncertainty is worse than one that admits it.

6. Before and after, in one picture

The last thing we added is the part that makes the whole exercise defensible: a measured before-and-after. The script renders a PRE and a POST view per node, VM count, memory, aligned versus misaligned, and role collisions, as plain bars in the log and optionally as an HTML report. Between the two it shows the intermediate state after the CSV-ownership phase alone, so you can see exactly how many VMs were aligned without any VM moving at all.

That projection is what lets you run -Balance -WhatIf, read precisely what would change, and only then decide. It turns "trust me, this will help" into "here is the node-by-node picture before, the picture after the ownership moves, and the picture after the few remaining VM moves". A reviewer who was not in the session can read the outcome from the report. The log closes with the recommendations and the exact commands to preview and to apply, so nobody has to reconstruct the invocation from memory.

A preview run reads like this. The names are generic, but the shape is exactly what the tool prints:

PS> .\Optimize-VMPlacement.ps1 -Balance -WhatIf

Hyper-V VM Placement Optimizer  -  WhatIf (no changes will be made)
Cluster: PROD-CL   Nodes: 4   CSVs: 6   Clustered VMs: 48

PRE  -----------------------------------------------------------------
  Site A  HV-A1 : 14 VMs | 11 aligned | 3 misaligned | roles: 2 collisions
  Site A  HV-A2 : 10 VMs |  4 aligned | 6 misaligned | roles: 1 collision
  Site B  HV-B1 : 13 VMs |  9 aligned | 4 misaligned | roles: 1 collision
  Site B  HV-B2 : 11 VMs |  6 aligned | 5 misaligned | roles: 0 collisions
  Total aligned: 30 / 48

PHASE A  CSV ownership -> where the VMs already run
  What if: Move-ClusterSharedVolume 'CSV02'  HV-A2 -> HV-A1   (aligns 6 VMs)
  What if: Move-ClusterSharedVolume 'CSV04'  HV-B2 -> HV-B1   (aligns 5 VMs)
  What if: Move-ClusterSharedVolume 'CSV05'  HV-A2 -> HV-A1   (aligns 3 VMs)
  Aligned after CSV-owner phase: 44 / 48   (14 VMs aligned, 0 VMs moved)

PHASE B  VM moves (running, still misaligned)
  What if: Move-ClusterVirtualMachineRole 'APP07'  HV-A1 -> HV-B1  (Live)
  What if: Move-ClusterVirtualMachineRole 'WEB03'  HV-A2 -> HV-B2  (Live)
  SKIP (off): BACKUP01  - aligns on a later run under load
  MANUAL:     MGMT01    - excluded from Live Migration, shut down by hand

PHASE C  Anti-affinity (spreadable role groups)
  What if: Set AntiAffinityClassNames 'DC' on DC01, DC02
  Pool (not forced): RDP01..RDP04 on Site B - 4 members > 2 site nodes, reported only

POST (projected) -----------------------------------------------------
  Site A  HV-A1 : 13 VMs | 13 aligned | 0 misaligned | roles: 0 collisions
  Site A  HV-A2 :  9 VMs |  9 aligned | 0 misaligned | roles: 0 collisions
  Site B  HV-B1 : 14 VMs | 13 aligned | 1 misaligned | roles: 0 collisions
  Site B  HV-B2 : 12 VMs | 11 aligned | 1 misaligned | roles: 0 collisions
  Total aligned: 46 / 48   (2 left: BACKUP01 off, MGMT01 needs manual shutdown)

RECOMMENDATIONS
  - 3 CSV ownership moves align 14 VMs with zero downtime. Safe any time.
  - 2 Live Migrations (APP07, WEB03). Off-peak preferred.
  - MGMT01 is pinned and misaligned; shut it down by hand to align, or leave as-is.
  - Preview : .\Optimize-VMPlacement.ps1 -Balance -WhatIf
  - Apply   : .\Optimize-VMPlacement.ps1 -Balance

Reading top to bottom you can see the whole argument of this article play out: the ownership phase aligns fourteen VMs without moving one, only two running VMs need a Live Migration, the powered-off and the pinned VM are named and left alone rather than forced, and the oversized role pool is reported instead of broken apart. That is the run you take into a change window with confidence, because you have already seen exactly what it will do.

7. What we kept from Darryl, and what we owe him

Strip away everything we added and the core is still his: find the mismatch between where a VM runs and where its storage is owned, and close the gap. The strict CSV mount-prefix matching, the per-VM primary-CSV logic, the instinct to automate the whole loop from a single management server with nothing but cluster cmdlets, that lineage runs straight back to the original align-VMs-with-storage script. We did not replace it. We grew it for a bigger, stretched, SAN-backed, role-sensitive environment, and we built production-grade protections around it.

That is how good operational tooling tends to evolve. Someone sees the essential problem clearly and writes the tight version. Years later it meets an environment with more constraints, and the tight version becomes the kernel of something larger, while the insight at its centre stays exactly the same. Hats off to Darryl van der Peijl, whose script taught a generation of Hyper-V admins to stop letting their VMs talk to their storage the long way around. We are glad to keep building on it.

Get the script A cleaned-up version of the placement balancer, Optimize-VMPlacement.ps1, is on GitHub at github.com/trippledutch/shared, under the MIT licence, with thanks to Darryl van der Peijl for the original idea. It is read-only by default; run it first with -Balance -WhatIf and read exactly what it would do. Would you rather we ran it on your cluster and handed you the before and after? That is part of a ClusterTriage Hyper-V cluster assessment.

If you have never checked whether your VMs run where their storage is owned, the odds are good that some of them do not, and a SAN will hide the symptom until a node drains at the wrong moment.

A ClusterTriage Hyper-V cluster assessment measures alignment, role spread and ownership drift, and hands you the before-and-after.

Schedule a cluster assessment intro call →

Schedule a Hyper-V cluster assessment intro call →

Frequently asked questions

What does it mean to align a VM with its storage?

A VM is aligned when it runs on the cluster node that currently owns the Cluster Shared Volume holding most of its disk data. When VM and CSV owner sit on the same node, metadata operations stay local and you avoid redirected I/O over the cluster network. Darryl van der Peijl's original script automated finding and fixing that mismatch by Live Migrating the VM to the CSV owner.

Why move CSV ownership instead of moving the VMs?

Live Migrating dozens of VMs concentrates load on the owner nodes and can wreck role spread, and on SAN storage the alignment payoff is smaller than on Storage Spaces Direct. Moving the CSV ownership to where the VMs already run is near-instant and non-disruptive, and aligns many VMs at once without changing the VM or memory distribution. We made ownership moves the primary action and kept VM moves as the remainder.

Is the balancer safe to run on a production cluster?

The analysis is read-only and always safe. Remediation only happens with an explicit Balance switch, and you are expected to run it first with WhatIf, which prints every ownership move, VM move and anti-affinity change without touching anything. The script never powers a VM off: a running VM excluded from Live Migration is reported as needing a manual shutdown rather than being moved.

What is site-aware role spread?

On a stretched cluster, VMs with the same function should be spread across nodes for resilience, but only within the sites where they actually run. A role is spreadable when its member count is at most the number of usable nodes in those sites. Larger groups are treated as a pool and reported with their imbalance rather than forced apart, which stops site-bound pools like RDP hosts from being mislabelled as fully spreadable.

Does this replace the cluster's automatic balancer?

No, it complements it. Automatic CSV balancing and node fairness handle the general case. This script targets the specific alignment between VM placement and CSV ownership, sets anti-affinity so the cluster keeps role members apart on future failover, and gives you a measured before-and-after so you can prove the change helped.