Shrinking VHDs on Azure, live
Azure decided to deprecate unmanaged disks, which really cramped my style for a silly reason: my unmanaged disks were just a bit too large to fit into the “round” managed disk size of 256GiB. So that 4GiB was going to double my storage costs. Not cool!
Complicating the matter…
- My 257GiB disks were mostly full, so I really didn’t want to download them, copy the data around, then reupload.
- I was using the max number of data disks for the VM size I had (four)
- I was using
btrfs
and striping the data across the data disks. So mounting a single disk and copying some data over to a larger disk couldn’t be done - all four had to be mounted at once.
Happily, the disks are in the standard VHD format. I decided to take this as a bit of a challenge and modify them using the Azure Page Blob API, but still keep them usable enough to migrate.
What? How?
⚠️ This was, and is, probably a bad idea, so follow these steps at your own risk. I didn't care much about the data but I preferred not to lose it. Standard advice of back up your data, and disclaimer that it's not my fault if you lose it. There's always the easier and safer option of just mounting more disks on a larger VM temporarily, and copying the data over!
The general idea I came up with was - as long as the disks had unallocated space at the end, the only thing I’d have to do was truncate the blobs and rewrite the VHD footer.
At first, I thought I was going to have to write my own VHD footer reading/writing code myself - but then I found someone with a similar idea which inspired my C# version.
With that in mind, I started with a few manual steps:
btrfs balance
andbtrfs filesystem resize <devId>:-1G /home
more than once for each “physical” drive and verified there was free space at the end- Create a dummy disk with the desired size so the trailer is correct
- I did this individually, for each data disk, because there’s a UUID in the footer. Unclear if this was actually necessary.
- Set the dummy disk to export mode to get a SAS URI
- Get the exact size of the dummy disk and save off its footer
- One dash of caution: take a snapshot of each pre-modification VHD so I could roll back if something went wrong.
Then, some code to:
- Resize each target disk to the exact same size as the dummy disk
- Copy over the footer from the dummy disk to the target disk so Hyper-V (Azure) is happy
And then finally finish the migration to managed disks.
So did it work?
Yep! Everything went fine. The only issue was a post-migration fsck
which showed some errors like
btrfs: csum mismatch on free space cache
failed to load free space cache for block group
These were easy to fix via btrfs check --progress --clear-space-cache v1
Give me the code
using System.Text;
using Azure.Storage;
using Azure.Storage.Blobs.Specialized;
var accountName = "myStorageAccount";
var containerName = "myContainer";
var blobName = "myBlobName.vhd";
var creds = new StorageSharedKeyCredential(accountName, "myStorageAccountKey");
var dummyStorageName = "dummy4-footer.bin";
var existingBackupFooterName = "existing4-footer.bin";
var dummySasUri = "<dummy_export_URL>";
var tempSourceBlob = new PageBlobClient(new Uri(dummySasUri));
await ReadFooterAsync(tempSourceBlob, dummyStorageName);
var targetPageBlob = new PageBlobClient(new Uri($"https://{accountName}.blob.core.windows.net/{containerName}/{blobName}"), creds);
await ReadFooterAsync(targetPageBlob, existingBackupFooterName);
await ResizeAndWriteFooterAsync(targetPageBlob, dummyStorageName);
static async Task ReadFooterAsync(PageBlobClient sourceBlob, string targetBinaryFooterFileName)
{
var footerSize = 512;
var length = (await sourceBlob.GetPropertiesAsync()).Value.ContentLength;
Console.WriteLine($"Dummy length: {length}");
using var blobStream = sourceBlob.OpenRead(length - footerSize, footerSize);
var buf = new byte[footerSize];
int read = 0;
while (read < footerSize && read >= 0)
{
read += blobStream.Read(buf, read, footerSize - read);
}
Console.WriteLine($"Read {read} bytes");
Console.WriteLine($"Footer: {Encoding.UTF8.GetString(buf)}");
using var fs = File.OpenWrite(targetBinaryFooterFileName);
await fs.WriteAsync(buf);
}
static async Task ResizeAndWriteFooterAsync(PageBlobClient targetBlob, string sourceFooterBinaryFileName)
{
var footerSize = 512;
var finalSize = 273804165632; // 255GB
var length = (await targetBlob.GetPropertiesAsync()).Value.ContentLength;
Console.WriteLine($"Before length: {length}");
await targetBlob.ResizeAsync(finalSize);
using (var blobStream = await targetBlob.OpenWriteAsync(overwrite: false, finalSize - footerSize))
using (var fs = File.OpenRead(sourceFooterBinaryFileName))
{
await fs.CopyToAsync(blobStream);
}
length = (await targetBlob.GetPropertiesAsync()).Value.ContentLength;
Console.WriteLine($"After length: {length}");
}