Read-only copy of pvginkel/Ansible
docs/runbooks/disk-resize.md as of 80f07c4 (2026-10-17).
The source is authoritative: change it there.
Growing a managed VM's disk #
How to grow the boot (or a data) disk on a Terraform-provisioned, Ansible-managed VM. Replaces the manual three-step "grow in PVE UI → growpart → resize2fs" routine. A ZFS pool on a virtual disk grows differently in the guest: see its section.
The contract is split across the two tools:
- Terraform owns the PVE-side disk geometry — bumping
size_gbis the only place you touch. - Ansible owns the guest-side filesystem — the
managed_filesystemsrole readsqm configfrom the VM's PVE host and reconciles the partition + filesystem to match. (Same role also handles the first-boot create path: partition + format + mount on a fresh data disk. On a converged host every step reportsok.)
Disks can only grow. Shrinking requires destroy + recreate of the VM (the rebuild path).
Prerequisites #
- Both SSH identities loaded in
ssh-agentperoperator-workstation.md:pve-root(Terraform → PVE) and theansibleservice key (Ansible → managed VM). - The target VM has
vm_idandpve_nodein inventory. All managed VMs and every scratch host already do; checkinventories/prd/host_vars/<host>.yml(orinventories/scratch/host_vars/<host>.ymlfor a scratch VM) if a new VM is missing them. - The PVE host's thin pool has room:
pvesm statuson the VM'spve_nodeshowslocal-lvm's free space. A grown disk takes thin space only as the guest writes it, but a full thin pool stalls every VM on it.
Steps #
1. Bump size_gb in Terraform #
For the scratch VM, this is vm_disk_size_gb in terraform/scratch/terraform.tfvars. For production VMs, edit the matching managed_disks[*].size entry under the VM's key in terraform/prd/vms.tf.
cd terraform/scratch # or: cd terraform/prd
terraform plan # confirm the only change is the disk size you intended
terraform apply
For a production resize, narrow the apply to the affected VM with terraform apply -target='module.vm["<host>"]'.
PVE resizes the underlying logical volume online — no VM reboot.
2. Grow the in-guest filesystem #
From the ansible/ directory:
poetry run ansible-playbook playbooks/grow-disks.yml --limit <host> --check
poetry run ansible-playbook playbooks/grow-disks.yml --limit <host>
The check run prints what growpart and resize2fs will do without running them. The real run grows the partition and filesystem to match what PVE declared in step 1.
A second real run reports changed=0 — that's the idempotency proof.
3. Verify #
ssh ansible@<host> 'lsblk /dev/sda && df -h <mountpoint>'
Disk, partition, and filesystem should all show the new size (modulo the small overhead at the end of disk for the GPT secondary header and the BIOS/EFI/boot helper partitions on cloud-init Ubuntu).
Multiple disks per VM #
VMs in pve_vms inherit a root entry from group_vars/pve_vms.yml. To extend it to a data disk, set managed_filesystems_volumes at the right inventory level (group_vars/<group>.yml if every host in the group has it; host_vars/<host>.yml otherwise) — overriding replaces the list:
# inventories/prd/group_vars/k8s_prd.yml — every k8s_prd node has scsi1 = /var/snap
managed_filesystems_volumes:
- scsi_index: 0
mountpoint: /
fstype: ext4
- scsi_index: 1
mountpoint: /var/snap
fstype: ext4
Passthrough disks (Ceph OSD volumes, ZFS-passthrough drives) are deliberately not in scope for this role and must not be added to the list — they're owned by their respective stacks (Ceph, ZFS) and have their own resize procedures. A ZFS pool on a virtual disk stays off the list as well: it has its own section.
Growing a ZFS pool on a virtual disk #
Each pool in a host's zfs_pools (zpool1 on srvk8sdev, zpool3–5 on srvk8s2–4) fills a whole Terraform virtual disk. The zfs role only creates pools, and grow-disks.yml handles ext4 only, so you grow the pool by hand in the guest. The datasets on the pool have quotas set by their deploy repo's Terraform, and growing the pool doesn't raise any of them.
-
Bump the disk in Terraform, as in step 1. The slot is the pool's
scsi_index. -
Expand the pool on the guest.
<N>is the pool'sscsi_index:d=$(basename "$(readlink -f /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi<N>)") echo 1 | sudo tee /sys/block/$d/device/rescan # the kernel picks up the new size lsblk /dev/$d # the disk shows it, part1 still the old size sudo zpool online -e <pool> scsi-0QEMU_QEMU_HARDDISK_drive-scsi<N> zpool list <pool> # SIZE is the new sizeThe pool was created on the whole disk, so ZFS owns its partition table:
online -egrows partition 1 itself, with nogrowpart. These pools haveautoexpandoff, so the new space stays unused until you runonline -e. -
Raise the quota of the dataset that needs the space.
zfs get -r quota <pool>lists the quotas. Each dataset is ahomelab_zfs_datasetin the Terraform of the deploy repo that uses it. Change the quota there, and the repo's Argo CD sync applies it. For KubeCoder's env storage on zpool5, that is KubeCoderDeployconfig/<stage>/terraform.tfvars:- Raise both
zfs_quota, the real limit, andzfs_size, the PV's capacity, which Terraform updates in place. - Leave the chart's
storage.zfs.size(the PVC's request) alone. The PVC is bound to a fixed PV, and Kubernetes won't resize such a claim. - prd's value reaches the cluster only through a promotion. That ships everything else waiting on
mainand restarts every prd env pod (SOP).
- Raise both
-
Verify:
zfs list -o name,used,avail,quota -r <pool>.
Failure modes #
terraform planproposes more than the disk grow. Stop and investigate before applying — drift on other fields signals an out-of-band PVE UI edit or a forgottenlifecycle.ignore_changesline.- Role asserts "Rescan did not pick up the new size." The kernel didn't see the SCSI capacity change. Reboot the VM (
ssh <host> sudo reboot), wait for it to come back, re-run the playbook. zpool listshows the old size afterzpool online -e.lsblkshows the old disk size too, which means the rescan didn't land. Reboot the VM and run the ZFS step 2 again.growpartfails with anything other thanNOCHANGE. Usually means the partition layout is not what the role expects (LVM under the partition, dm-crypt, btrfs subvolumes). The role's contract is "partition is a direct child of the disk and holds an ext4 filesystem"; anything else needs a role extension.