The problem: two worlds, two identifiers
A common scenario in teams managing Azure infrastructure with IaC: the Windows systems team (Wintel) opens a ticket requesting disk changes on a VM. The problem is that they speak different languages:
- Wintel says: "Delete the disk with Msft Virtual Disk ID
60022480XXXXXXXXXXXXXXXXXXXXXXXXand expand the 32 GB one to 48 GB" - You see in Terraform:
data1,data2,data3with LUNs and sizes
The ID that Windows provides (60022480...) is a guest OS UniqueId that does not appear anywhere in your IaC code nor in the Azure portal. You cannot simply grep the repository and find the answer.
This article documents the complete process to resolve this mapping and execute the change safely with Terragrunt.
Key concepts
Before getting into it, let's clarify the terminology:
| Concept | Where it lives | Example |
|---|---|---|
| UniqueId (Guest) | Inside Windows | 60022480B94E649D631F68911AA694D1 |
| Managed Disk | Azure Resource Manager | vm-name-data2-1x |
| LUN | VM configuration in Azure | 10 |
| DiskNumber | Windows Disk Management | 2 |
| Name in IaC | Terragrunt/Terraform | data2 |
The key is to understand that DiskNumber in Windows ≠ LUN in Azure. They are independent concepts that require cross-correlation.
Step 1: Inventory from Azure (source of truth)
The first thing is to obtain which disks the VM actually has in Azure:
az account set --subscription "<subscription-id>"
az vm show -g <resource-group> -n <vm-name> \
--query "storageProfile.dataDisks[].{LUN:lun,Name:name,SizeGB:diskSizeGb}" \
-o tableExample result:
LUN Name SizeGB
----- -------------------- --------
1 vm-name-data1-1x 128
10 vm-name-data2-1x 32
11 vm-name-data3-1x 128We already know: 3 data disks in Azure with LUN 1, 10 and 11.
Step 2: Inventory from the guest OS (RunCommand)
If you don't have RDP access to the VM, you can run a PowerShell script remotely with az vm run-command invoke. This step usually requires several iterations because the environment does not always return what is expected.
2.1 Launch RunCommand from Azure CLI
az vm run-command invoke \
-g <resource-group> \
-n <vm-name> \
--command-id RunPowerShellScript \
--scripts "Get-Disk | Select-Object Number, @{N='SizeGB';E={[math]::Round(\$_.Size/1GB,0)}}, UniqueId | Format-Table -AutoSize"The response comes in JSON with the stdout and stderr fields. If the script fails, the error will appear in stderr.
2.2 The truncated script error (first failed attempt)
On the first attempt, a more complex script with Where-Object to filter by a specific ID was sent. The result was:
You must provide a value expression following the '-eq' operator.
Missing closing '}' in statement block or type definition.Why does it happen? The script was cut in the middle of a Where-Object { $_.Index -eq ... } block. RunCommand has a practical limit on the size of the inline --scripts, and special characters like $, {, } need proper escaping depending on the shell from which it is launched.
Solution: Use the complete, untruncated script with explicitly cast variables:
Get-Disk | ForEach-Object {
$d = $_
$wmi = Get-CimInstance Win32_DiskDrive |
Where-Object { $_.Index -eq [int]$d.Number } |
Select-Object -First 1
[pscustomobject]@{
DiskNumber = $d.Number
SizeGB = [math]::Round($d.Size / 1GB, 0)
UniqueId = $d.UniqueId
LUN = $wmi.SCSITargetId
Model = $wmi.Model
}
} | Sort-Object DiskNumber | Format-Table -AutoSizeTip: If the script is long, it is more reliable to pass it as a file with
--scripts @script.ps1instead of inline.
2.3 The confusing result: LUN=0 for all
The second attempt ran correctly, but returned something unexpected when querying a specific disk by its UniqueId:
TargetId : 600224808C5B855329AF53DEFF061AF0
DiskNumber : 1
UniqueId : 600224808C5B855329AF53DEFF061AF0
SerialNumber :
LUN : 0This is where many get confused: the disk shows up with LUN 0, but in Azure the data disks of this VM are on LUN 1, 10 and 11. How is that possible?
Explanation: SCSITargetId (which is what Win32_DiskDrive exposes as LUN inside the guest) is not the same as the Azure LUN. In many Azure Windows VMs:
- The guest SCSI controller reports
SCSITargetId = 0for all Msft Virtual Disk disks - The real Azure LUN is only visible from the Azure API (the
az vm showfrom Step 1) - Trying to map using this field will lead you to incorrect decisions
2.4 Final complete inventory (the decisive evidence)
Given the ambiguity of the LUN in the guest, the correct approach is to obtain the complete inventory of all disks with their sizes and UniqueIds:
DiskNumber SizeGB UniqueId LUN Model
---------- ------ -------- --- -----
0 128 IDE\DISKVIRTUAL_HD... 0 Virtual HD
1 128 600224808C5B855329AF53DEFF061AF0 0 Msft Virtual Disk
2 32 60022480B94E649D631F68911AA694D1 0 Msft Virtual Disk
3 128 60022480B16D7C3BE95C7D51B2235E9A 0 Msft Virtual DiskNow we do have what we need: UniqueId + size for each disk. This, combined with the Azure inventory, allows us to do the correlation.
Observations:
- DiskNumber 0 with ID
IDE\DISK...is the OS disk (do not touch) - The three remaining ones are the managed data disks (they all report
LUN 0in the guest, but that is irrelevant) - The 32 GB one is unequivocally
data2 - Between the two 128 GB ones, Wintel explicitly identified which one to delete by its UniqueId
Step 3: Cross-correlation (the mapping table)
With evidence from both sides, we build the decision table. This is the most important artifact of the process — it is what justifies the change in an audit:
| Guest UniqueId | Guest Size | Azure Disk | IaC Name | Azure LUN | Action |
|---|---|---|---|---|---|
...1AF0 | 128 GB | vm-name-data1-1x | data1 | 1 | Delete |
...94D1 | 32 GB | vm-name-data2-1x | data2 | 10 | Resize → 48 GB |
...5E9A | 128 GB | vm-name-data3-1x | data3 | 11 | Keep |
Decision logic
- 32 GB disk: There is only one in Azure (
data2, LUN 10) and one in the guest (UniqueId...94D1). Unequivocal mapping. - 128 GB disk to delete: Wintel provided the explicit UniqueId (
...1AF0). In Azure there are two 128 GB disks (data1on LUN 1 anddata3on LUN 11). We takedata1as the deletion target based on the combined evidence. - 128 GB disk to keep:
data3(LUN 11, UniqueId...5E9A) was not mentioned in the request; it stays unchanged.
Golden rule: When there are multiple disks of the same size, never decide by size alone. Require the systems team to confirm the UniqueId, the drive letter, or the volume label of the affected disk.
Step 4: Implementation in Terragrunt
In the Windows VM module, the data disks are defined in the dynamic_data_disks block of the terragrunt.hcl:
State BEFORE the change
dynamic_data_disks = {
"data1" = {
location = "North Europe"
storage_account_type = "StandardSSD_LRS"
create_option = "Empty"
managed_disk_size_gb = 128
public_network_access_enabled = false
os_type = "Windows"
lun = 1
caching = "ReadWrite"
}
"data2" = {
location = "North Europe"
storage_account_type = "StandardSSD_LRS"
create_option = "Empty"
managed_disk_size_gb = 32
public_network_access_enabled = false
os_type = "Windows"
lun = 10
caching = "ReadWrite"
}
"data3" = {
location = "North Europe"
storage_account_type = "StandardSSD_LRS"
create_option = "Empty"
managed_disk_size_gb = 128
public_network_access_enabled = false
os_type = "Windows"
lun = 11
caching = "ReadWrite"
}
}State AFTER the change
dynamic_data_disks = {
"data2" = {
location = "North Europe"
storage_account_type = "StandardSSD_LRS"
create_option = "Empty"
managed_disk_size_gb = 48 # Before: 32
public_network_access_enabled = false
os_type = "Windows"
lun = 10
caching = "ReadWrite"
}
"data3" = {
location = "North Europe"
storage_account_type = "StandardSSD_LRS"
create_option = "Empty"
managed_disk_size_gb = 128
public_network_access_enabled = false
os_type = "Windows"
lun = 11
caching = "ReadWrite"
}
}Changes made:
- Removed: the entire
data1block (the module handles the detach + delete) - Modified:
managed_disk_size_gbofdata2from 32 → 48 - Unchanged:
data3remains intact
Step 5: Pre-apply validation
terragrunt planThe plan should show exactly:
destroy: disk data1 (LUN 1, 128 GB)update in-place: disk data2, size 32 → 48- No changes in data3
If any other unexpected change appears: STOP. Check whether there is state drift or inherited variables that have changed.
Step 6: Apply and post-change verification
terragrunt applyAzure verification
az vm show -g <resource-group> -n <vm-name> \
--query "storageProfile.dataDisks[].{LUN:lun,Name:name,SizeGB:diskSizeGb}" \
-o tableExpected:
LUN Name SizeGB
----- -------------------- --------
10 vm-name-data2-1x 48
11 vm-name-data3-1x 128Guest verification (request from Wintel)
- Rescan disks in Disk Management
- Confirm that the deleted disk no longer appears
- Confirm that the expanded disk shows 48 GB
- Extend the partition at the filesystem level (the Azure resize does not do it automatically)
Rollback strategy
If something goes wrong post-apply:
- Don't touch anything else — do not try to fix on the fly
- Restore
data1indynamic_data_diskswith its original values - Revert
data2to 32 GB if necessary terragrunt plan→ verify that the plan is coherentterragrunt apply
Important note: If the deleted disk had data, the rollback will create a new empty disk. The data will have been lost. That is why it is critical to confirm with the systems team that the disk is free of data before the apply.
Common mistakes and lessons learned
1. Trusting the guest's SCSITargetId
In many Azure Windows VMs, Win32_DiskDrive.SCSITargetId returns 0 for all data disks. Do not use that value as the source of truth for the LUN.
2. Incomplete RunCommand scripts
If you copy multi-line PowerShell scripts into RunCommand, make sure the Where-Object { ... } block is complete. A cut in the middle produces cryptic errors:
You must provide a value expression following the '-eq' operator.
Missing closing '}' in statement block or type definition.3. Two disks of the same size
When there are multiple disks with the same size (like two 128 GB ones), you cannot map by size alone. You need the guest's UniqueId or, alternatively, for Wintel to confirm the associated drive letter/volume.
4. Resize does not extend the partition
Azure can resize the managed disk, but the filesystem inside Windows will still see the previous size until someone runs Extend-Volume or does it from Disk Management.
Operational checklist
Use this checklist for any disk change on Azure VMs managed with IaC:
- [ ] Subscription and Resource Group confirmed
- [ ] Azure inventory (
az vm show) saved - [ ] Guest inventory (PowerShell) saved
- [ ] Mapping table completed and reviewed
- [ ] IaC change reviewed (ideally by a second pair of eyes)
- [ ]
terragrunt planwith no unexpected changes - [ ] Backup/snapshot verified before destructive operation
- [ ] Apply executed in an approved change window
- [ ] Post-apply verification in Azure
- [ ] Post-apply verification with the systems team
- [ ] Documentation archived
Conclusion
Mapping disk IDs between the Windows guest and Azure managed disks is not trivial, especially when the identifiers each team handles are completely different. The key lies in:
- Don't assume — always cross-check evidence from both sides
- Document the decision — the mapping table is your insurance against audits
- Validate before destroying — a clean
plancosts nothing, a disk deleted by mistake costs a lot
With a well-defined flow and Terragrunt/Terraform as the source of truth of the infrastructure, these changes go from being risky operations to controlled, repeatable procedures.
Comments