See What's on an Old Azure VM Disk Before You Delete It
You get the old disk’s top-level folders with the size and last-modified date of each, so you can decide what to keep before the disk is gone.

On this page
TL;DR
Take a snapshot of the old OS disk before you delete anything. Create a disk
from the snapshot in the same zone as a VM you already run, attach it to that
VM as a data disk, and start the VM. Bring the disk online and list its folders
with az vm run-command invoke. You need no RDP session and no download.
The variables are set in the guide’s first step, and inspect.ps1 is the
script in its mount step.
az snapshot create --resource-group "$ARCHIVE_GROUP" --name "$SNAPSHOT_NAME" \
--source "$OLD_DISK_ID" --incremental true --sku Standard_LRS
SNAPSHOT_ID=$(az snapshot show --resource-group "$ARCHIVE_GROUP" \
--name "$SNAPSHOT_NAME" --query id --output tsv)
az disk create --resource-group "$VM_GROUP" --name "$INSPECT_DISK" \
--source "$SNAPSHOT_ID" --zone "$VM_ZONE" --sku Standard_LRS
az vm disk attach --resource-group "$VM_GROUP" --vm-name "$VM_NAME" \
--name "$INSPECT_DISK" --lun 0
az vm start --resource-group "$VM_GROUP" --name "$VM_NAME"
az vm run-command invoke --resource-group "$VM_GROUP" --name "$VM_NAME" \
--command-id RunPowerShellScript --scripts @inspect.ps1Tested with
- Azure CLI
- 2.90.0 on macOS 27.0.1
- Guest VM
- Windows Server 2019 Datacenter
- AWS CLI
- 2.38.1
- Date
- October 2026
I had an old resource group in Azure. It held a VM, a public IP, a network card and a 127 GB OS disk, and I couldn’t remember what was on the disk. The VM had been a Rust game server years ago. Since then, the game I host has changed to 7 Days to Die, on a different VM. I didn’t want to pay for the old disk, but I also didn’t want to delete a world save I might miss. So I asked Claude Code, with the Azure CLI signed in on my Mac, what was on the disk.
Set the names once
Every command in this guide reads its names from shell variables. In a terminal with the Azure CLI signed in, set them, replacing each placeholder:
OLD_DISK_ID="<resource-id-of-the-old-os-disk>"
ARCHIVE_GROUP="<new-resource-group-for-the-snapshot>"
SNAPSHOT_NAME="<snapshot-name>"
VM_GROUP="<resource-group-of-the-vm-you-run>"
VM_NAME="<vm-you-run>"
INSPECT_DISK="<temporary-disk-name>"Snapshot the disk first
The snapshot is what lets you delete everything else right away. This creates a resource group for it, so it outlives the old VM’s group, and an incremental snapshot of the old OS disk:
az group create --name "$ARCHIVE_GROUP" --location "<region>"
az snapshot create --resource-group "$ARCHIVE_GROUP" --name "$SNAPSHOT_NAME" \
--source "$OLD_DISK_ID" --incremental true --sku Standard_LRSUse the old disk’s region for --location. An incremental snapshot is
billed for its used size only (opens in a new tab),
not for the disk’s provisioned size.
Do not wait for the snapshot’s completionPercent to confirm it finished. For
mine, that field came back empty, and a loop polling it never ended. Prove the
snapshot is readable instead. This grants read access for five minutes,
requests the first 512 bytes, and revokes access:
SAS=$(az snapshot grant-access --resource-group "$ARCHIVE_GROUP" \
--name "$SNAPSHOT_NAME" --duration-in-seconds 300 --access-level Read \
--query accessSAS --output tsv)
curl --silent --output /dev/null --write-out '%{http_code}\n' \
--range 0-511 "$SAS"
unset SAS
az snapshot revoke-access --resource-group "$ARCHIVE_GROUP" \
--name "$SNAPSHOT_NAME"206 means the snapshot is readable. It doesn’t prove that every file is in
it; the listing later in this guide does. After a 206 you can delete the old
VM’s resource group, but that deletes the old disk with it, and the snapshot
becomes the only copy. To be certain first, wait until you have listed the
files.
Create a disk in the same zone as the VM
A VM in an availability zone can attach only a disk in the same zone. Read the VM’s zone and the snapshot’s ID into variables:
VM_ZONE=$(az vm show --resource-group "$VM_GROUP" --name "$VM_NAME" \
--query "zones[0]" --output tsv)
SNAPSHOT_ID=$(az snapshot show --resource-group "$ARCHIVE_GROUP" \
--name "$SNAPSHOT_NAME" --query id --output tsv)This creates a Standard HDD disk from the snapshot, which is enough for a disk you read once:
az disk create --resource-group "$VM_GROUP" --name "$INSPECT_DISK" \
--source "$SNAPSHOT_ID" --zone "$VM_ZONE" --sku Standard_LRSAttach the disk and start the VM
Attaching works while the VM is deallocated. This attaches the disk at LUN 0 and starts the VM:
az vm disk attach --resource-group "$VM_GROUP" --vm-name "$VM_NAME" \
--name "$INSPECT_DISK" --lun 0
az vm start --resource-group "$VM_GROUP" --name "$VM_NAME"Mount the disk from your terminal
Run Command runs a script inside the guest through the VM agent, and returns its output to your terminal.
Warning
Test these scripts on a VM and disk you can afford to lose before you run
them on yours. inspect.ps1 changes the state of every disk except disk 0,
and the cleanup commands delete disks and resource groups. Run Get-Disk on
the VM first to confirm that disk 0 is its OS disk.
Save this as inspect.ps1. It brings every disk other than the OS disk
online, clears read-only, and gives a drive letter to any partition over 10 GB
that lacks one:
Get-Disk | Where-Object Number -ne 0 | ForEach-Object {
if ($_.IsOffline) { Set-Disk -Number $_.Number -IsOffline $false }
if ($_.IsReadOnly) { Set-Disk -Number $_.Number -IsReadOnly $false }
Get-Partition -DiskNumber $_.Number |
Where-Object { -not $_.DriveLetter -and $_.Size -gt 10GB } |
Add-PartitionAccessPath -AssignDriveLetter
}
Get-Volume |
Format-Table DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemainingIn the terminal, run it on the VM:
az vm run-command invoke --resource-group "$VM_GROUP" --name "$VM_NAME" \
--command-id RunPowerShellScript --scripts @inspect.ps1 \
--query "value[].message" --output tsvThe old disk appears as a second Windows volume beside the VM’s own C:.
On mine it was E::
DriveLetter FileSystemLabel FileSystem Size SizeRemaining
----------- --------------- ---------- ---- -------------
Recovery NTFS 471855104 454938624
C Windows NTFS 135771664384 66295812096
D System Reserved NTFS 524283904 488136704
E Windows NTFS 135838822400 105699086336
FAT32 99614720 65237504List what’s on it
Run Command returns only the end of what the script prints, and drops the rest
without an error; the size is in the
Run Command restrictions (opens in a new tab).
So print one line per folder, cap any search, and skip the Windows folder,
which took more than ten minutes to measure on mine.
This script prints one line per top-level folder, skips Windows, and caps
the search for game folders at 15 results. It reads the old disk as E:. If
yours got another letter in the volume output, replace each E: with it.
Save it as list.ps1:
function Get-SizeGB($path) {
$bytes = (Get-ChildItem -LiteralPath $path -Recurse -Force -File `
-ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
'{0:N1}' -f ($bytes / 1GB)
}
$skip = 'Windows', 'System Volume Information', '$Recycle.Bin',
'Documents and Settings', 'Recovery'
Get-ChildItem E:\ -Force -Directory -ErrorAction SilentlyContinue |
Where-Object Name -notin $skip |
ForEach-Object {
'{0} {1} {2}' -f (Get-SizeGB $_.FullName),
$_.LastWriteTime.ToString('yyyy-MM-dd'), $_.Name
}
$gameNames = '(?i)^(rust|steamcmd|steamapps|server|minecraft|world.*|saves?)$'
Get-ChildItem E:\ -Recurse -Force -Directory -Depth 4 `
-ErrorAction SilentlyContinue |
Where-Object {
$_.FullName -notmatch '^E:\\(Windows|ProgramData\\Microsoft)' -and
$_.Name -match $gameNames
} |
Select-Object -First 15 |
ForEach-Object {
'{0} {1} {2}' -f (Get-SizeGB $_.FullName),
$_.LastWriteTime.ToString('yyyy-MM-dd'), $_.FullName
}Run it the same way, with --scripts @list.ps1. The top-level folders, with
size in GB and the last-modified date:
0.1 2020-10-10 jre8
0.2 2024-01-08 Minecraft
0.0 2022-12-11 Packages
0.0 2020-05-13 PerfLogs
0.4 2024-01-08 Program Files
0.0 2016-07-16 Program Files (x86)
0.9 2018-12-04 ProgramData
0.0 2018-12-04 runRust
0.1 2018-12-04 RustAdminRelease
6.1 2024-01-08 rustserver
0.2 2024-01-08 steamcmd
2.1 2023-12-29 Users
1.3 2023-12-29 WindowsAzurerustserver was the Rust server I remembered. Its save folder held two server
identities, the older from April 2020 and the newer
last written in December 2023. A Rust server wipes its map on a schedule, so
those saves had no value to me.
Minecraft was the folder I had forgotten about. It held a survival world,
E:\Minecraft\world, 100 MB across 25 region files. The newest region file
was dated April 2021, the last time the server saved the world. The folder
also held two Java crash logs from January 2024.
The VM I already run has a Minecraft world too, but a different one, so this world was the only copy and I kept it.
Zip what you keep to C:\Archive\<file-name>.zip on the OS disk of the VM you already run, with
Compress-Archive through Run Command. Before you delete anything, check that
the zip holds as many files as the folders you zipped. Mine held 80: 76 world
files plus four server config files.
Copy what you keep off the VM
A zip on the OS disk of the VM you already run is one disk failure from gone, and once the snapshot is deleted it’s the only copy. Put it in object storage before you delete the snapshot.
Any object store works. I used S3, where my 7 Days to Die backups already go. Check the bucket’s lifecycle rules before you choose it. The bucket my backups use was the obvious place, but its rule expires every object after 30 days, not only the backups:
aws s3api get-bucket-lifecycle-configuration --bucket "<bucket-name>" \
--query "Rules[].{id:ID,filter:Filter,expiration:Expiration}"A rule with an empty Filter, or a Prefix of "", applies to the whole
bucket. I used a different bucket, one whose only rule removes expired delete
markers.
The hash check at the end of this section works only when the bucket encrypts with SSE-S3, which is S3’s default.
To upload from the VM without putting AWS credentials on it, generate a
presigned PUT URL on your own machine. The AWS CLI’s aws s3 presign signs
only downloads, so this uses boto3 through uv. The URL expires after 30
minutes:
uv run --no-project --with boto3 python -c "
import boto3
from botocore.config import Config
s3 = boto3.client('s3', region_name='<aws-region>',
config=Config(signature_version='s3v4'))
print(s3.generate_presigned_url('put_object', ExpiresIn=1800,
Params={'Bucket': '<bucket-name>', 'Key': 'archive/<file-name>.zip'}))"Through Run Command on the VM, upload the file to that URL and print both
hashes. Set $url to the URL from the previous step:
$file = 'C:\Archive\<file-name>.zip'
$response = Invoke-WebRequest -Uri $url -Method Put -InFile $file `
-UseBasicParsing
"etag $($response.Headers['ETag'])"
"md5 $((Get-FileHash $file -Algorithm MD5).Hash.ToLower())"For a single-part upload with SSE-S3 encryption, the ETag S3 returns is the object’s MD5 (opens in a new tab), so matching values mean S3 has the same bytes. Under SSE-KMS or SSE-C the ETag isn’t an MD5, and the two values differ even when the bytes are the same. Mine matched, at 73,508,292 bytes.
Clean up
This detaches the temporary disk and deletes it. az disk delete asks for
confirmation. Run the last command only if the VM was deallocated before you
started; it deallocates the VM again so it stops billing for compute:
az vm disk detach --resource-group "$VM_GROUP" --vm-name "$VM_NAME" \
--name "$INSPECT_DISK"
az disk delete --resource-group "$VM_GROUP" --name "$INSPECT_DISK"
az vm deallocate --resource-group "$VM_GROUP" --name "$VM_NAME"Confirm the VM no longer lists a data disk:
az vm show --resource-group "$VM_GROUP" --name "$VM_NAME" \
--query "storageProfile.dataDisks" --output jsonAn empty list, [], means the disk is detached.
Delete the snapshot last, once the copy in object storage is confirmed. List
the archive group first, so the delete removes only what you expect. az group delete asks for confirmation:
az resource list --resource-group "$ARCHIVE_GROUP" \
--query "[].{name:name,type:type}" --output tsv
az group delete --name "$ARCHIVE_GROUP"
az group exists --name "$ARCHIVE_GROUP"false means the group and the snapshot in it are gone. From the old disk,
I kept one 73.5 MB zip.