Rocky Linux storage builds in layers: partition a disk, create a volume group and logical volume, format a filesystem, then mount it persistently in /etc/fstab. Ansible automates that workflow with modules from community.general and ansible.posix—not from ansible-core alone. This guide walks through the full stack on Rocky Linux 10.2 with playbooks you can rerun safely.
It assumes you can run plays with become from your first playbook. Full LVM theory, root filesystem migration, disaster recovery, and casual filesystem shrinking are out of scope—this page focuses on Ansible modules for a new data volume.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16;
community.general9.5.2;ansible.posix2.1.0.
This article covers a new secondary data disk—partition → LVM → filesystem → persistent mount. It does not cover moving / or /var to new storage, shrinking production volumes, or complex disk recovery.
Before every destructive task: run lsblk, pvs, and vgs on each host and confirm the block device in your variables is the spare data disk—not the OS disk (sda, nvme0n1, or whatever holds /). Never target the OS disk. Do not hard-code /dev/sdb in playbooks; store disk paths in group vars or host vars so one review point controls the target. On a first run against a new host, use --check and read the planned changes carefully—a dry-run mindset catches wrong-disk mistakes before parted or lvol runs.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
My test VM had no spare disk handy, so I used a loopback device (/dev/loop0, partition /dev/loop0p1) instead of /dev/sdb. Loop devices name partitions with a p (loop0p1); on SCSI you would see sdb1. Point storage_disk and storage_partition in group vars at the block device you actually have.
| Task | Module |
|---|---|
| Create partition | community.general.parted |
| Create volume group | community.general.lvg |
| Create logical volume | community.general.lvol |
| Create filesystem | community.general.filesystem |
| Create mount directory | ansible.builtin.file |
Add /etc/fstab entry and mount |
ansible.posix.mount |
| Verify block devices | ansible.builtin.command or facts |
| Debug storage variables | ansible.builtin.debug |
| Step | Example |
|---|---|
| Disk | /dev/sdb |
| Partition | /dev/sdb1 |
| Volume group | vg_data |
| Logical volume | lv_app |
| Filesystem | xfs |
| Mount point | /data/app |
| Persistent mount | /etc/fstab entry |
What This Article Covers
community.general.partedfor GPT partitions with the LVM flagcommunity.general.lvgandcommunity.general.lvolfor volume groups and logical volumescommunity.general.filesystemfor XFS (and other supported types)ansible.builtin.filefor mount point directoriesansible.posix.mountfor persistent mounts in/etc/fstab- Safe verification with
lsblk,vgs,lvs,df, andfindmnt - Extending a logical volume with
resizefs: true
| Cover here | Avoid here |
|---|---|
| Partition creation | Complex disk-recovery scenarios |
lvg and lvol basics |
Full LVM theory |
| Filesystem creation | Deep XFS/ext4 tuning |
| Persistent mounts | Full /etc/fstab reference |
| Safe verification commands | Disaster recovery / root filesystem migration |
| Basic LV extend | Shrinking filesystems or risky production resize workflows |
Why Manage LVM and Mounts with Ansible?
Manual parted / lvcreate / mkfs |
Ansible storage modules |
|---|---|
| Scripts re-run blindly | state: present and state: mounted are idempotent |
| Easy to partition the wrong disk | Variables + pre-flight lsblk checks |
/etc/fstab typos break boot |
ansible.posix.mount reduces manual edits and can mount immediately for validation |
| Resize steps scattered in notes | lvol with absolute size and resizefs: true in one task |
Storage tasks appear in baseline and application-server plays. Install lvm2, parted, and xfsprogs with manage packages on Rocky Linux before the storage play runs. When the mounted tree serves network traffic, follow with firewalld and SELinux automation on the same host. Keeping storage work in modules (not raw shell) matches idempotency practice and reads clearly in playbook structure. Use FQCN in playbooks—see modules and ansible-doc.
Storage Workflow: Partition, LVM, Filesystem and Mount
Run tasks in this order every time:
- Confirm the target disk with
lsblk - Create a partition (
community.general.parted) - Create a volume group (
community.general.lvg) - Create a logical volume (
community.general.lvol) - Create a filesystem (
community.general.filesystem) - Create the mount point directory (
ansible.builtin.file) - Mount persistently (
ansible.posix.mount)
Creating a filesystem before the logical volume exists, or mounting before the directory exists, produces errors that are easy to avoid by following the sequence above.
Prerequisites on Rocky Linux
- Managed host running Rocky Linux 10 with
lvm2,parted, andxfsprogsinstalled (default on server images) ansible-coreon the control node and Python on the target- A spare empty disk (for example
/dev/sdb) or an isolated lab disk—never practice on your OS disk become: true(or-b) on storage taskscommunity.generalandansible.posixcollections on the control node
Quick connectivity check:
ansible lab -m ansible.builtin.pingSample output:
localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}Required Ansible Collections and Packages
LVM and partition modules are not part of ansible-core. The lvg module documentation states that lvg and lvol require community.general.
Pin compatible collection versions in requirements.yml next to your playbooks:
---
collections:
- name: community.general
version: "9.5.2"
- name: ansible.posix
version: "2.1.0"Install from that file on the control node:
ansible-galaxy collection install -r requirements.ymlSample output:
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Nothing to do. All requested collections are already installed.Confirm installed versions:
ansible-galaxy collection list community.generalSample output:
Collection Version
----------------- -------
community.general 9.5.2ansible-galaxy collection list ansible.posixSample output:
Collection Version
------------- -------
ansible.posix 2.1.0Version 9.5.2 of community.general matches ansible-core 2.16.x used in this course. Newer collection releases may require ansible-core 2.18 or newer—pin versions in requirements.yml rather than installing latest blindly.
On the managed host, confirm storage tools are present:
rpm -q lvm2 parted xfsprogsSample output:
lvm2-2.03.36-2.el10.x86_64
parted-3.6-7.el10.x86_64
xfsprogs-6.16.0-1.el10.x86_64Without community.general, playbooks fail with couldn't resolve module/action 'community.general.lvg'.
Choose the Target Disk Safely
Before any partition task, inspect block devices and confirm the target disk has no mountpoints and no existing data you need.
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTPick a disk with no MOUNTPOINT and no child partitions—typically /dev/sdb on a VM with a second virtual disk.
Store paths in group variables:
# group_vars/lab.yml
storage_disk: /dev/sdb
storage_partition: /dev/sdb1
vg_name: vg_data
lv_name: lv_app
lv_size: 1g
fs_type: xfs
mount_point: /data/appAdd pre-flight tasks in the play:
- name: Inspect target disk before partitioning
ansible.builtin.command: lsblk -n -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT "{{ storage_disk }}"
register: disk_check
changed_when: false
- name: Show target disk layout
ansible.builtin.debug:
var: disk_check.stdout_lines
- name: Stop if target disk already appears to contain data
ansible.builtin.fail:
msg: "Refusing to partition {{ storage_disk }}. It has child devices, a filesystem, or a mountpoint."
when:
- disk_check.stdout_lines | length > 1
or (disk_check.stdout is search('xfs|ext4|swap|LVM2_member'))
or (disk_check.stdout is search('/'))Sample output:
[
"sdb 2G disk"
]This guard catches child partitions or LVM layers (stdout_lines | length > 1) and also stops when FSTYPE or MOUNTPOINT columns suggest existing use—for example a whole-disk XFS filesystem without partitions. The search filters are not perfect parsers; still review the FSTYPE and MOUNTPOINT columns manually before you partition.
Use --check as an extra preview, but do not rely on check mode alone for storage safety. Always confirm the disk path and current lsblk output before running partition or filesystem tasks.
Create a Partition with community.general.parted
Create a GPT partition spanning the disk with the LVM flag set:
- name: Create LVM partition on target disk
community.general.parted:
device: "{{ storage_disk }}"
label: gpt
number: 1
name: data1
part_start: 0%
part_end: 100%
flags:
- lvm
state: presentansible-playbook playbooks/01-partition.ymlSample output:
TASK [Create LVM partition on target disk] *************************************
changed: [localhost]Verify the partition exists:
lsblk -o NAME,SIZE,TYPE,FSTYPE "{{ storage_disk }}"Sample output:
sdb 2G disk
└─sdb1 2G part LVM2_memberOfficial reference: parted module.
Create a Volume Group with community.general.lvg
Point the volume group at the partition you just created:
- name: Create vg_data volume group
community.general.lvg:
vg: "{{ vg_name }}"
pvs: "{{ storage_partition }}"ansible-playbook playbooks/02-lvg.ymlSample output:
TASK [Create vg_data volume group] *********************************************
changed: [localhost]vgs "{{ vg_name }}"Sample output:
VG #PV #LV #SN Attr VSize VFree
vg_data 1 0 0 wz--n- 2.00g 2.00gFor a new VG with one PV, pvs: "{{ storage_partition }}" is fine. In newer community.general versions (10.4.0+), lvg also supports remove_extra_pvs: false to avoid removing unlisted PVs from an existing VG. That option is not available in the pinned community.general 9.5.2 used in this lab, so do not use this article's new-VG example as an existing-VG expansion play without checking your installed collection version and module options first.
Official reference: lvg module.
Create a Logical Volume with community.general.lvol
Create the logical volume inside the volume group:
- name: Create lv_app logical volume
community.general.lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: "{{ lv_size }}"
state: presentansible-playbook playbooks/03-lvol.ymlSample output:
TASK [Create lv_app logical volume] ********************************************
changed: [localhost]lvs "{{ vg_name }}/{{ lv_name }}"Sample output:
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lv_app vg_data -wi-a----- 1.00gOfficial reference: lvol module.
Create a Filesystem with community.general.filesystem
Format the logical volume—XFS is the default on Rocky Linux 10:
- name: Create XFS on logical volume
community.general.filesystem:
fstype: "{{ fs_type }}"
dev: "/dev/{{ vg_name }}/{{ lv_name }}"
state: presentansible-playbook playbooks/04-filesystem.ymlSample output:
TASK [Create XFS on logical volume] ********************************************
changed: [localhost]blkid /dev/vg_data/lv_appSample output:
/dev/vg_data/lv_app: UUID="dadd3623-c7b2-49e7-ace3-2c2dfaa6d185" BLOCK_SIZE="512" TYPE="xfs"Official reference: filesystem module.
Create a Mount Point Directory
Create the directory before calling the mount module:
- name: Ensure mount point exists
ansible.builtin.file:
path: "{{ mount_point }}"
state: directory
mode: '0755'ansible-playbook playbooks/05-mountpoint.ymlSample output:
TASK [Ensure mount point exists] ***********************************************
changed: [localhost]Mounting without the directory fails with a clear path error—create the directory first.
Mount Filesystem Persistently with ansible.posix.mount
ansible.posix.mount with state: mounted adds an /etc/fstab entry and mounts the filesystem:
- name: Mount lv_app in fstab
ansible.posix.mount:
path: "{{ mount_point }}"
src: "/dev/{{ vg_name }}/{{ lv_name }}"
fstype: "{{ fs_type }}"
opts: defaults
state: mounted
backup: trueansible-playbook playbooks/06-mount.ymlSample output:
TASK [Mount lv_app in fstab] ***************************************************
changed: [localhost]grep '{{ mount_point }}' /etc/fstabSample output:
/dev/vg_data/lv_app /data/app xfs defaults 0 0For environments where device names might change, capture the UUID with blkid and set src: "UUID=<uuid>" instead. LVM device mapper paths are usually stable on the same host.
Official reference: mount module.
Verify LVM, Filesystem and Mount Status
After the play, confirm each layer:
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTSample output:
sdb 2G disk
└─sdb1 2G part LVM2_member
└─vg_data-lv_app 1.2G lvm xfs /data/appdf -h /data/appSample output:
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg_data-lv_app 1.2G 55M 1.1G 5% /data/appfindmnt /data/appSample output:
TARGET SOURCE FSTYPE OPTIONS
/data/app /dev/mapper/vg_data-lv_app xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquotaInside playbooks, register lsblk or df output with changed_when: false when you need assertions—see debug playbook techniques for register and assert patterns.
Resize Logical Volumes and Filesystems Safely
To extend a volume to a known final size, set an absolute size and resizefs: true so the filesystem grows with the logical volume:
- name: Ensure lv_app is 1200M with filesystem resize
community.general.lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: 1200M
resizefs: true
shrink: falseansible-playbook playbooks/07-extend-lv.ymlSample output:
TASK [Ensure lv_app is 1200M with filesystem resize] *****************************
changed: [localhost]lvs vg_data/lv_app -o lv_size --noheadingsSample output:
1.20gA second run with the same size: 1200M stays ok with changed: false—absolute sizes are idempotent.
size: +200M is useful for a one-time extension task, but it is not idempotent. The lvol module documentation states that when using +, -, or a percentage of FREE, each rerun can extend or reduce again. For repeatable desired state, use an absolute final size such as size: 1200M.
Set shrink: false to avoid accidental shrink if someone lowers the target size later—the module defaults shrink to true, and shrinking requires careful handling.
force on lvol and can destroy data. XFS can grow online, but it cannot be shrunk. Re-run lsblk and confirm storage_disk / storage_partition still point at the intended spare device before any shrink or resize task—never assume device names stayed the same after a reboot or VM clone. If you need shrink workflows, use a separate recovery-style procedure with backup and restore planning—not a routine playbook task.
Common Examples
Create a new LVM volume and mount it
Full play chaining every step (see playbooks/full-lvm-mount.yml in the lab):
- name: Create LVM partition
community.general.parted:
device: "{{ storage_disk }}"
label: gpt
number: 1
name: data1
part_start: 0%
part_end: 100%
flags: [lvm]
state: present
- name: Create volume group
community.general.lvg:
vg: "{{ vg_name }}"
pvs: "{{ storage_partition }}"
- name: Create logical volume
community.general.lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: "{{ lv_size }}"
state: present
- name: Create XFS filesystem
community.general.filesystem:
fstype: xfs
dev: "/dev/{{ vg_name }}/{{ lv_name }}"
state: present
- name: Create mount point
ansible.builtin.file:
path: "{{ mount_point }}"
state: directory
mode: '0755'
- name: Mount persistently
ansible.posix.mount:
path: "{{ mount_point }}"
src: "/dev/{{ vg_name }}/{{ lv_name }}"
fstype: xfs
opts: defaults
state: mounted
backup: trueCreate an XFS filesystem on a logical volume
- name: Format logical volume as XFS
community.general.filesystem:
fstype: xfs
dev: /dev/vg_data/lv_app
state: presentAdd a persistent mount to /etc/fstab
- name: Persistent mount for application data
ansible.posix.mount:
path: /data/app
src: /dev/vg_data/lv_app
fstype: xfs
opts: defaults
state: mounted
backup: trueExtend a logical volume
- name: Grow lv_app and XFS to a fixed size
community.general.lvol:
vg: vg_data
lv: lv_app
size: 1200M
resizefs: true
shrink: falseVerify mount and filesystem usage
- name: Check mount is active
ansible.builtin.command: findmnt -n /data/app
register: mount_check
changed_when: false
- name: Show mount source
ansible.builtin.debug:
var: mount_check.stdoutCommon Mistakes
| Mistake | Fix |
|---|---|
| Running storage tasks on wrong disk | Verify disk with lsblk before partitioning |
| Forgetting required collection | Install community.general |
| Creating filesystem before LV exists | Follow storage order |
| Mounting before mount directory exists | Create directory with ansible.builtin.file first |
| Using device name in fstab when UUID is safer | Prefer stable identifiers where practical |
Forgetting become: true |
Storage tasks need root |
| Using shell commands for everything | Prefer Ansible storage modules |
| Shrinking LVM/filesystem casually | Avoid or handle only with backups and explicit checks |
| Wrong partition path on loop devices | SCSI uses sdb1; loop uses loop0p1 |
Re-running lvol with size: +200M |
Relative + sizes are not idempotent—use absolute size: 1200M |
Re-running lvol with smaller absolute size |
Set shrink: false; shrinking needs force and risks data loss |
Expanding an existing VG with the new-VG lvg example |
remove_extra_pvs needs community.general 10.4.0+; pinned 9.5.2 lacks it |
Best Practices
| Practice | Why |
|---|---|
| Store disk paths in variables | One place to review before destructive tasks |
Run lsblk before parted |
Catches wrong-disk mistakes early |
Pin community.general in requirements.yml |
Avoids ansible-core / collection version mismatch |
| Follow partition → VG → LV → FS → mount order | Each step depends on the previous |
Use absolute size when extending LVs |
size: +200M extends on every rerun |
Set shrink: false on extend tasks |
Prevents accidental shrink if size variable drops |
Use resizefs: true when extending |
Filesystem size matches LV without a separate task |
Set backup: true on mount when editing fstab |
Roll back if a play misconfigures entries |
Prefer ansible.posix.mount over hand-edited fstab |
Idempotent fstab management |
Verify with findmnt and df after the play |
Confirms mount is live, not just in fstab |
| Move repeated storage stacks into roles | Reuse across data and app tiers |
| Test on a spare disk or loopback image | Never experiment on the OS disk |
Summary
On Rocky Linux 10, build storage with Ansible in order: community.general.parted partitions the disk, community.general.lvg and community.general.lvol create LVM layers, community.general.filesystem formats XFS (or another supported type), ansible.builtin.file creates the mount point, and ansible.posix.mount writes /etc/fstab and mounts the filesystem. Pin community.general 9.5.2 in requirements.yml, confirm the target disk with lsblk and a fail guard, and verify with vgs, lvs, df, and findmnt. Extend volumes with an absolute size and resizefs: true; XFS cannot shrink—avoid casual shrink operations.
References
- community.general.parted module
- community.general.lvg module
- community.general.lvol module
- community.general.filesystem module
- ansible.posix.mount module

