Introduction
In this lab, you will learn the fundamental skills for deploying and managing files on a Red Hat Enterprise Linux (RHEL) system using Ansible. You will gain hands-on experience with some of the most common and powerful Ansible modules designed for file operations, moving from basic file deployment to more advanced content manipulation and state management.
You will begin by using the ansible.builtin.copy module to transfer a static file and set its attributes. Next, you will modify file content with lineinfile and blockinfile, and generate a custom MOTD using the ansible.builtin.template module. The lab also covers creating symbolic links, verifying file states with stat, retrieving logs with fetch, and cleaning up managed files, providing a comprehensive overview of Ansible's file management capabilities.
Copy a Static File and Set Attributes with the ansible.builtin.copy Module
In this step, you will learn how to use one of the most fundamental Ansible modules: ansible.builtin.copy. This module is used to transfer files from your control node (the LabEx VM) to a specified location on your managed hosts. In our case, the managed host will be localhost itself. Beyond just copying, the copy module allows you to precisely control the file's attributes, such as its owner, group, and permission mode, which is essential for proper system configuration.
First, let's set up our project environment. All our work will be done inside the ~/project directory.
Navigate to the project directory and create a subdirectory for our source files. This is a common practice to keep your project organized.
Install the
ansible-corepackage.sudo dnf install -y ansible-coreThen, navigate to the project directory and create a subdirectory for our source files.
cd ~/project mkdir filesNext, create a simple text file that we will copy. We'll use a
catcommand with a "here document" to create the fileinfo.txtinside thefilesdirectory.cat << EOF > ~/project/files/info.txt This file was deployed by Ansible. It contains important system information. EOFNow, create an Ansible inventory file. The inventory tells Ansible which hosts to manage. For this lab, we will manage the local machine. Create a file named
inventory.ini.cat << EOF > ~/project/inventory.ini localhost ansible_connection=local EOFIn this inventory,
localhostis the host we are targeting. The variableansible_connection=localinstructs Ansible to execute the tasks directly on the control node, without using SSH.Create your first Ansible playbook. This playbook will contain the instructions to copy the file. Use
nanoorcatto create a file namedcopy_file.yml.nano ~/project/copy_file.ymlAdd the following content to the file. This playbook defines one task: to copy
info.txtto the/tmp/directory and set its attributes.--- - name: Deploy a static file to localhost hosts: localhost tasks: - name: Copy info.txt and set attributes ansible.builtin.copy: src: files/info.txt dest: /tmp/info.txt owner: labex group: labex mode: "0640"Let's break down the parameters in the
copytask:src: files/info.txt: The path to the source file on the control node, relative to the playbook's location.dest: /tmp/info.txt: The absolute path where the file will be placed on the managed host.owner: labex: Sets the file's owner to thelabexuser.group: labex: Sets the file's group to thelabexgroup.mode: '0640': Sets the file's permissions.0640means the owner can read/write, the group can read, and others have no permissions.
Execute the playbook using the
ansible-playbookcommand. The-iflag specifies our inventory file.ansible-playbook -i inventory.ini copy_file.ymlYou should see output indicating the successful execution of the playbook, similar to this:
PLAY [Deploy a static file to localhost] *************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Copy info.txt and set attributes] **************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Finally, verify that the file was copied correctly and has the right attributes. Use the
ls -lcommand to check the permissions, owner, and group.ls -l /tmp/info.txtThe output should show that
labexis the owner and group, and the permissions are-rw-r-----.-rw-r----- 1 labex labex 72 Jul 10 14:30 /tmp/info.txtYou can also view the file's content to ensure it was copied completely.
cat /tmp/info.txtThis file was deployed by Ansible. It contains important system information.
You have successfully used the ansible.builtin.copy module to deploy a file and configure its attributes on your local system.
Modify File Content with lineinfile and blockinfile
In this step, you will learn how to modify existing files on a managed host without replacing the entire file. Ansible provides powerful modules for this purpose: ansible.builtin.lineinfile for managing single lines and ansible.builtin.blockinfile for managing multi-line blocks of text. These are extremely useful for tasks like changing configuration settings or adding entries to log files.
We will continue working with the info.txt file you created in the previous step, which is located at /tmp/info.txt.
First, ensure you are in the project directory.
cd ~/projectCreate a new playbook named
modify_file.yml. This playbook will contain two tasks: one to add a single line and another to add a block of text to our existing file.nano ~/project/modify_file.ymlAdd the following content to your
modify_file.ymlplaybook. This playbook targetslocalhostand uses bothlineinfileandblockinfileto append content to/tmp/info.txt.--- - name: Modify an existing file hosts: localhost tasks: - name: Add a single line of text to a file ansible.builtin.lineinfile: path: /tmp/info.txt line: This line was added by the lineinfile module. state: present - name: Add a block of text to an existing file ansible.builtin.blockinfile: path: /tmp/info.txt block: | ## BEGIN ANSIBLE MANAGED BLOCK This block of text consists of two lines. They have been added by the blockinfile module. ## END ANSIBLE MANAGED BLOCK state: presentLet's examine the modules used:
ansible.builtin.lineinfile: This module ensures a specific line is present in a file. If the line already exists, Ansible does nothing, making the task idempotent.path: The file to modify.line: The line of text to ensure is in the file.state: present: This ensures the line exists. You could usestate: absentto remove it.
ansible.builtin.blockinfile: This module manages a block of text, surrounded by marker lines (e.g.,## BEGIN ANSIBLE MANAGED BLOCK). This is ideal for managing configuration sections.path: The file to modify.block: The multi-line string to insert. The|is YAML syntax for a literal block, preserving newlines.state: present: Ensures the block exists.
Execute the playbook using the
ansible-playbookcommand and yourinventory.inifile.ansible-playbook -i inventory.ini modify_file.ymlThe output will show that both tasks made changes to the file.
PLAY [Modify an existing file] ************************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Add a single line of text to a file] ************************************* changed: [localhost] TASK [Add a block of text to an existing file] ********************************* changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Finally, verify the changes by viewing the content of
/tmp/info.txt.cat /tmp/info.txtYou should see the original content, followed by the new line and the new block of text.
This file was deployed by Ansible. It contains important system information. This line was added by the lineinfile module. ## BEGIN ANSIBLE MANAGED BLOCK This block of text consists of two lines. They have been added by the blockinfile module. ## END ANSIBLE MANAGED BLOCKIf you run the playbook again, Ansible will report
ok=3andchanged=0because the content is already present, demonstrating the idempotent nature of these modules.
Generate a Custom MOTD with the ansible.builtin.template Module
In this step, you will advance from copying static files to generating dynamic files using the ansible.builtin.template module. This module leverages the Jinja2 templating engine to create files customized with variables and system information, known as "facts," that Ansible gathers from your managed hosts. We will create a dynamic Message of the Day (MOTD) that displays system-specific information.
First, ensure you are in the
~/projectdirectory and create a dedicated subdirectory for your templates. It is a standard Ansible best practice to store Jinja2 templates in atemplatesdirectory.cd ~/project mkdir templatesNext, create the Jinja2 template file. This file,
motd.j2, will contain the structure of our MOTD, with placeholders for dynamic data. The.j2extension is a common convention for Jinja2 templates.nano ~/project/templates/motd.j2Add the following content to the file. Notice the
{{ ... }}syntax, which denotes a placeholder for a variable or fact.################################################################# ## Welcome to {{ ansible_facts['fqdn'] }} # ## This is a {{ ansible_facts['distribution'] }} system. ## System managed by Ansible. # ## For support, contact: {{ admin_email }} #################################################################In this template:
{{ ansible_facts['fqdn'] }}will be replaced by the host's Fully Qualified Domain Name.{{ ansible_facts['distribution'] }}will be replaced by the Linux distribution name (e.g., RedHat).{{ admin_email }}is a custom variable that we will define in our playbook.
Now, create a new playbook named
template_motd.yml. This playbook will use the template to generate/etc/motd.nano ~/project/template_motd.ymlAdd the following content. This playbook requires elevated privileges (
become: true) to write to the/etcdirectory. It also defines the customadmin_emailvariable.--- - name: Deploy a custom MOTD from a template hosts: localhost become: true vars: admin_email: admin@labex.io tasks: - name: Generate /etc/motd from template ansible.builtin.template: src: templates/motd.j2 dest: /etc/motd owner: root group: root mode: "0644"Key parameters in this playbook:
become: true: This tells Ansible to usesudoto execute the task, which is necessary for writing to/etc/motd.vars: This section is where we define custom variables, likeadmin_email.ansible.builtin.template: The module that processes the Jinja2 template.srcpoints to our.j2file, anddestis the target file on the managed host.
Execute the playbook.
ansible-playbook -i inventory.ini template_motd.ymlThe output should confirm the task was successful.
PLAY [Deploy a custom MOTD from a template] ************************************ TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Generate /etc/motd from template] **************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify the result. Check the content of the newly generated
/etc/motdfile.cat /etc/motdYou will see the rendered output, with the Jinja2 placeholders replaced by actual system facts and the custom variable you defined. The
fqdnwill match your lab environment's hostname.################################################################# ## Welcome to host.labex.io # ## This is a RedHat system. ## System managed by Ansible. # ## For support, contact: admin@labex.io #################################################################
You have now successfully used a template to create a customized file, a core skill in infrastructure automation.
Deploy Supporting Files and Create a Symbolic Link with copy and file
In this step, you will combine your knowledge of the copy module with a new, versatile module: ansible.builtin.file. While copy is for transferring content, file is used to manage the state of files, directories, and symbolic links on the managed host. You will use it to create directories, set permissions, and, most importantly for this exercise, create symbolic links.
Our scenario is to configure the pre-login messages displayed by the system. In many Linux systems, /etc/issue is shown to local terminal users, and /etc/issue.net is shown to remote users (like over SSH). We will deploy a single issue file and then create a symbolic link so that /etc/issue.net points to /etc/issue, ensuring they always display the same message.
First, ensure you are in the
~/projectdirectory and create the source file for our issue message. We will place this file in thefilessubdirectory you created earlier.cd ~/project cat << EOF > ~/project/files/issue Authorized access only. All connections are logged and monitored. EOFCreate a new playbook named
deploy_issue.yml. This playbook will contain two tasks: one to copy theissuefile and another to create the symbolic link.nano ~/project/deploy_issue.ymlAdd the following content to your
deploy_issue.ymlplaybook. This playbook requires elevated privileges (become: true) to manage files in the/etc/directory.--- - name: Configure system issue files hosts: localhost become: true tasks: - name: Copy custom /etc/issue file ansible.builtin.copy: src: files/issue dest: /etc/issue owner: root group: root mode: "0644" - name: Ensure /etc/issue.net is a symlink to /etc/issue ansible.builtin.file: src: /etc/issue dest: /etc/issue.net state: link force: yesLet's analyze the new
ansible.builtin.filetask:src: /etc/issue: Whenstateislink,srcspecifies the file that the symbolic link should point to.dest: /etc/issue.net: This is the path where the symbolic link itself will be created.state: link: This crucial parameter tells thefilemodule to create a symbolic link, not a regular file or directory.force: yes: This is a useful option that ensures idempotency. If/etc/issue.netalready exists as a regular file, Ansible will remove it and create the link. Withoutforce: yes, the playbook would fail in that situation.
Execute the playbook.
ansible-playbook -i inventory.ini deploy_issue.ymlThe output will show both tasks successfully making changes.
PLAY [Configure system issue files] ******************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Copy custom /etc/issue file] ********************************************* changed: [localhost] TASK [Ensure /etc/issue.net is a symlink to /etc/issue] ************************ changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify the result using the
ls -lcommand. This command provides a detailed listing that clearly shows symbolic links.ls -l /etc/issue /etc/issue.netThe output should show that
/etc/issueis a regular file and/etc/issue.netis a symbolic link pointing to it. Thelat the beginning of the permissions for/etc/issue.netindicates it's a link.-rw-r--r--. 1 root root 65 Jul 10 15:00 /etc/issue lrwxrwxrwx. 1 root root 10 Jul 10 15:00 /etc/issue.net -> /etc/issue
You have now successfully deployed a configuration file and used the ansible.builtin.file module to create a symbolic link, a common and powerful pattern for managing system configurations.
Verify File State with stat and Fetch Logs with fetch
In this step, you will learn about two important data-gathering modules: ansible.builtin.stat and ansible.builtin.fetch. The stat module is used to check the status of a file or directory on a managed host—for example, to see if it exists, what its permissions are, or when it was last modified. It doesn't change anything, making it perfect for checks and conditional logic. The fetch module does the opposite of copy: it retrieves files from the managed host and saves them to your control node, which is ideal for backing up configurations or collecting log files for analysis.
We will create a playbook that first checks for the existence of the /etc/motd file you created earlier, and then fetches the DNF package manager log file (/var/log/dnf.log) to a local directory on your LabEx VM.
First, ensure you are in the
~/projectdirectory and create a new subdirectory to store the files you will fetch.cd ~/project mkdir fetched_logsCreate a new playbook named
check_and_fetch.yml. This playbook will contain the tasks to check the file and retrieve the log.nano ~/project/check_and_fetch.ymlAdd the following content to your
check_and_fetch.ymlplaybook. This playbook usesstatto get file details,registerto store those details in a variable,debugto display the variable, andfetchto retrieve the log file.--- - name: Check file status and fetch logs hosts: localhost become: true tasks: - name: Check if /etc/motd exists ansible.builtin.stat: path: /etc/motd register: motd_status - name: Display stat results ansible.builtin.debug: var: motd_status.stat - name: Fetch the dnf log file from managed host ansible.builtin.fetch: src: /var/log/dnf.log dest: fetched_logs/ flat: yesLet's break down the key concepts:
register: motd_status: This is a crucial Ansible feature. It takes the entire output of a task and saves it into a new variable namedmotd_status.ansible.builtin.debug: This module is used to print values during a playbook run. Here, we print thestatobject within our registered variable (motd_status.stat) to see the file's properties.ansible.builtin.fetch: This module retrieves a file from the managed host.src: The path of the file to retrieve from the managed host.dest: The directory on the control node (your LabEx VM) where the file will be saved.flat: yes: By default,fetchcreates a subdirectory structure matching the host and source path.flat: yessimplifies this by copying the file directly into thedestdirectory without any extra subdirectories.
Execute the playbook. Since we are reading a system log file,
become: trueis used to gain the necessary permissions.ansible-playbook -i inventory.ini check_and_fetch.ymlThe output will show the results of the
statcheck in the debug task, followed by thefetchtask.PLAY [Check file status and fetch logs] **************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Check if /etc/motd exists] *********************************************** ok: [localhost] TASK [Display stat results] **************************************************** ok: [localhost] => { "motd_status.stat": { "exists": true, "gid": 0, "isreg": true, "mode": "0644", "path": "/etc/motd", ... } } TASK [Fetch the dnf log file from managed host] ******************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify that the log file was fetched successfully. List the contents of the
fetched_logsdirectory.ls -l ~/project/fetched_logs/You should see the
dnf.logfile, now stored locally on your control node.total 4 -rw-r--r--. 1 labex labex 1234 Jul 10 15:30 dnf.log
You have now learned how to inspect file properties without making changes and how to retrieve important files from your managed systems back to your control node.
Clean Up Managed Host Files with the file Module
In this final step, you will learn how to use the ansible.builtin.file module to ensure files and directories are not present on a system. A critical part of configuration management is not just creating and modifying resources, but also cleaning them up. By setting the state parameter to absent, you can instruct Ansible to remove files, symbolic links, or even entire directories.
To conclude this lab, we will write a single "cleanup" playbook that removes all the artifacts we created in the previous steps: /tmp/info.txt, /etc/motd, /etc/issue, and the /etc/issue.net symbolic link.
First, ensure you are in the
~/projectdirectory.cd ~/projectCreate a new playbook named
cleanup.yml. This playbook will contain all the tasks needed to revert our changes.nano ~/project/cleanup.ymlAdd the following content to your
cleanup.ymlplaybook. This playbook uses a list of tasks, each targeting one of the files we created. Note thatbecome: trueis set at the play level, so all tasks will run with elevated privileges.--- - name: Clean up managed files from the system hosts: localhost become: true tasks: - name: Remove the temporary info file ansible.builtin.file: path: /tmp/info.txt state: absent - name: Remove the custom MOTD file ansible.builtin.file: path: /etc/motd state: absent - name: Remove the custom issue file ansible.builtin.file: path: /etc/issue state: absent - name: Remove the issue.net symbolic link ansible.builtin.file: path: /etc/issue.net state: absentThe key to this playbook is the
state: absentparameter in each task. This tells thefilemodule to ensure the item at the specifiedpathdoes not exist. If it finds the file, it will remove it. If the file is already gone, it will do nothing, maintaining idempotency.Execute the cleanup playbook.
ansible-playbook -i inventory.ini cleanup.ymlThe output will show that each task successfully made a change by removing a file.
PLAY [Clean up managed files from the system] ********************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Remove the temporary info file] ****************************************** changed: [localhost] TASK [Remove the custom MOTD file] ********************************************* changed: [localhost] TASK [Remove the custom issue file] ******************************************** changed: [localhost] TASK [Remove the issue.net symbolic link] ************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify that the files have been removed. You can use the
lscommand to check for their existence. The command will report that it cannot access them because they are gone.ls /tmp/info.txt /etc/motd /etc/issue /etc/issue.netThe expected output is a series of errors, confirming the cleanup was successful.
ls: cannot access '/tmp/info.txt': No such file or directory ls: cannot access '/etc/motd': No such file or directory ls: cannot access '/etc/issue': No such file or directory ls: cannot access '/etc/issue.net': No such file or directory
You have now successfully used Ansible to remove files and clean up a system, completing the full lifecycle of file management from creation to deletion.
Summary
In this lab, you learned the fundamentals of file management on RHEL systems using Ansible. You started by using the ansible.builtin.copy module to transfer a static file to a managed host while setting specific ownership and permissions. You then explored how to modify existing files by ensuring a specific line is present with lineinfile and managing multi-line blocks of text with blockinfile. A key skill covered was generating dynamic file content using the ansible.builtin.template module and Jinja2 syntax to create a customized Message of the Day (MOTD) populated with system facts.
Furthermore, you practiced deploying supporting files and creating symbolic links using the ansible.builtin.file module. To ensure your deployments were successful, you used the stat module to verify the state and attributes of files and the fetch module to retrieve files, such as logs, from the managed host back to the control node. Finally, you learned how to perform cleanup operations by using the file module with state: absent to remove the files and directories created throughout the lab, ensuring a clean state on the managed host.


