jelius/infra

Shrinking an EC2 Root Volume

How to safely shrink an AWS root volume below the base AMI snapshot limit on a live system.

View on Git

AWS forcibly restricts you from provisioning a root volume smaller than the base AMI's snapshot (typically 8 GB). If you want to squeeze a Debian instance into a 3 GB drive to enforce hard storage quotas or save on EBS costs, you have to do it by hand.

Groundwork

This guide is executed on a Graviton (arm64) instance running Debian 13 with UEFI boot. If you are on x86_64, the process is identical, but your GRUB target will be x86_64-efi instead of arm64-efi.

ItemValue
Instancet4g.micro (Graviton, arm64/aarch64)
BootUEFI + GRUB 2 (arm64-efi) via shim
Target Build3 GB total: 512 MiB ESP (FAT32) + ~2.5 GiB ext4 root
New Disk ID/dev/nvme4n1 (Always verify yours with lsblk)

Before touching the terminal, go to the AWS Console, create a new 3 GB gp3 volume in the same Availability Zone, and attach it to your instance.

Important: All commands below assume a root shell (sudo -i) and must be run in a continuous session so the environment variables persist.

# Find the new, empty 3G disk
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS  

# Ensure your current system actually fits in ~2.4 GiB
df -h /                              

# Set the target disk variable (change to match your lsblk output)
DISK=/dev/nvme4n1                    

The Migration Process

  1. Partition and Format the Target: UEFI requires a dedicated FAT32 boot partition. Why two partitions? The instance boots via UEFI, and UEFI refuses to load a bootloader from anything other than a dedicated EFI System Partition (ESP). A single ext4 partition will flatly fail to boot. I use GPT with a 1 MiB offset to ensure perfect partition alignment.
apt-get update && apt-get install -y parted dosfstools

parted -s $DISK mklabel gpt
parted -s $DISK mkpart primary fat32 1MiB 513MiB    # Partition 1: ESP (512 MiB)
parted -s $DISK set 1 esp on                        # Flag as EFI System Partition
parted -s $DISK mkpart primary ext4 513MiB 100%     # Partition 2: Root

# Format the partitions
mkfs.fat -F 32 ${DISK}p1
mkfs.ext4 ${DISK}p2

# Mount the new root (ESP gets mounted later)
mkdir -p /mnt/root
mount ${DISK}p2 /mnt/root
mkdir -p /mnt/root/boot/efi
  1. Clone the Live Root Filesystem: Strict boundary rules prevent recursion. The goal is a faithful, bit-for-bit clone of the operating system without recursively copying attached network drives or virtual file systems into themselves.
rsync -aHAX --one-file-system \
  --exclude='/dev/*' --exclude='/proc/*' --exclude='/sys/*' --exclude='/run/*' \
  --exclude='/tmp/*' --exclude='/mnt/*' --exclude='/media/*' \
  --exclude='/lost+found' --exclude='/swapfile' \
  / /mnt/root/
  • -aHAX: Maintains permissions, owners, hard links, ACLs, and extended attributes.
  • --one-file-system: Stops rsync dead at mount boundaries.
  • --exclude='/x/*': Skips the contents of the directories but keeps the empty directories themselves. Excluding /dev entirely would delete the mountpoint and break the chroot environment later.

Because /mnt/* was excluded, you must manually recreate any custom mountpoints your system expects under /mnt on the target drive (e.g., mkdir -p /mnt/root/mnt/postgres).

  1. Rewire the Mount Points: Updating /etc/fstab to use the new UUIDs. The rsync command faithfully cloned the old fstab, which means the new drive is currently configured to look for the old drive's partitions. Debian cloud images use PARTUUID, so we swap the old IDs for the new ones using sed.
NEW_ROOT=$(blkid -s PARTUUID -o value ${DISK}p2)
NEW_ESP=$(blkid -s PARTUUID -o value ${DISK}p1)
OLD_ROOT=$(blkid -s PARTUUID -o value $(findmnt -no SOURCE /))
OLD_ESP=$(blkid -s PARTUUID -o value $(findmnt -no SOURCE /boot/efi))

sed -i "s/$OLD_ROOT/$NEW_ROOT/; s/$OLD_ESP/$NEW_ESP/" /mnt/root/etc/fstab
cat /mnt/root/etc/fstab

Verify the output. Leaving an old ID in place guarantees a kernel panic on boot.

  1. Inject the UEFI Bootloader: Chroot execution with standard fallback. To install GRUB correctly, we have to fake a live environment for the new disk by bind-mounting the virtual file systems. We also pass through DNS (resolv.conf) so package managers work inside the jail if needed.
mount ${DISK}p1 /mnt/root/boot/efi
mount --bind /dev      /mnt/root/dev
mount --bind /dev/pts  /mnt/root/dev/pts
mount --bind /proc     /mnt/root/proc
mount --bind /sys      /mnt/root/sys
mount -t tmpfs tmpfs   /mnt/root/run
mount --bind /etc/resolv.conf /mnt/root/etc/resolv.conf

chroot /mnt/root /bin/bash

Inside the chroot jail, install GRUB targeting the Graviton architecture. I explicitly pass --force-extra-removable. This writes a copy of the bootloader to the fallback path (EFI/BOOT/BOOTAA64.EFI). Without this, the EC2 instance's firmware has no NVRAM entry pointing to the new disk and will drop you straight into a UEFI shell on boot.

grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=debian --force-extra-removable
update-grub

exit
sync
umount -R /mnt/root
poweroff
  1. Execute the Hot Swap: AWS Console manipulation. Once the instance state shows Stopped in the AWS Console, sever the old drive and promote the new one:

  2. Go to Instance → Storage and note your exact Root device name (e.g., /dev/xvda).

  3. Detach the original 8 GB volume. (Keep it as your rollback insurance).

  4. Detach the new 3 GB volume from its temporary /dev/sdf mounting point.

  5. Attach the 3 GB volume back to the instance, ensuring the Device name exactly matches the root device name you noted in step 1. If you use a different name, AWS will not treat it as a boot disk.

  6. Start the instance.


Emergency Recovery: The UEFI Shell Loop

If you skipped the --force-extra-removable flag in Step 4, the instance will boot up, fail its status checks, and drop into an infinite loop in the EC2 Serial Console:

The system will reboot in 48 seconds. Press any key to run interactive shell.
Shell>

This happens because the firmware's boot entries still point at the old volume's ESP. The files exist on your new disk, but the UEFI NVRAM doesn't know where to look. Press any key to interrupt the countdown and map the bootloader manually:

Shell> map -r                  # Refresh device list. FS0: should be the new FAT32 ESP.
Shell> FS0:
FS0:\> ls EFI\debian\          # Verify shimaa64.efi exists (use backslashes!)
FS0:\> bcfg boot dump          # View stale entries
FS0:\> bcfg boot add 0 FS0:\EFI\debian\shimaa64.efi "Debian"
FS0:\> exit                    # Reboot

This injects the correct boot mapping into position 0 (highest priority). The instance will immediately reboot and launch normally.

On this page