Red Hat Certified Engineer (RHCE) #16: RHCSA automation 3 — storage (LVM), filesystems (NFS)
In #15 RHCSA automation 2: services, chronyd, log we handled services, time synchronization, and logging with a playbook. This time we automate storage with Ansible — the most hands-intensive area of RHCSA. The goal is to express the whole sequence as idempotent tasks: create partitions on a disk, build a VG and LV with LVM, lay down a filesystem, and chain all the way to a persistent mount.
Storage is a regular on the RHCE practical exam. The work you learned by hand in RHCSA through the LVM series and the NFS/autofs series will be moved into a playbook that produces the same result.
The full picture of storage automation #
If you recall the storage work you used to do by hand, it ran in this order. Create a partition on the disk (parted), take that partition as a PV and build a VG (vgcreate), carve an LV out of the VG (lvcreate), format the LV (mkfs), then create a mount point, register it in /etc/fstab, and actually mount it.
Ansible provides a dedicated module for each of these steps.
| Manual work | Ansible module |
|---|---|
parted partition | community.general.parted |
vgcreate (VG) | community.general.lvg |
lvcreate (LV) | community.general.lvol |
mkfs format | community.general.filesystem |
/etc/fstab + mount | ansible.posix.mount |
The key here is that idempotency is guaranteed differently from module to module. lvg and filesystem simply skip if the resource already exists, but parted and lvol can be flagged as changed on every run, or can re-touch the size, if you pass the wrong options — so they need care.
parted: creating partitions #
community.general.parted creates a partition table and partitions on a disk. For idempotency, it’s safest to specify number (the partition number) along with the start and end positions.
- name: Create one partition on the disk
community.general.parted:
device: /dev/vdb
number: 1
state: present
part_start: 1MiB
part_end: 1024MiBIf you give the same number with the same part_start/part_end, the second run produces no changed. Conversely, giving the end position as a relative value like 100% can cause it to be recalculated on every run depending on the environment, which shakes idempotency — so an explicit size is recommended.
For a partition meant for LVM, you sometimes set the partition flag to LVM.
- name: Create a partition for LVM
community.general.parted:
device: /dev/vdb
number: 1
state: present
part_start: 1MiB
part_end: 2048MiB
flags: [ lvm ]That said, if the exam gives you a disk to use whole as a PV, you can hand /dev/vdb itself to lvg without any partition. If the question’s wording explicitly says “create a partition,” use parted; otherwise, using the whole disk as a PV is simpler.
lvg: creating a VG #
community.general.lvg bundles PVs into a VG. Hand it a disk or partition in pvs and it handles PV creation and VG creation in one shot.
- name: Create a VG
community.general.lvg:
vg: data_vg
pvs: /dev/vdb1
state: presentIf the VG already exists and is built from the same PVs, no changed occurs. To bundle multiple PVs, pass them as a list.
- name: Create a VG from multiple PVs
community.general.lvg:
vg: data_vg
pvs:
- /dev/vdb1
- /dev/vdc1lvol: creating an LV #
community.general.lvol carves an LV out of a VG. You specify the size with size, and it accepts both absolute sizes (2g, 500m) and relative ratios (50%VG, 100%FREE).
- name: Create an LV
community.general.lvol:
vg: data_vg
lv: data_lv
size: 1g
state: presentThe point where lvol breaks idempotency most easily is a size change. If you give a different size to an already-created LV, the module may try to shrink it, and shrinking leads to data loss. There are two options that prevent this.
shrink: false. Blocks the shrink action and prevents an LV from accidentally getting smaller.resizefs: true. When the LV size changes, it grows the filesystem on top of it too, keeping them consistent.
- name: Manage LV size safely
community.general.lvol:
vg: data_vg
lv: data_lv
size: 2g
shrink: false
resizefs: trueIn the exam, “create an LV of N gigabytes” is done once and you’re finished, but if “extend an existing LV” comes up, recall the pattern of growing the filesystem alongside it with resizefs: true.
filesystem: formatting #
community.general.filesystem lays a filesystem onto an LV or partition. You specify fstype and the target device (dev).
- name: Format the LV as xfs
community.general.filesystem:
fstype: xfs
dev: /dev/data_vg/data_lvIf that filesystem already exists, no changed occurs. To force a re-creation you can pass force: true, but since it wipes existing data, you’ll almost never use it on the exam. The target device path works in either the /dev/<vg>/<lv> or the /dev/mapper/<vg>-<lv> form.
mount: registering in fstab and the live mount #
ansible.posix.mount handles the /etc/fstab entry and the live mount together. The key is the state value.
| state | Action |
|---|---|
mounted | Register in fstab and mount right now |
present | Register in fstab only (don’t mount now) |
unmounted | Unmount now (keep the fstab entry) |
absent | Unmount and remove the fstab entry too |
If a persistent mount is the goal, it’s almost always state: mounted. Since it does the fstab registration and the live mount in one shot, the mount survives reboots while also being usable right now.
- name: Create the mount point directory
ansible.builtin.file:
path: /data
state: directory
mode: '0755'
- name: Mount the LV persistently
ansible.posix.mount:
path: /data
src: /dev/data_vg/data_lv
fstype: xfs
state: mountedYou can also give src as UUID=... or LABEL=... instead of a device path. To mount safely even if the device name changes, using a UUID is more robust.
Adding swap #
Swap is handled with the same module combination. Create the swap signature with fstype: swap in filesystem, then register it in fstab with the mount module. Since swap isn’t mounted on a directory, it’s common to register it in fstab only with path: none, fstype: swap, state: present and then activate it.
- name: Format the swap LV
community.general.filesystem:
fstype: swap
dev: /dev/data_vg/swap_lv
- name: Register the swap in fstab and activate it
ansible.posix.mount:
path: none
src: /dev/data_vg/swap_lv
fstype: swap
opts: sw
state: presentAfter registering in fstab with state: present, activate swap with command: swapon -a, or leave that step to a storage role that handles swap.
NFS remote mount #
An NFS mount is handled with the same ansible.posix.mount module. The only differences from a local filesystem are that fstype: nfs and src is in the server:exported_path form.
- name: Mount an NFS export persistently
ansible.posix.mount:
path: /mnt/share
src: nfs-server.example.com:/exports/share
fstype: nfs
opts: defaults,_netdev
state: mountedMake a habit of putting _netdev in opts so the mount happens after the network is up. If the NFS client package (nfs-utils) is missing, the mount fails, so it’s safe to put a package-install task in front of it.
- name: Install the NFS client package
ansible.builtin.dnf:
name: nfs-utils
state: presentA full LVM playbook example #
Tying together the tasks so far into a single flow gives the following. It’s the standard answer for the exam type that says “build a VG/LV from a disk, format it as xfs, and mount it persistently on /data.”
---
- name: Configure LVM storage
hosts: storage
become: true
tasks:
- name: Create a VG
community.general.lvg:
vg: data_vg
pvs: /dev/vdb
- name: Create an LV
community.general.lvol:
vg: data_vg
lv: data_lv
size: 1g
shrink: false
- name: Create the filesystem
community.general.filesystem:
fstype: xfs
dev: /dev/data_vg/data_lv
- name: Create the mount point
ansible.builtin.file:
path: /data
state: directory
mode: '0755'
- name: Mount persistently
ansible.posix.mount:
path: /data
src: /dev/data_vg/data_lv
fstype: xfs
state: mountedAlways run this playbook twice and confirm the second run yields zero changed. Since lvg/lvol/filesystem/mount all support idempotency, if you wrote it correctly every task is reported as ok on re-run.
The storage system role alternative #
The official role that bundles the work above in one go is the storage role of rhel-system-roles. It belongs to the system role family covered in #13 system roles, and declares PV, VG, LV, filesystem, and mount with a single variable.
---
- name: Configure storage with the storage role
hosts: storage
become: true
roles:
- rhel-system-roles.storage
vars:
storage_pools:
- name: data_vg
disks:
- vdb
volumes:
- name: data_lv
size: 1g
fs_type: xfs
mount_point: /dataIf you declare disks and volumes under storage_pools, the role internally handles VG/LV creation, formatting, fstab registration, and mounting all at once. Swap can also be declared in the same structure with fs_type: swap. Both wiring individual modules yourself and using the storage role are accepted on the exam, so use whichever you are more comfortable with. That said, if the wording specifies a particular module, using that module is the safe choice.
Exam points #
- The
mountmodule’sstate: mountedhandles fstab registration and the live mount in one shot. It’s the standard answer for persistent-mount questions.presentregisters in fstab only, so it misses the immediate mount. partedandlvolbreak idempotency easily. Specify start and end positions forparted, and block shrinking withshrink: falseforlvol.- For LV extension, pass
resizefs: truetolvolto grow the filesystem along with it. - Handle NFS with
fstype: nfs, aserver:path-formsrc, andopts: _netdevonansible.posix.mount, and put annfs-utilsinstall task in front. - Most storage modules belong to the
community.generalcollection, andmountbelongs toansible.posix. Confirm the FQCN and whether the collection is installed.
Wrap-up #
What this post locked in:
- The step-by-step modules of storage automation.
parted(partition) →lvg(VG) →lvol(LV) →filesystem(format) →mount(fstab + mount) - Shrink prevention and simultaneous filesystem extension via
lvol’sshrink: falseandresizefs: true - The meaning of the
mountmodule’sstate.mountedis the right answer for a persistent mount - Swap is handled with
filesystem’sfstype: swapandmount’spath: none - An NFS remote mount uses
fstype: nfs, the_netdevoption, and annfs-utilsinstall - The storage role of rhel-system-roles as an alternative to combining individual modules
Next: RHCSA automation 4 #
We’ve automated all the way to storage. The last piece of RHCSA automation is the security area.
In #17 RHCSA automation 4: firewall, SELinux, SSH keys, we’ll handle firewalld with ansible.posix.firewalld, automate SELinux modes and booleans/port labels with ansible.posix.selinux and community.general.sefcontext, and tie SSH public key distribution into the playbook as well.