Managing files and file systems is the most fundamental job of any operating system, and Ansible automates it so you don't have to log into each server and type fragile commands by hand. For the EX294 exam, you need to know exactly which module does what, how to set permissions securely, and how to mount file systems reliably across hundreds of machines at once.
Jump to a section
A simple way to picture Managing Files and File Systems with Ansible
Two friends deciding to renovate a fixer-upper house.
One friend crawls under the house with a hammer and starts yanking out nails, hoping to remove a wall. The other friend runs around, painting random rooms and then repainting them when the owner changes their mind. Total chaos. After a week, the house looks worse, they've ruined a load-bearing wall, and they're covered in paint they didn't need to use. That is managing files and file systems by manually logging into each server and typing commands by hand. It's slow, reckless, and impossible to track.
Now imagine the same house, but they first draw a blueprint. The blueprint says 'remove wall 3, install new window in room 7, paint all bedrooms blue, and set the thermostat to 72 degrees.' They hand that blueprint to a team of workers. Every worker does exactly what the blueprint says, in exactly the same order, every single time. No nail is pulled without the blueprint saying so. If the owner changes their mind, they update the blueprint, not run around repainting. That is Ansible managing files and files systems. The blueprint is an Ansible playbook. The workers are the Ansible modules like file, copy, lineinfile, and mount. The house is your file system. The thermostat is the file permission setting. Ansible ensures every single server ends up with identical files, permissions, and disk configurations, every time, traceable and repeatable.
At its core, file management with Ansible means using specialised modules to create, copy, edit, set permissions for, and delete files on remote servers. Instead of manually SSH-ing into a server and running touch, cp, chmod, or vim, you write a YAML playbook that declares what the final state of each file should be. Ansible then makes that state happen. This is called 'desired state management'.
Ansible uses a collection of built-in modules specifically for files. The most important ones for EX294 are:
file: Creates directories, creates empty files, removes files, sets ownership and permissions, and creates symbolic links.
copy: Copies a file from the control node (your laptop or a jump box) to the remote server. It can also set permissions when it places the file.
template: Like copy, but it uses Jinja2 templating to insert variables into the file before placing it on the remote server. For example, you could insert the server's hostname into a config file.
lineinfile: Edits a single line in an existing file. If the line exists, it can replace it. If it does not exist, it can add it. This is perfect for adding one setting to a configuration file without replacing the whole file.
blockinfile: Inserts, updates, or removes a multi-line block of text within a file. It marks the block with unique marker comments so it can find and replace the exact block later.
mount: Controls the mounting and unmounting of file systems, like attaching a new hard drive or a network share (NFS).
replace: Replaces all occurrences of a particular string in a file with another string. Think of it as a global find-and-replace.
sysctl: Manages kernel parameters that affect file system behaviour, though it is less common for basic file management.
Permissions in Linux are represented by a three-digit number, for example 755 or 644. The first digit is the owner's permission, the second is the group's permission, and the third is everyone else's. Each digit is a sum of read (4), write (2), and execute (1). Ansible's file module accepts a mode parameter where you specify these permissions as a string, like '0644'. The leading zero is important because it tells Ansible to treat it as an octal number.
Ownership is controlled with the owner and group parameters. You specify the username, like 'root' or 'nginx', and the group name, like 'www-data'. Ansible will check the current state and change it if it does not match the playbook.
The real power is combining these modules with variables and loops. For example, you can create a list of users and a list of home directories, then use a loop to create all directories in one playbook task. This is what replaces manual repetition.
The mount module requires special attention for the exam. It has two states: 'mounted' and 'present'. 'Mounted' means Ansible mounts the file system now and also adds it to /etc/fstab so it mounts automatically on boot. 'Present' means Ansible adds it to /etc/fstab but does not mount it now. You must know the difference because the exam tests this contrast.
Why does this exist? Before tools like Ansible, system administrators would write long shell scripts with many if statements to handle different server states. Those scripts were brittle and often failed on slightly different server configurations. Ansible's declarative model means you only say what you want the end result to be. Ansible figures out the steps and reports back if it cannot make it happen.
Define the Desired State
Before writing any Ansible code, decide exactly what the file system should look like. Which files exist? What are their permissions? Who owns them? What is their content? Write this down. This is your blueprint. For example: '/etc/myapp.conf must exist, owned by root, mode 0644, with content that includes the server's hostname.'
Choose the Correct Module
Select the Ansible module that matches the operation. For creating directories, choose 'file' with 'state: directory'. For copying a static file, choose 'copy'. For a file with variables, choose 'template'. For editing one line, choose 'lineinfile'. Selecting the wrong module is the most common mistake. The exam expects you to match the module to the exact task.
Write the Task in the Playbook
In your YAML playbook, write a task with the module name as the first key. Add parameters like 'path', 'src', 'dest', 'owner', 'group', 'mode', 'state', and 'content' as needed. For example: '- name: Ensure config directory exists\n ansible.builtin.file:\n path: /etc/myapp\n state: directory\n mode: '0755''. This step translates your blueprint into executable code.
Test the Task on a Single Host
Run the playbook against a single test server first using the '--limit' option, e.g. 'ansible-playbook -i inventory playbook.yml --limit testserver'. This catches errors like missing parent directories, permission issues, or typos in variable names without affecting all servers. Always check the 'changed' count to see if Ansible actually modified anything.
Verify the Result with an Ad-Hoc Command
After the playbook runs, use an ad-hoc command to check the file's properties on the remote host. For example: 'ansible testserver -m ansible.builtin.stat -a "path=/etc/myapp/conf"'. This shows permissions, owner, and file type. If the state does not match your intent, adjust the playbook. This step is crucial for learning because you see exactly what Ansible did versus what you expected.
Scale to All Hosts
Once the playbook works perfectly on the test host, run it against the full inventory. Ansible will execute the tasks on all hosts in parallel. Monitor the output for any 'failed' or 'unreachable' hosts. If some hosts fail (e.g., because a disk is full), fix the issue and re-run the playbook. Ansible will only apply changes to hosts that are not yet in the correct state.
A financial services company needs to deploy a new compliance monitoring application across 60 Linux web servers. The application requires a specific configuration file to be present on every server, with exact permissions of 0600 (owner read/write only), owned by the 'monitor' user and the 'monitor' group. The config file contains the central logging server IP address, which changes depending on which data centre the server is in.
Here is what the IT professional does with Ansible:
They create an Ansible inventory file that groups servers by data centre. For example, group 'dc_east' and group 'dc_west'. They then create a variable file for each group that defines the logging server IP. For example, 'log_server: 10.0.1.50' for the east and '10.0.2.50' for the west.
They write a playbook that does the following:
Creates the 'monitor' user and group on all servers using the user module.
Creates the directory '/opt/monitor/conf' using the file module with state=directory, owner='monitor', group='monitor', and mode='0755'.
Copies the configuration template from the control node to each remote server using the template module. The template file (app.conf.j2) contains the Jinja2 variable '{{ log_server }}'. Ansible automatically substitutes the correct IP address based on which group the server belongs to.
Sets the permissions of the deployed config file to 0600, owner 'monitor', group 'monitor' using the file module.
Uses the lineinfile module to add a specific line to the '/etc/rsyslog.conf' file on each server to forward logs to the central logging server. This line is 'local0.* @{{ log_server }}:514'. Ansible adds the line only if it is not already present.
Uses the mount module to mount an NFS share from the central storage server to '/opt/monitor/data' on each server. The state is set to 'mounted' so it mounts immediately and persists across reboots.
Finally, they run the playbook with the command 'ansible-playbook -i inventory deploy_monitor.yml'. Ansible runs through all 60 servers in parallel (or in batches, depending on the 'forks' setting). If one server is down, Ansible reports an unreachable host and continues with the others. The IT professional can then check the error log and re-run the playbook for just that server.
The benefit is that the entire process takes under a minute instead of hours of manual SSH sessions. The process is documented (the playbook itself is the documentation) and fully repeatable. If they need to deploy to a new server next month, they just add it to the inventory and run the same playbook.
The EX294 exam tests real-world file management skills, not abstract theory. You will not be asked to explain what a file system is. You will be asked to write tasks that use the file, copy, template, lineinfile, blockinfile, and mount modules. The exam environment gives you a control node with Ansible installed and several target VMs.
Specific concepts the exam loves to test:
The difference between 'state=present' and 'state=touch' in the file module. 'Present' is for directories (it just ensures the directory exists). 'Touch' is for files (creates an empty file if it does not exist, updates timestamp if it does). This is a classic trap: beginners use 'present' for files and it fails silently.
The 'mode' parameter requires a leading zero in the string, like '0644'. Writing '644' (without the zero) works in most cases but the exam sometimes tests the correct octal notation. Memorise the leading zero.
The 'recurse' parameter for the file module. If you set permissions on a directory with 'state=directory', Ansible only changes the directory itself. To change everything inside it recursively, you need 'recurse=yes'. The exam will give you a scenario where you need to secure a directory tree.
The 'backup' parameter on the copy, template, and lineinfile modules. If you set 'backup=yes', Ansible saves a backup of the original file with a timestamp before making changes. The exam will ask you to ensure backups are kept.
The 'lineinfile' module's 'regexp' parameter for matching an existing line. If you do not provide 'regexp', the module compares the entire line. If you provide 'regexp', it matches the pattern and then uses 'line' to set the replacement. The exam often asks you to replace a specific config line that starts with a certain pattern.
The 'blockinfile' module markers. By default, it uses '# BEGIN ANSIBLE MANAGED BLOCK' and '# END ANSIBLE MANAGED BLOCK'. The exam can test that you know you can customise these markers with the 'marker' parameter.
The mount module and its 'src', 'path', 'fstype', and 'opts' parameters. The exam often sets up a scenario with an unmounted disk partition and asks you to make it mount persistently. You must provide 'src' (device or NFS export), 'path' (mount point), 'fstype' (like ext4 or nfs), and 'state=mounted'.
The difference between 'mounted' and 'present' in the mount module. 'Mounted' mounts now and persists. 'Present' only persists (updates /etc/fstab) but does not mount now. If the exam explicitly says 'make sure the file system is available immediately', you need 'mounted'.
The exam does not allow you to use arbitrary shell commands with the 'command' or 'shell' modules for file management unless explicitly instructed. They want you to use the dedicated file modules. Using 'command' with 'chmod' or 'cp' will lose marks even if it works correctly.
The file module creates directories with 'state: directory' and empty files with 'state: touch', and you must memorise the distinction for the exam.
Always use a leading zero in the 'mode' parameter, like '0644', to ensure proper octal interpretation by Ansible.
The copy module copies files from the control node to the remote host but does not create destination directories automatically; use a separate file task for that.
The lineinfile module edits a single line based on a 'regexp' pattern, while blockinfile manages multi-line blocks with unique markers.
The mount module's 'state: mounted' both mounts the file system immediately and adds it to /etc/fstab, whereas 'state: present' only updates /etc/fstab.
Idempotency in Ansible means running the same playbook twice results in the same state; file modules are idempotent by design, so they only make changes when the current state differs from the desired state.
Use the 'backup: yes' parameter on copy, template, and lineinfile to keep an original copy before any changes are made.
Templating with the template module allows you to insert variables into files, making the same playbook work across different environments by simply changing the variables.
These come up on the exam all the time. Here's how to tell them apart.
file module (state: directory)
Creates a directory path; fails if parent directories do not exist unless 'recurse' is used but also works
Returns changed only if the directory did not exist
Commonly used for ensuring application directories exist before deploying files
file module (state: touch)
Creates an empty regular file or updates the timestamp if the file already exists
Returns changed if the file was created, unchanged if it already existed (timestamp update does not count as change in some versions)
Commonly used for creating placeholder files or log file stubs
copy module
Copies a static file from the control node to the remote host; content is fixed at authoring time
Supports 'content' parameter to generate file content directly in the playbook without a source file
Best for configuration files that are identical across all target hosts
template module
Copies a file with Jinja2 templating, allowing runtime variable substitution (e.g., hostname, IP address)
The source file must be a .j2 template file; the 'content' parameter is not available
Best for configuration files that need different values on different hosts or environments
lineinfile module
Manages a single line of text; replaces or adds one line based on regex match or full line match
No marker lines are added; it operates invisibly on the existing file content
Best for ensuring a specific configuration parameter is set, e.g., 'MaxClients = 150'
blockinfile module
Manages a block of multiple lines; inserts, updates, or removes the entire block
Adds marker comments (by default '# BEGIN ANSIBLE MANAGED BLOCK' and '# END ANSIBLE MANAGED BLOCK') to identify the block
Best for inserting multi-line configuration stanzas, like a virtual host definition in Apache
mount module (state: mounted)
Mounts the file system immediately on the running system
Also adds the entry to /etc/fstab for persistence across reboots
Use when the file system must be available right now, e.g., during a playbook that writes data to the mount
mount module (state: present)
Only adds the entry to /etc/fstab; does NOT mount the file system
The mount will only become active after a reboot or a manual 'mount -a' command
Use when you want to prepare the system for a future mount without affecting the current runtime state
Mistake
I can use the 'command' module to run 'chmod' and 'chown' commands directly, which is simpler than learning the file module's parameters.
Correct
The file module is purpose-built for permissions and ownership, it handles idempotency (only changes when needed) and works more reliably across different systems. The command module breaks idempotency and is not checked for correctness by the exam grader.
Beginners find the raw command syntax familiar from the Linux command line, so they default to it instead of learning the module's declarative parameters.
Mistake
Setting 'state: directory' on the file module will create a file if I specify a path ending in a filename.
Correct
The file module with 'state: directory' will create a directory. If the path ends in something that looks like a filename (no trailing slash), it still creates a directory with that name. To create a file, you must use 'state: touch'.
The word 'file' as the module name is misleading. People assume the 'file' module is for creating files, but it is for managing properties of a file system object. The creation of an empty file is a separate mode called 'touch', which is confusing naming.
Mistake
I can just use the 'copy' module to copy a file and set its permissions at the same time, but I must also create the destination directory manually beforehand.
Correct
The 'copy' module has a 'directory_mode' parameter and also accepts 'owner', 'group', and 'mode' parameters. However, it does NOT create the parent directory structure by default. You need to use the 'file' module with 'state: directory' first, or use 'recurse: no' which is default anyway.
Beginners see that the copy module sends a file and think it is a complete file deployment tool, but it lacks directory creation. This leads to errors like 'destination directory does not exist' that confuse new users.
Mistake
Using 'state: present' with the 'mount' module is equivalent to 'state: mounted' because the mount happens automatically.
Correct
'present' only ensures the entry exists in the /etc/fstab file. It does NOT mount the file system. 'mounted' does both. If you use 'present', the mount will only happen on the next reboot or with a manual mount command.
The words 'present' and 'mounted' sound similar. Beginners think 'present' means the file system is available in the system, but it only refers to the configuration file's presence.
Mistake
I should use 'lineinfile' to add a block of multiple lines to a config file.
Correct
For multiple lines, use 'blockinfile'. The 'lineinfile' module is designed for a single line. Using 'lineinfile' with a multi-line string will treat it as one line and cause formatting errors in the target file.
Both modules start with a similar purpose (inserting text into files), so beginners think they are interchangeable. The exam explicitly tests the distinction between single-line and multi-line operations.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
The file module manages properties of existing files or directories (like permissions, ownership) and can create empty files or directories. The copy module places a file from your control machine onto the remote server, optionally setting its permissions and ownership as it does so.
The leading zero tells Ansible that the number is in octal format (base-8). Without the leading zero, some versions of Ansible may interpret the number as decimal, which can lead to completely wrong permissions. Always use the leading zero to be safe.
Use the file module with 'state: touch'. This is idempotent: if the file exists, it only updates the timestamp; if it does not exist, it creates an empty file. For creating a directory, use 'state: directory'.
'state: mounted' mounts the file system immediately and also adds an entry to /etc/fstab for persistence across reboots. 'state: present' only adds the entry to /etc/fstab but does not mount the file system right away. Use 'mounted' if you need the mount to be active now.
Technically yes, but it is bad practice and will lose marks in the exam. The file module is idempotent and reports correctly whether changes were made. The command module with 'chmod' will always report 'changed' even if the permissions were already correct, breaking idempotency.
Use the lineinfile module with 'state: present' and 'line: "the line you want"'. By default, lineinfile checks the entire line and only adds it if it is missing. If you want to replace a specific line that matches a pattern, use the 'regexp' parameter.
You've finished Managing Files and File Systems with Ansible. Continue through the EX294 study guide to build a complete picture of the exam.
Done with this chapter?