Manage LVM, Filesystems and Mounts on Rocky Linux with Ansible

Build Rocky Linux 10 storage with Ansible in order—partition a safe target disk, create LVM volume groups and logical volumes, format XFS, mount persistently in /etc/fstab, verify with lsblk/findmnt, and extend volumes safely.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Ansible LVM partition, volume group, logical volume, filesystem and mount modules for Rocky Linux 10

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.general 9.5.2; ansible.posix 2.1.0.

IMPORTANT

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.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/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.parted for GPT partitions with the LVM flag
  • community.general.lvg and community.general.lvol for volume groups and logical volumes
  • community.general.filesystem for XFS (and other supported types)
  • ansible.builtin.file for mount point directories
  • ansible.posix.mount for persistent mounts in /etc/fstab
  • Safe verification with lsblk, vgs, lvs, df, and findmnt
  • 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:

  1. Confirm the target disk with lsblk
  2. Create a partition (community.general.parted)
  3. Create a volume group (community.general.lvg)
  4. Create a logical volume (community.general.lvol)
  5. Create a filesystem (community.general.filesystem)
  6. Create the mount point directory (ansible.builtin.file)
  7. 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, and xfsprogs installed (default on server images)
  • ansible-core on 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 tasks
  • community.general and ansible.posix collections on the control node

Quick connectivity check:

bash
ansible lab -m ansible.builtin.ping

Sample output:

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:

yaml
---
collections:
  - name: community.general
    version: "9.5.2"
  - name: ansible.posix
    version: "2.1.0"

Install from that file on the control node:

bash
ansible-galaxy collection install -r requirements.yml

Sample output:

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:

bash
ansible-galaxy collection list community.general

Sample output:

output
Collection        Version
----------------- -------
community.general 9.5.2
bash
ansible-galaxy collection list ansible.posix

Sample output:

output
Collection    Version
------------- -------
ansible.posix 2.1.0

Version 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:

bash
rpm -q lvm2 parted xfsprogs

Sample output:

output
lvm2-2.03.36-2.el10.x86_64
parted-3.6-7.el10.x86_64
xfsprogs-6.16.0-1.el10.x86_64

Without 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.

bash
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT

Pick 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:

yaml
# 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/app

Add pre-flight tasks in the play:

yaml
- 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:

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:

yaml
- 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: present
bash
ansible-playbook playbooks/01-partition.yml

Sample output:

output
TASK [Create LVM partition on target disk] *************************************
changed: [localhost]

Verify the partition exists:

bash
lsblk -o NAME,SIZE,TYPE,FSTYPE "{{ storage_disk }}"

Sample output:

output
sdb      2G disk
└─sdb1   2G part LVM2_member

Official reference: parted module.


Create a Volume Group with community.general.lvg

Point the volume group at the partition you just created:

yaml
- name: Create vg_data volume group
  community.general.lvg:
    vg: "{{ vg_name }}"
    pvs: "{{ storage_partition }}"
bash
ansible-playbook playbooks/02-lvg.yml

Sample output:

output
TASK [Create vg_data volume group] *********************************************
changed: [localhost]
bash
vgs "{{ vg_name }}"

Sample output:

output
VG      #PV #LV #SN Attr   VSize  VFree
  vg_data   1   0   0 wz--n-  2.00g 2.00g

For 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:

yaml
- name: Create lv_app logical volume
  community.general.lvol:
    vg: "{{ vg_name }}"
    lv: "{{ lv_name }}"
    size: "{{ lv_size }}"
    state: present
bash
ansible-playbook playbooks/03-lvol.yml

Sample output:

output
TASK [Create lv_app logical volume] ********************************************
changed: [localhost]
bash
lvs "{{ vg_name }}/{{ lv_name }}"

Sample output:

output
LV     VG      Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  lv_app vg_data -wi-a----- 1.00g

Official reference: lvol module.


Create a Filesystem with community.general.filesystem

Format the logical volume—XFS is the default on Rocky Linux 10:

yaml
- name: Create XFS on logical volume
  community.general.filesystem:
    fstype: "{{ fs_type }}"
    dev: "/dev/{{ vg_name }}/{{ lv_name }}"
    state: present
bash
ansible-playbook playbooks/04-filesystem.yml

Sample output:

output
TASK [Create XFS on logical volume] ********************************************
changed: [localhost]
bash
blkid /dev/vg_data/lv_app

Sample output:

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:

yaml
- name: Ensure mount point exists
  ansible.builtin.file:
    path: "{{ mount_point }}"
    state: directory
    mode: '0755'
bash
ansible-playbook playbooks/05-mountpoint.yml

Sample output:

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:

yaml
- 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: true
bash
ansible-playbook playbooks/06-mount.yml

Sample output:

output
TASK [Mount lv_app in fstab] ***************************************************
changed: [localhost]
bash
grep '{{ mount_point }}' /etc/fstab

Sample output:

output
/dev/vg_data/lv_app /data/app xfs defaults 0 0

For 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:

bash
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT

Sample output:

output
sdb                       2G disk
└─sdb1                    2G part LVM2_member
  └─vg_data-lv_app      1.2G lvm  xfs         /data/app
bash
df -h /data/app

Sample output:

output
Filesystem                  Size  Used Avail Use% Mounted on
/dev/mapper/vg_data-lv_app  1.2G   55M  1.1G   5% /data/app
bash
findmnt /data/app

Sample output:

output
TARGET    SOURCE                     FSTYPE OPTIONS
/data/app /dev/mapper/vg_data-lv_app xfs    rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota

Inside 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:

yaml
- name: Ensure lv_app is 1200M with filesystem resize
  community.general.lvol:
    vg: "{{ vg_name }}"
    lv: "{{ lv_name }}"
    size: 1200M
    resizefs: true
    shrink: false
bash
ansible-playbook playbooks/07-extend-lv.yml

Sample output:

output
TASK [Ensure lv_app is 1200M with filesystem resize] *****************************
changed: [localhost]
bash
lvs vg_data/lv_app -o lv_size --noheadings

Sample output:

output
1.20g

A 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.

IMPORTANT
Do not casually shrink logical volumes or filesystems. Shrinking requires 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):

yaml
- 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: true

Create an XFS filesystem on a logical volume

yaml
- name: Format logical volume as XFS
  community.general.filesystem:
    fstype: xfs
    dev: /dev/vg_data/lv_app
    state: present

Add a persistent mount to /etc/fstab

yaml
- 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: true

Extend a logical volume

yaml
- name: Grow lv_app and XFS to a fixed size
  community.general.lvol:
    vg: vg_data
    lv: lv_app
    size: 1200M
    resizefs: true
    shrink: false

Verify mount and filesystem usage

yaml
- 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.stdout

Common 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

Frequently Asked Questions

1. Are LVM modules included in ansible-core?

No. community.general.lvg, community.general.lvol, community.general.filesystem, and community.general.parted live in the community.general collection. Pin community.general 9.5.2 in requirements.yml and install with ansible-galaxy collection install -r requirements.yml on the control node.

2. What order should storage tasks run in a playbook?

Partition the disk, create the volume group, create the logical volume, create the filesystem, create the mount point directory, then mount with ansible.posix.mount. Skipping a step or reversing filesystem and LV order causes predictable failures.

3. Should I use /dev/sdb1 or a UUID in /etc/fstab?

Either works when managed with ansible.posix.mount. Device mapper paths such as /dev/vg_data/lv_app are stable on LVM hosts. UUIDs are safer when device names might change; use blkid to capture the UUID if you prefer that style.

4. Can I shrink a logical volume with Ansible?

Shrinking requires force and risks data loss. XFS can grow online but cannot be shrunk. This guide extends volumes with absolute size and resizefs. Avoid casual shrink operations in production; back up first and treat shrink as a separate recovery procedure.

5. How do I avoid partitioning the wrong disk?

Run lsblk and confirm the target disk has no partitions and is not mounted before any parted task. Store the disk path in a variable such as storage_disk and review it in check mode or with a debug task first.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …