Deploy and Manage Files on RHEL with Ansible

AnsibleBeginner
Practice Now

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.

This is a Guided Lab, which provides step-by-step instructions to help you learn and practice. Follow the instructions carefully to complete each step and gain hands-on experience. Historical data shows that this is a beginner level lab with a 83% completion rate. It has received a 100% positive review rate from learners.

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.

  1. 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-core package.

    sudo dnf install -y ansible-core

    Then, navigate to the project directory and create a subdirectory for our source files.

    cd ~/project
    mkdir files
  2. Next, create a simple text file that we will copy. We'll use a cat command with a "here document" to create the file info.txt inside the files directory.

    cat << EOF > ~/project/files/info.txt
    This file was deployed by Ansible.
    It contains important system information.
    EOF
  3. Now, 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
    EOF

    In this inventory, localhost is the host we are targeting. The variable ansible_connection=local instructs Ansible to execute the tasks directly on the control node, without using SSH.

  4. Create your first Ansible playbook. This playbook will contain the instructions to copy the file. Use nano or cat to create a file named copy_file.yml.

    nano ~/project/copy_file.yml

    Add the following content to the file. This playbook defines one task: to copy info.txt to 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 copy task:

    • 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 the labex user.
    • group: labex: Sets the file's group to the labex group.
    • mode: '0640': Sets the file's permissions. 0640 means the owner can read/write, the group can read, and others have no permissions.
  5. Execute the playbook using the ansible-playbook command. The -i flag specifies our inventory file.

    ansible-playbook -i inventory.ini copy_file.yml

    You 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=0
  6. Finally, verify that the file was copied correctly and has the right attributes. Use the ls -l command to check the permissions, owner, and group.

    ls -l /tmp/info.txt

    The output should show that labex is the owner and group, and the permissions are -rw-r-----.

    -rw-r----- 1 labex labex 72 Jul 10 14:30 /tmp/info.txt

    You can also view the file's content to ensure it was copied completely.

    cat /tmp/info.txt
    This 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.

  1. First, ensure you are in the project directory.

    cd ~/project
  2. Create 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.yml
  3. Add the following content to your modify_file.yml playbook. This playbook targets localhost and uses both lineinfile and blockinfile to 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: present

    Let'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 use state: absent to 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.
  4. Execute the playbook using the ansible-playbook command and your inventory.ini file.

    ansible-playbook -i inventory.ini modify_file.yml

    The 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=0
  5. Finally, verify the changes by viewing the content of /tmp/info.txt.

    cat /tmp/info.txt

    You 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 BLOCK

    If you run the playbook again, Ansible will report ok=3 and changed=0 because 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.

  1. First, ensure you are in the ~/project directory and create a dedicated subdirectory for your templates. It is a standard Ansible best practice to store Jinja2 templates in a templates directory.

    cd ~/project
    mkdir templates
  2. Next, create the Jinja2 template file. This file, motd.j2, will contain the structure of our MOTD, with placeholders for dynamic data. The .j2 extension is a common convention for Jinja2 templates.

    nano ~/project/templates/motd.j2

    Add 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.
  3. Now, create a new playbook named template_motd.yml. This playbook will use the template to generate /etc/motd.

    nano ~/project/template_motd.yml

    Add the following content. This playbook requires elevated privileges (become: true) to write to the /etc directory. It also defines the custom admin_email variable.

    ---
    - 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 use sudo to execute the task, which is necessary for writing to /etc/motd.
    • vars: This section is where we define custom variables, like admin_email.
    • ansible.builtin.template: The module that processes the Jinja2 template. src points to our .j2 file, and dest is the target file on the managed host.
  4. Execute the playbook.

    ansible-playbook -i inventory.ini template_motd.yml

    The 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=0
  5. Verify the result. Check the content of the newly generated /etc/motd file.

    cat /etc/motd

    You will see the rendered output, with the Jinja2 placeholders replaced by actual system facts and the custom variable you defined. The fqdn will 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.

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.

  1. First, ensure you are in the ~/project directory and create the source file for our issue message. We will place this file in the files subdirectory you created earlier.

    cd ~/project
    cat << EOF > ~/project/files/issue
    Authorized access only.
    All connections are logged and monitored.
    EOF
  2. Create a new playbook named deploy_issue.yml. This playbook will contain two tasks: one to copy the issue file and another to create the symbolic link.

    nano ~/project/deploy_issue.yml
  3. Add the following content to your deploy_issue.yml playbook. 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: yes

    Let's analyze the new ansible.builtin.file task:

    • src: /etc/issue: When state is link, src specifies 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 the file module to create a symbolic link, not a regular file or directory.
    • force: yes: This is a useful option that ensures idempotency. If /etc/issue.net already exists as a regular file, Ansible will remove it and create the link. Without force: yes, the playbook would fail in that situation.
  4. Execute the playbook.

    ansible-playbook -i inventory.ini deploy_issue.yml

    The 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=0
  5. Verify the result using the ls -l command. This command provides a detailed listing that clearly shows symbolic links.

    ls -l /etc/issue /etc/issue.net

    The output should show that /etc/issue is a regular file and /etc/issue.net is a symbolic link pointing to it. The l at the beginning of the permissions for /etc/issue.net indicates 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.

  1. First, ensure you are in the ~/project directory and create a new subdirectory to store the files you will fetch.

    cd ~/project
    mkdir fetched_logs
  2. Create 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.yml
  3. Add the following content to your check_and_fetch.yml playbook. This playbook uses stat to get file details, register to store those details in a variable, debug to display the variable, and fetch to 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: yes

    Let'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 named motd_status.
    • ansible.builtin.debug: This module is used to print values during a playbook run. Here, we print the stat object 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, fetch creates a subdirectory structure matching the host and source path. flat: yes simplifies this by copying the file directly into the dest directory without any extra subdirectories.
  4. Execute the playbook. Since we are reading a system log file, become: true is used to gain the necessary permissions.

    ansible-playbook -i inventory.ini check_and_fetch.yml

    The output will show the results of the stat check in the debug task, followed by the fetch task.

    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=0
  5. Verify that the log file was fetched successfully. List the contents of the fetched_logs directory.

    ls -l ~/project/fetched_logs/

    You should see the dnf.log file, 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.

  1. First, ensure you are in the ~/project directory.

    cd ~/project
  2. Create a new playbook named cleanup.yml. This playbook will contain all the tasks needed to revert our changes.

    nano ~/project/cleanup.yml
  3. Add the following content to your cleanup.yml playbook. This playbook uses a list of tasks, each targeting one of the files we created. Note that become: true is 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: absent

    The key to this playbook is the state: absent parameter in each task. This tells the file module to ensure the item at the specified path does not exist. If it finds the file, it will remove it. If the file is already gone, it will do nothing, maintaining idempotency.

  4. Execute the cleanup playbook.

    ansible-playbook -i inventory.ini cleanup.yml

    The 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=0
  5. Verify that the files have been removed. You can use the ls command 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.net

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