Introduction
In this lab, you will learn fundamental techniques for managing variables, facts, and secrets within Ansible playbooks on a Red Hat Enterprise Linux (RHEL) system. You will explore how to make your automation more flexible and powerful by using playbook variables, gathering system information with both built-in and custom Ansible facts, and securing sensitive data like passwords using Ansible Vault.
Through a series of hands-on steps, you will build a playbook to deploy and configure an Apache web server. You will start by defining simple variables for the package name and web content, then leverage custom facts to dynamically update the web server's configuration. Finally, you will use Ansible Vault to securely create a new system user with an encrypted password, run the complete playbook, and verify that all configurations have been successfully applied.
Define and Use Playbook Variables to Deploy an Apache Web Server
In this step, you will learn how to use variables in an Ansible playbook. Variables are essential for making your automation flexible, reusable, and easier to read and maintain. Instead of hardcoding values like package names or file paths directly into your tasks, you can define them as variables and reference them throughout the playbook. We will create a simple playbook that uses variables to install the Apache web server (httpd) and deploy a basic web page.
Navigate to the Project Directory
First, ensure you are in the correct working directory. All your work for this lab will be done inside the
~/projectdirectory, which has been created for you.cd ~/projectInstall the
ansible-corepackage.sudo dnf install -y ansible-coreCreate the Ansible Playbook
Now, let's create our playbook file. We will name it
playbook.yml. You can use a command-line text editor likenanoto create and edit the file.nano playbook.ymlThis command opens an empty file in the
nanoeditor. Now, add the initial part of the playbook. This section defines the play's name, the target host (localhost, since we are running it on the same machine), and avarssection where we will define our variables.--- - name: Deploy Apache using variables hosts: localhost become: true vars: web_pkg: httpd web_content: "Hello from Ansible Variables"Here's a breakdown of the playbook structure:
hosts: localhost: Specifies that the playbook should run on the local machine.become: true: Tells Ansible to use privilege escalation (equivalent tosudo) for the tasks, which is necessary for installing software.vars: This is a dictionary where we define our key-value pairs for variables. We've definedweb_pkgfor the package name andweb_contentfor the content of our test web page.
Add Tasks to the Playbook
Next, below the
varssection, add thetasksthat will use these variables. The first task will install the Apache package, and the second will create anindex.htmlfile. Add the followingtasksblock to yourplaybook.ymlfile while still in thenanoeditor.tasks: - name: Install the latest version of Apache ansible.builtin.dnf: name: "{{ web_pkg }}" state: latest - name: Create a basic index.html file ansible.builtin.copy: content: "{{ web_content }}" dest: /var/www/html/index.htmlNotice how we use
{{ variable_name }}to reference the variables we defined earlier. This is Jinja2 templating, which Ansible uses for variables. This makes the task definitions generic; if you wanted to install Nginx instead, you would only need to change theweb_pkgvariable, not the task itself.Review and Save the Playbook
Your complete
playbook.ymlfile should now look like this. Double-check the content and indentation, as YAML is very sensitive to spacing.--- - name: Deploy Apache using variables hosts: localhost become: true vars: web_pkg: httpd web_content: "Hello from Ansible Variables" tasks: - name: Install the latest version of Apache ansible.builtin.dnf: name: "{{ web_pkg }}" state: latest - name: Create a basic index.html file ansible.builtin.copy: content: "{{ web_content }}" dest: /var/www/html/index.htmlTo save the file in
nano, pressCtrl+X, thenYto confirm the changes, and finallyEnterto write the file with the nameplaybook.yml.Check the Playbook Syntax
Before running a playbook, it's always a good practice to check its syntax for any errors.
ansible-playbook --syntax-check playbook.ymlIf the syntax is correct, you will see the playbook's file path as output, confirming it's valid:
playbook: playbook.ymlIf you see any errors, reopen the file with
nano playbook.ymland fix them. Pay close attention to correct indentation (usually two spaces).Run the Playbook
Now, execute the playbook. Ansible will connect to
localhost, read the variables, and run the tasks.ansible-playbook playbook.ymlYou should see output indicating the successful execution of each task. The
changedstatus means that Ansible made a modification to the system, such as installing a package or creating a file.PLAY [Deploy Apache using variables] ******************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Install the latest version of Apache] ************************************ changed: [localhost] TASK [Create a basic index.html file] ****************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0If you run the playbook a second time, the tasks should report
okinstead ofchanged, because the package is already installed and the file already has the correct content. This demonstrates Ansible's idempotency.Verify the Configuration Manually
Although the playbook has completed, you can manually verify that the tasks worked as expected. First, check if the
httpdpackage was installed:rpm -q httpdThe output should show the package name and version:
httpd-2.4.57-7.el9.x86_64Next, check the content of the
index.htmlfile:cat /var/www/html/index.htmlThe output should match the value of your
web_contentvariable:Hello from Ansible VariablesYou have successfully used variables in an Ansible playbook to configure a system.
Display System Information using Ansible Facts
In this step, you will explore Ansible facts. Facts are pieces of information that Ansible gathers about the systems it manages (in this case, localhost). This information includes details like the operating system, network interfaces, memory, and much more. By default, Ansible collects facts at the beginning of every play, making them available in a special variable called ansible_facts. Using facts allows you to create dynamic playbooks that adapt to the environment they are running in.
Navigate to the Project Directory
First, ensure you are in the
~/projectdirectory where you will create the new playbook.cd ~/projectCreate a Playbook to Display All Facts
Let's start by creating a playbook that simply displays all the facts Ansible can gather about your system. This will give you an idea of the vast amount of information available. Use
nanoto create a new file nameddisplay_facts.yml.nano display_facts.ymlInside the
nanoeditor, add the following content. This playbook targetslocalhostand uses theansible.builtin.debugmodule to print the contents of theansible_factsvariable.--- - name: Display all Ansible facts hosts: localhost tasks: - name: Print all available facts ansible.builtin.debug: var: ansible_factsSave the file and exit
nanoby pressingCtrl+X, thenY, andEnter.Run the Playbook
Now, execute the playbook to see the result.
ansible-playbook display_facts.ymlThe output will be very long, as Ansible collects a lot of data. It will be a large JSON structure containing all the system details. This is expected.
PLAY [Display all Ansible facts] *********************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Print all available facts] *********************************************** ok: [localhost] => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.17.0.2" ], "ansible_all_ipv6_addresses": [ "fe80::42:acff:fe11:2" ], "ansible_apparmor": { "status": "disabled" }, "ansible_architecture": "x86_64", "ansible_bios_date": "01/01/2011", "ansible_bios_version": "1.0", "ansible_cmdline": { "BOOT_IMAGE": "/boot/vmlinuz-5.14.0-427.16.1.el9_4.x86_64", "root": "UUID=...", "ro": true }, "ansible_date_time": { "date": "2024-05-21", "day": "21", "epoch": "1716298855", ... }, "ansible_distribution": "RedHat", "ansible_distribution_major_version": "9", "ansible_distribution_version": "9.4", ... } } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Create a Playbook to Display Specific Facts
Displaying all facts is useful for discovery, but in most cases, you only need specific pieces of information. Let's create another playbook,
display_specific_facts.yml, to display a formatted message with just a few key facts.nano display_specific_facts.ymlAdd the following content. This playbook uses the
msgparameter of thedebugmodule to print a custom string. We access individual facts using bracket notation, likeansible_facts['distribution'].--- - name: Display specific Ansible facts hosts: localhost tasks: - name: Print a summary of system facts ansible.builtin.debug: msg: > The operating system is {{ ansible_facts['distribution'] }} version {{ ansible_facts['distribution_major_version'] }}. It has {{ ansible_facts['processor_cores'] }} processor cores and {{ ansible_facts['memtotal_mb'] }} MB of total memory.The
>character inmsg: >is a YAML feature that allows you to write a multi-line string more cleanly. Save the file and exitnano.Run the Playbook for Specific Facts
Now, run this new playbook.
ansible-playbook display_specific_facts.ymlThe output will be much cleaner and more readable, showing only the information you requested.
PLAY [Display specific Ansible facts] ****************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Print a summary of system facts] ***************************************** ok: [localhost] => { "msg": "The operating system is RedHat version 9. It has 2 processor cores and 3925 MB of total memory." } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0This demonstrates how you can leverage Ansible facts to make your playbooks aware of the environment they are running in, allowing for more intelligent and conditional automation.
Configure the Web Server using Custom Facts from the Managed Host
In this step, you will learn how to use custom facts. While Ansible automatically gathers a wide range of standard facts, you can also define your own. These are called "local facts" or "custom facts". This is a powerful feature that allows you to provide specific information from a managed host to your playbooks, such as application settings or hardware-specific data that Ansible doesn't collect by default.
Ansible looks for custom facts in the /etc/ansible/facts.d directory on the managed host. Any file in this directory with a .fact extension will be processed. These files can be simple INI-style text files or JSON files.
Create the Custom Facts Directory
First, you need to create the directory where Ansible will look for custom fact files. Since this is a system directory, you must use
sudoto create it.sudo mkdir -p /etc/ansible/facts.dThe
-pflag ensures that the command doesn't return an error if the directory already exists.Create a Custom Fact File
Now, let's create a custom fact file to define a welcome message for our web server. We will create an INI-formatted file named
web_config.factinside the/etc/ansible/facts.ddirectory.sudo nano /etc/ansible/facts.d/web_config.factAdd the following content to the file. This defines a section
[webserver]with a keywelcome_message.[webserver] welcome_message = Welcome to the server configured by Custom Facts!Save the file and exit
nanoby pressingCtrl+X, thenY, andEnter.Create a Playbook to Use the Custom Fact
With the custom fact in place, we can now create a playbook that reads this fact and uses it to configure our web server's home page. In your
~/projectdirectory, create a new playbook namedconfigure_web.yml.cd ~/project nano configure_web.ymlAdd the following content to the playbook. This playbook will update the
/var/www/html/index.htmlfile with the message defined in our custom fact.--- - name: Configure web server using custom facts hosts: localhost become: true tasks: - name: Update index.html with custom message ansible.builtin.copy: content: "{{ ansible_facts.ansible_local.web_config.webserver.welcome_message }}" dest: /var/www/html/index.htmlLet's break down the variable
{{ ansible_facts.ansible_local.web_config.webserver.welcome_message }}:ansible_facts: The root dictionary for all facts.ansible_local: The key where all custom facts are stored.web_config: The name of our fact file (web_config.fact), without the extension.webserver: The section name[webserver]from our INI file.welcome_message: The key for the value we want to use.
Save the file and exit
nano.Run the Configuration Playbook
Now, execute the playbook to apply the configuration.
ansible-playbook configure_web.ymlThe output should show that the
copytask haschangedtheindex.htmlfile.PLAY [Configure web server using custom facts] ********************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Update index.html with custom message] *********************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify the Result
Finally, let's verify that the web page was updated correctly. Use the
catcommand to view the contents of theindex.htmlfile.cat /var/www/html/index.htmlThe output should now display the message from your custom fact file:
Welcome to the server configured by Custom Facts!You have successfully created a custom fact on the managed host and used it within a playbook to dynamically configure a service. This technique is incredibly useful for making your automation more flexible and data-driven.
Create a System User using Encrypted Variables with Ansible Vault
In this step, you will learn how to manage sensitive data, such as passwords or API keys, using Ansible Vault. Storing sensitive information in plain text within your playbooks is a major security risk. Ansible Vault provides a way to encrypt files or individual variables, keeping your secrets safe. You can then use these encrypted files in your playbooks, and Ansible will decrypt them at runtime when you provide the correct password.
We will create an encrypted file containing a username and a hashed password, and then use a playbook to create a new system user with these credentials.
Navigate to the Project Directory
Ensure you are in the
~/projectdirectory for this task.cd ~/projectCreate an Encrypted Vault File
We will use the
ansible-vault createcommand to create a new, encrypted YAML file namedsecrets.yml. This command will prompt you to create a password for the vault. This password is required to open, edit, or use the file later.First, let's set the editor to
nanoto make it easier to work with:export EDITOR=nanoNow create the vault file:
ansible-vault create secrets.ymlWhen prompted, enter a password for your vault. For this lab, let's use
labexas the vault password to keep things simple. You will need to enter it twice.New Vault password: Confirm New Vault password:After you confirm the password, the command will open the
secrets.ymlfile in thenanotext editor.Add Secret Variables to the Vault File
Inside the
nanoeditor, which is now editing the encryptedsecrets.ymlfile, add the following variables. We will define a username and a pre-hashed password for a new user. Using a hashed password is much more secure than storing a plain-text password.username: myappuser pwhash: $6$mysalt$QwMzWSEyCAGmz7tzVrAi5o.8k4d05i2QsfGGwmPtlJsWhGjSjCW6yFCH/OEqEsHk7GMSxqYNXu5sshxPmWyxo0username: The name of the system user we want to create.pwhash: A securely hashed password. This specific hash corresponds to the passwordAnsibleUserP@ssw0rdand is in a format that theansible.builtin.usermodule understands.
Save the file and exit
nano(Ctrl+X, thenY, thenEnter). Thesecrets.ymlfile in your~/projectdirectory is now encrypted. If you try to view it withcat secrets.yml, you will only see encrypted text.Create a Playbook to Use the Vault File
Now, create a new playbook named
create_user.ymlthat will use the variables from your encryptedsecrets.ymlfile.nano create_user.ymlAdd the following content. The
vars_filesdirective tells Ansible to load variables from the specified file.--- - name: Create a user from secret variables hosts: localhost become: true vars_files: - secrets.yml tasks: - name: Create the {{ username }} user ansible.builtin.user: name: "{{ username }}" password: "{{ pwhash }}" state: presentThis playbook will create a user with the name and password hash defined in
secrets.yml. Save the file and exitnano.Run the Playbook with the Vault Password
To run a playbook that uses a vaulted file, you must provide the vault password. You can do this interactively using the
--ask-vault-passflag.ansible-playbook --ask-vault-pass create_user.ymlAnsible will prompt you for the vault password. Enter
labex(the password you set in step 2).Vault password:After you provide the correct password, Ansible will decrypt the file in memory and run the playbook. You should see the following output, indicating the user was created.
PLAY [Create a user from secret variables] ************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Create the myappuser user] *********************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify the User was Created
You can confirm that the
myappuserwas successfully created on the system by using theidcommand.id myappuserIf the user exists, you will see their user ID (uid) and group ID (gid) information.
uid=1002(myappuser) gid=1002(myappuser) groups=1002(myappuser)This confirms that you have successfully used Ansible Vault to manage sensitive data for your automation tasks.
Run a Playbook with a Vault Password File to Apply Configurations
In this step, you will learn a more automated way to provide the vault password to Ansible. In the previous step, you used --ask-vault-pass to enter the password interactively. While this is secure, it's not suitable for automated environments like CI/CD pipelines where no user is present to type a password.
The solution is to use a vault password file. This is a simple text file that contains the vault password. You can then reference this file when running your playbook, and Ansible will read the password from it automatically. For security, it is crucial to restrict the permissions of this password file so that only authorized users can read it.
Navigate to the Project Directory
Make sure you are in the
~/projectdirectory where your playbook and vault file are located.cd ~/projectCreate the Vault Password File
Let's create a file to store our vault password. We will name it
vault_pass.txt. We can use theechocommand to create the file and write the password (labex) into it in a single step.echo "labex" > vault_pass.txtYou can verify the file's content with
cat:cat vault_pass.txtThe output should be:
labexSecure the Password File
Storing a password in a plain text file is risky. You must restrict its file permissions to protect it. The
chmodcommand allows you to change file permissions. We will set the permissions to600, which means only the file owner (in this case, thelabexuser) has read and write permissions. No other users on the system will be able to access it.chmod 600 vault_pass.txtYou can verify the new permissions using the
ls -lcommand:ls -l vault_pass.txtThe output should start with
-rw-------, confirming the restricted permissions.-rw-------. 1 labex labex 6 May 21 14:30 vault_pass.txtModify the Playbook to Add a User to a Group
Let's modify our
create_user.ymlplaybook to perform an additional action. We will add themyappuserto thewheelgroup, which on many systems grants administrative (sudo) privileges. This will demonstrate running a playbook that makes a change to an existing configuration.First, open the
create_user.ymlplaybook for editing.nano create_user.ymlModify the
ansible.builtin.usertask to include thegroupsandappendparameters.--- - name: Create a user from secret variables hosts: localhost become: true vars_files: - secrets.yml tasks: - name: Create the {{ username }} user and add to wheel group ansible.builtin.user: name: "{{ username }}" password: "{{ pwhash }}" state: present groups: wheel append: truegroups: wheel: Specifies the group to add the user to.append: true: Ensures that the user is added to this group without removing them from any other groups they might belong to.
Save the file and exit
nano.Run the Playbook with the Vault Password File
Now, run the playbook again. This time, instead of
--ask-vault-pass, use the--vault-password-fileoption (or its shorter alias--vault-pass-file) to specify the path to your password file.ansible-playbook --vault-password-file vault_pass.txt create_user.ymlAnsible will now run without prompting for a password because it reads it directly from
vault_pass.txt. You should see output indicating that the user's configuration was changed.PLAY [Create a user from secret variables] ************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Create the myappuser user and add to wheel group] ************************ changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0The
changedstatus confirms that Ansible modified the user by adding them to thewheelgroup.Verify the User's Group Membership
Finally, verify that
myappuseris now a member of thewheelgroup. You can do this with thegroupscommand.groups myappuserThe output should show both the user's primary group (
myappuser) and thewheelgroup.myappuser : myappuser wheelYou have successfully used a vault password file to run a playbook non-interactively, a key skill for automating secure workflows.
Verify the Web Server and User Configuration
In this final step, you will consolidate your learning by creating a dedicated verification playbook. So far, you have been manually checking the results of your playbooks using standard Linux commands like cat, id, and groups. A more powerful and repeatable approach is to use Ansible itself to audit and validate the state of your system.
This playbook will act as a test suite, programmatically checking that the web server is installed, the web page has the correct content, and the system user exists with the proper group membership. This demonstrates how Ansible can be used not just for configuration management, but also for compliance and state validation.
Navigate to the Project Directory
First, ensure you are in the
~/projectdirectory.cd ~/projectCreate the Verification Playbook
Let's create a new playbook named
verify_config.yml. This playbook will contain a series of tasks that check the configurations you applied in the previous steps.nano verify_config.ymlAdd Tasks to Verify the Configuration
Inside the
nanoeditor, add the following content. We will build this playbook with several tasks, each one designed to assert a specific condition is true. If any assertion fails, the playbook will stop and report an error, immediately telling you what is wrong.--- - name: Verify system configuration hosts: localhost become: true tasks: - name: Check if httpd package is installed ansible.builtin.dnf: list: httpd register: httpd_pkg_info - name: Assert that httpd is installed ansible.builtin.assert: that: - httpd_pkg_info.results | length > 0 fail_msg: "Apache (httpd) package is not installed." success_msg: "Apache (httpd) package is installed." - name: Read the content of the index.html file ansible.builtin.slurp: src: /var/www/html/index.html register: index_file - name: Assert that the web page content is correct ansible.builtin.assert: that: - "'Custom Facts' in (index_file.content | b64decode)" fail_msg: "Web page content is incorrect." success_msg: "Web page content is correct." - name: Check if myappuser exists ansible.builtin.getent: database: passwd key: myappuser register: user_info - name: Assert that myappuser exists ansible.builtin.assert: that: - user_info.ansible_facts.getent_passwd['myappuser'] is defined fail_msg: "User 'myappuser' does not exist." success_msg: "User 'myappuser' exists." - name: Query the wheel group members ansible.builtin.getent: database: group key: wheel register: wheel_group_info - name: Assert that myappuser is in the wheel group ansible.builtin.assert: that: - "'myappuser' in (wheel_group_info.ansible_facts.getent_group['wheel'][2] | default('') | split(','))" fail_msg: "User 'myappuser' is not in the wheel group." success_msg: "User 'myappuser' is in the wheel group."Let's review the key modules used here:
ansible.builtin.dnfwithlist: This checks for a package andregisters the result.ansible.builtin.slurp: This "slurps" up the entire content of a file from the remote host. The content is base64-encoded for safe transport.ansible.builtin.getent: This is a safe way to query system databases likepasswdandgroup. The results are stored underansible_facts, so access the returned data through keys such asuser_info.ansible_facts.getent_passwd.ansible.builtin.assert: This is the core of our verification. It checks if a given condition is true. If not, it fails the play. We provide custom success and failure messages.b64decode: This is a Jinja2 filter used to decode the base64 content we got from theslurpmodule.
Notice that we query the
passwdandgroupdatabases separately. This keeps the user existence check and the wheel group membership check aligned with the actual data returned bygetent.Save the file and exit
nano(Ctrl+X,Y,Enter).Run the Verification Playbook
Now, execute your verification playbook. Since it doesn't use any vaulted files, you don't need to provide a password.
ansible-playbook verify_config.ymlIf all your previous steps were completed correctly, the playbook will run successfully, and you will see the custom success message for each assertion.
PLAY [Verify system configuration] ********************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Check if httpd package is installed] ************************************* ok: [localhost] TASK [Assert that httpd is installed] ****************************************** ok: [localhost] => { "changed": false, "msg": "Apache (httpd) package is installed." } TASK [Read the content of the index.html file] ********************************* ok: [localhost] TASK [Assert that the web page content is correct] ***************************** ok: [localhost] => { "changed": false, "msg": "Web page content is correct." } TASK [Check if myappuser exists] *********************************************** ok: [localhost] TASK [Assert that myappuser exists] ******************************************** ok: [localhost] => { "changed": false, "msg": "User 'myappuser' exists." } TASK [Query the wheel group members] ******************************************* ok: [localhost] TASK [Assert that myappuser is in the wheel group] ***************************** ok: [localhost] => { "changed": false, "msg": "User 'myappuser' is in the wheel group." } PLAY RECAP ********************************************************************* localhost : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Congratulations! You have successfully used Ansible to define variables, gather system facts, manage secrets with Vault, and finally, to verify the state of your system in an automated way.
Summary
In this lab, you learned to manage different types of data within Ansible playbooks to configure a RHEL system. You began by defining and using standard playbook variables to flexibly install and configure an Apache web server. Following this, you explored how to leverage Ansible's built-in facts to display system information, providing a foundation for creating dynamic and host-aware automation tasks.
Building on this, you configured the web server further by creating and utilizing custom facts from the managed host. To handle sensitive information securely, you used Ansible Vault to encrypt a user password, created a new system user with this encrypted variable, and executed the playbook non-interactively with a vault password file. The lab concluded by verifying that both the web server and the new system user were configured correctly, confirming the successful application of all learned concepts.


