Ansible Roles and Collections on RHEL

AnsibleBeginner
Practice Now

Introduction

In this lab, you will learn how to automate the configuration of a Red Hat Enterprise Linux (RHEL) web server by leveraging the power and reusability of Ansible Roles and Collections. You will build a comprehensive automation workflow by creating a custom role to deploy specific configurations, integrating an external role from a Git repository as a dependency, and utilizing a pre-built RHEL System Role from an Ansible Collection to manage system services like SELinux.

The process begins with creating a standardized role structure using ansible-galaxy init. You will then define and install a role dependency from a Git repository using a requirements.yml file. After integrating an RHEL System Role, you will assemble all three types of roles—custom, Git-based, and Collection-based—into a single master playbook. Finally, you will execute the playbook and verify that the Apache web server and SELinux settings have been correctly applied to the target RHEL server, demonstrating a complete, modular automation solution.

Create a Custom Ansible Role with ansible-galaxy init

In this step, you will begin by creating a standardized directory structure for a new Ansible role using the ansible-galaxy init command. Ansible roles are a fundamental concept for building reusable and organized automation content. They allow you to package tasks, handlers, variables, and other components into a self-contained, portable unit. Using a standard structure is a best practice that makes your automation easier to understand, manage, and share.

First, ensure you are in the correct working directory. All work for this lab will be done within the ~/project directory.

cd ~/project

Before creating a role, you need to ensure the Ansible command-line tools are installed. The ansible-core package provides the essential tools, including ansible-galaxy.

Install ansible-core using the dnf package manager. The -y flag automatically answers "yes" to any confirmation prompts.

sudo dnf install -y ansible-core

You should see output indicating that the package is being installed and dependencies are resolved.

...
Installed:
  ansible-core-2.16.x-1.el9.x86_64
  ...
Complete!

It is a common practice to organize all project roles within a dedicated roles directory. Create this directory now.

mkdir roles

Now, navigate into the newly created roles directory. This is where you will initialize your new custom role.

cd roles

You will now use the ansible-galaxy init command to create a skeleton for a role named apache.developer_configs. This command automatically generates a set of standard directories and files, providing a clean starting point for your role development.

ansible-galaxy init apache.developer_configs

After running the command, you will see a confirmation message.

- Role apache.developer_configs was created successfully

To see the structure that was just created, you can use the ls -R command, which lists the contents of a directory and all its subdirectories recursively.

ls -R apache.developer_configs

The output shows the standard directory structure for an Ansible role:

apache.developer_configs:
defaults  files  handlers  meta  README.md  tasks  templates  tests  vars

apache.developer_configs/defaults:
main.yml

apache.developer_configs/files:

apache.developer_configs/handlers:
main.yml

apache.developer_configs/meta:
main.yml

apache.developer_configs/tasks:
main.yml

apache.developer_configs/templates:

apache.developer_configs/tests:
inventory  test.yml

apache.developer_configs/vars:
main.yml

Here is a brief overview of the most important directories:

  • tasks: Contains the main list of tasks to be executed by the role.
  • handlers: Contains handlers, which are tasks that only run when notified by another task.
  • vars: Contains variables for the role.
  • templates: Contains file templates that use the Jinja2 templating engine.
  • meta: Contains metadata for the role, including dependencies on other roles.

You have now successfully created the basic structure for your custom Ansible role. In the next steps, you will populate these directories with content to configure a web server.

Install a Role Dependency from a Git Repository using requirements.yml

In this step, you will learn how to manage role dependencies from external sources, such as a Git repository. This is a common practice in larger Ansible projects where you reuse roles developed by the community or other teams. Ansible uses a file, typically named requirements.yml, to define a list of roles to be installed.

Your custom role, apache.developer_configs, will depend on a foundational Apache role to ensure the web server is installed and running. You will define this dependency and install it.

First, ensure you are in the main project directory. If you are still in the roles subdirectory from the previous step, navigate back to ~/project.

cd ~/project

Now, you will create the requirements.yml file inside your roles directory. This file will list all the external roles your project needs. Use the nano editor to create and edit the file.

nano roles/requirements.yml

Add the following content to the file. This entry tells ansible-galaxy to download a specific version of an Apache role from a public Git repository and name it infra.apache locally.

- name: infra.apache
  src: https://github.com/geerlingguy/ansible-role-apache.git
  scm: git
  version: 3.2.0

Let's break down this definition:

  • name: This is the local name for the role. Even though the source repository has a different name, it will be installed into a directory called infra.apache.
  • src: The source URL of the Git repository.
  • scm: Specifies the source control management tool, which is git in this case.
  • version: The specific Git branch, tag, or commit hash to use. Pinning a version is crucial for ensuring your automation is stable and predictable.

Save the file and exit nano by pressing Ctrl+X, followed by Y, and then Enter.

With the requirements.yml file in place, you can now use the ansible-galaxy install command to download and install the role.

  • The -r flag points to your requirements file.
  • The -p flag specifies the path where the roles should be installed.
ansible-galaxy install -r roles/requirements.yml -p roles

You will see output confirming the download and installation process.

Starting galaxy role install process
- downloading role 'ansible-role-apache', owned by geerlingguy
- downloading role from https://github.com/geerlingguy/ansible-role-apache/archive/3.2.0.tar.gz
- extracting infra.apache to /home/labex/project/roles/infra.apache
- infra.apache (3.2.0) was installed successfully

To confirm that the role was installed correctly, list the contents of the roles directory.

ls -l roles

You should now see the infra.apache directory alongside the apache.developer_configs role you created earlier.

total 12
drwxr-xr-x. 9 labex labex 4096 Nov 10 10:10 apache.developer_configs
drwxr-xr-x. 9 labex labex 4096 Nov 10 10:15 infra.apache
-rw-r--r--. 1 labex labex  118 Nov 10 10:12 requirements.yml

You have now successfully declared an external Git repository as a dependency and installed it into your project. The next step is to integrate this dependency into your custom role's metadata.

Integrate an RHEL System Role from an Ansible Collection

In this step, you will work with Ansible Collections, which are the standard way to distribute Ansible content, including roles, modules, and plugins. You will install the Community General collection, which provides a set of useful modules for automating common administrative tasks including SELinux management.

For our web server scenario, we need to correctly configure SELinux to allow the Apache service to listen on non-standard ports. The community.general collection includes SELinux modules that are perfect for this task.

First, ensure you are in the main project directory.

cd ~/project

It is a best practice to keep collections installed within the project directory to make your project self-contained. Create a directory named collections to store them.

mkdir collections

Now, use the ansible-galaxy collection install command to install the required collections. The -p flag tells the command to install the collections into the collections directory you just created.

ansible-galaxy collection install community.general:7.5.0 ansible.posix:1.5.4 -p collections

The command will download the collections and their dependencies. You will see output similar to the following:

Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'community.general:7.5.0' to '/home/labex/project/collections/ansible_collections/community/general'
Installing 'ansible.posix:1.5.4' to '/home/labex/project/collections/ansible_collections/ansible/posix'
...
community.general:7.5.0 was installed successfully
ansible.posix:1.5.4 was installed successfully

To verify that the collection is now available to your project, you can list all installed collections by specifying the collections path.

ansible-galaxy collection list -p collections

The output will show the installed collections and their installation paths within your project.

## /home/labex/project/collections/ansible_collections
Collection              Version
----------------------- -------
ansible.posix           1.5.4
community.general       7.5.0

When you use a module from a collection in a playbook, you must refer to it by its Fully Qualified Collection Name (FQCN). For SELinux management, you'll use ansible.posix.selinux for SELinux state management and community.general.seport for SELinux port management.

You have now successfully installed powerful collections that include SELinux management modules. In the next step, you will assemble a playbook that uses your custom role, the role from Git, and SELinux modules from these collections to fully configure the development web server.

Assemble and Run a Playbook with Custom, Git, and System Roles

In this step, you will bring together all the components you have prepared: your custom role, the dependency from Git, and the RHEL System Role. You will create a main playbook that orchestrates these roles to fully configure the development web server.

Think of this step as assembling a complex machine from different parts - each role serves a specific purpose, and they work together to create a complete web server environment. Let's break this down into manageable pieces:

First, ensure you are in the main project directory.

cd ~/project

Before diving into the configuration, let's understand what we're creating:

  • Ansible Configuration: Sets up how Ansible behaves and where it finds files
  • Inventory: Defines which servers to manage (in our case, localhost)
  • Variables: Store data that our roles will use (developer information, SELinux settings)
  • Custom Role Content: The actual tasks that will configure developer environments
  • Main Playbook: The orchestrator that runs everything in the right order

1. Create Ansible Configuration and Inventory

The ansible.cfg file is like a configuration file that tells Ansible how to behave. Without it, you would need to specify paths and options in every command. With it, Ansible automatically knows where to find your roles, collections, and inventory.

Create the ansible.cfg file using nano. This file tells Ansible where to find your roles, collections, and inventory.

nano ansible.cfg

Add the following content. Let's understand each line:

[defaults]
inventory = inventory
roles_path = roles
collections_paths = collections
host_key_checking = False

[privilege_escalation]
become = True

What each setting does:

  • inventory = inventory: Instead of typing -i inventory every time, Ansible will automatically use this file
  • roles_path = roles: Ansible will look for roles in the roles directory
  • collections_paths = collections: Ansible will find your installed collections here
  • host_key_checking = False: Prevents SSH key verification errors in lab environments
  • become = True: Automatically runs tasks with elevated privileges when needed

Save and exit nano (Press Ctrl+X, then Y, then Enter).

The inventory file tells Ansible which machines to manage. In our case, we're configuring the local machine.

nano inventory

Add the following line:

localhost ansible_connection=local

What this means:

  • localhost: The name of our target host
  • ansible_connection=local: Instead of SSH, use local connections (since we're managing the same machine we're running Ansible on)

Save and exit nano.

2. Define Role Variables

Variables in Ansible are like settings that your roles can use. Instead of hardcoding values like usernames or port numbers in your tasks, you define them in variable files. This makes your automation flexible and reusable.

The group_vars/all directory is a special location where Ansible automatically loads variables for all hosts. Any YAML file in this directory becomes available to your playbooks and roles.

Create the directory structure for variables that apply to all hosts:

mkdir -p group_vars/all

Now, create a file to define the developer information. This data will be used by your custom role to create user accounts and web configurations.

nano group_vars/all/developers.yml

Add the following content:

---
web_developers:
  - username: jdoe ## First developer
    port: 9081 ## Custom port for this developer's website
  - username: jdoe2 ## Second developer
    port: 9082 ## Custom port for this developer's website

What this data structure means:

  • web_developers: A list containing developer information
  • Each developer has a username and a port
  • Your custom role will loop through this list to create configurations for each developer

Save and exit.

Next, create a variable file for the SELinux configuration. SELinux (Security-Enhanced Linux) is a security module that controls what applications can do.

nano group_vars/all/selinux.yml

Add the following content:

---
selinux_state: enforcing ## Set SELinux to enforcing mode (highest security)
selinux_ports: ## List of ports to allow Apache to use
  - ports: "9081" ## Allow port 9081
    proto: "tcp" ## Protocol: TCP
    setype: "http_port_t" ## SELinux type: HTTP port
    state: "present" ## Add this rule
  - ports: "9082" ## Allow port 9082
    proto: "tcp" ## Protocol: TCP
    setype: "http_port_t" ## SELinux type: HTTP port
    state: "present" ## Add this rule

Understanding SELinux settings:

  • selinux_state: enforcing: SELinux will actively block unauthorized actions
  • selinux_ports: A list of port configurations
  • http_port_t: The SELinux type that allows Apache to bind to ports
  • By default, Apache can only use ports 80 and 443; we need to explicitly allow 9081 and 9082

Save and exit.

3. Populate the Custom Role

Your apache.developer_configs role currently has the directory structure but no actual content. We need to add:

  • Templates: Files that can include variables (using Jinja2 syntax)
  • Tasks: The actual work that Ansible will perform
  • Handlers: Special tasks that only run when notified (like restarting services)
  • Metadata: Information about role dependencies

Templates allow you to create configuration files that adapt based on your variables. The .j2 extension indicates this is a Jinja2 template.

nano roles/apache.developer_configs/templates/developer.conf.j2

Add the following content:

{% for dev in web_developers %}
Listen {{ dev.port }}
<VirtualHost *:{{ dev.port }}>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/{{ dev.username }}

    <Directory /var/www/{{ dev.username }}>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>
</VirtualHost>
{% endfor %}

Understanding the template syntax:

  • {% for dev in web_developers %}: Start a loop through the developer list
  • {{ dev.port }}: Insert the port number for this developer
  • {{ dev.username }}: Insert the username for this developer
  • {% endfor %}: End the loop
  • The result will be separate virtual host configurations for each developer

What this creates: For our two developers, this template will generate Apache configuration that:

  1. Makes Apache listen on ports 9081 and 9082
  2. Creates virtual hosts that serve content from /var/www/jdoe and /var/www/jdoe2
  3. Sets appropriate permissions for each directory

Save and exit.

Tasks are the actual work that Ansible performs. Each task uses an Ansible module to accomplish something specific.

nano roles/apache.developer_configs/tasks/main.yml

Add the following content and let's understand each task:

---
## Task 1: Create user accounts for each developer
- name: Create developer user accounts
  ansible.builtin.user: ## Use the 'user' module
    name: "{{ item.username }}" ## Create user with this name
    state: present ## Ensure the user exists
  loop: "{{ web_developers }}" ## Do this for each developer in the list

## Task 2: Create web directories for each developer
- name: Create developer web root directories
  ansible.builtin.file: ## Use the 'file' module
    path: "/var/www/{{ item.username }}" ## Create this directory
    state: directory ## Ensure it's a directory
    owner: "{{ item.username }}" ## Set the owner
    group: "{{ item.username }}" ## Set the group
    mode: "0755" ## Set permissions (rwxr-xr-x)
  loop: "{{ web_developers }}"

## Task 3: Create a sample webpage for each developer
- name: Create a sample index.html for each developer
  ansible.builtin.copy: ## Use the 'copy' module
    content: "Welcome to {{ item.username }}'s dev space\n" ## File content
    dest: "/var/www/{{ item.username }}/index.html" ## Where to put the file
    owner: "{{ item.username }}" ## File owner
    group: "{{ item.username }}" ## File group
    mode: "0644" ## File permissions (rw-r--r--)
  loop: "{{ web_developers }}"

## Task 4: Deploy the Apache configuration file
- name: Deploy developer apache configs
  ansible.builtin.template: ## Use the 'template' module
    src: developer.conf.j2 ## Source template file
    dest: /etc/httpd/conf.d/developer.conf ## Destination on the server
    mode: "0644" ## File permissions
  notify: restart apache ## Trigger the restart handler when this changes

Understanding key concepts:

  • loop: Repeats the task for each item in the list
  • {{ item.username }}: Refers to the current item's username in the loop
  • notify: restart apache: When this task makes changes, it will trigger a handler named "restart apache"
  • File permissions: 0755 means owner can read/write/execute, others can read/execute; 0644 means owner can read/write, others can read only

Save and exit.

Handlers are special tasks that only run when notified by other tasks. They're typically used for actions like restarting services.

nano roles/apache.developer_configs/handlers/main.yml

Add the following content:

---
- name: restart apache ## This name must match the notify: statement
  ansible.builtin.service: ## Use the 'service' module
    name: httpd ## The service name (Apache is called 'httpd' on RHEL)
    state: restarted ## Restart the service

Why use handlers?

  • Efficiency: The service only restarts if configuration actually changed
  • Order: All tasks run first, then all handlers run at the end
  • Idempotency: Multiple tasks can notify the same handler, but it only runs once

Save and exit.

Finally, we need to tell Ansible that our custom role depends on the infra.apache role we installed earlier.

nano roles/apache.developer_configs/meta/main.yml

Replace the file's content with:

---
dependencies:
  - role: infra.apache ## This role must run before our custom role

What this does:

  • When Ansible runs apache.developer_configs, it will automatically run infra.apache first
  • This ensures Apache is installed and configured before we add our custom configurations
  • Dependencies run in the order they're listed

Save and exit.

4. Assemble and Run the Main Playbook

A playbook is like a recipe that tells Ansible what to do and in what order. Our playbook will:

  1. Configure SELinux settings (pre_tasks)
  2. Run our roles (which includes the dependency chain)

Create the main playbook file:

nano web_dev_server.yml

Add the following content with detailed explanations:

---
- name: Configure Dev Web Server ## Playbook name
  hosts: localhost ## Run on localhost
  pre_tasks: ## Tasks that run before roles
    ## Task 1: Configure SELinux mode
    - name: Set SELinux to enforcing mode
      ansible.posix.selinux: ## Module from ansible.posix collection
        policy: targeted ## Use the 'targeted' SELinux policy
        state: "{{ selinux_state }}" ## Use the variable we defined
      when: selinux_state is defined ## Only run if the variable exists

    ## Task 2: Configure SELinux ports
    - name: Configure SELinux ports for Apache
      community.general.seport: ## Module from community.general collection
        ports: "{{ item.ports }}" ## Port number
        proto: "{{ item.proto }}" ## Protocol (tcp)
        setype: "{{ item.setype }}" ## SELinux type (http_port_t)
        state: "{{ item.state }}" ## present or absent
      loop: "{{ selinux_ports }}" ## Loop through our port list
      when: selinux_ports is defined ## Only run if the variable exists

  roles: ## Roles to execute
    - apache.developer_configs ## Our custom role (which will trigger infra.apache)

Understanding the execution order:

  1. pre_tasks: SELinux configuration runs first
  2. roles: Role dependencies run (infra.apache), then our custom role
  3. handlers: Any notified handlers run last

Why this order matters:

  • SELinux must be configured before Apache tries to bind to custom ports
  • Apache must be installed before we can configure virtual hosts
  • Service restarts happen after all configuration is complete

Save and exit.

Now you're ready to execute your complete automation:

ansible-playbook web_dev_server.yml

The playbook will execute and you'll see detailed output. Here's what to expect (for example):

PLAY [Configure Dev Web Server] *************************************************

TASK [Gathering Facts] **********************************************************
ok: [localhost]                     ## Ansible collects system information

TASK [Set SELinux to enforcing mode] *******************************************
changed: [localhost]                ## SELinux mode was changed

TASK [Configure SELinux ports for Apache] **************************************
changed: [localhost] => (item={'ports': '9081', 'proto': 'tcp', 'setype': 'http_port_t', 'state': 'present'})
changed: [localhost] => (item={'ports': '9082', 'proto': 'tcp', 'setype': 'http_port_t', 'state': 'present'})

TASK [infra.apache : Ensure Apache is installed.] *******************************
changed: [localhost]                ## Apache package was installed

TASK [apache.developer_configs : Create developer user accounts] ****************
changed: [localhost] => (item={'username': 'jdoe', 'port': 9081})
changed: [localhost] => (item={'username': 'jdoe2', 'port': 9082})

TASK [apache.developer_configs : Create developer web root directories] *********
changed: [localhost] => (item={'username': 'jdoe', 'port': 9081})
changed: [localhost] => (item={'username': 'jdoe2', 'port': 9082})

TASK [apache.developer_configs : Create a sample index.html for each developer] *
changed: [localhost] => (item={'username': 'jdoe', 'port': 9081})
changed: [localhost] => (item={'username': 'jdoe2', 'port': 9082})

TASK [apache.developer_configs : Deploy developer apache configs] ***************
changed: [localhost]                ## Configuration file was created

RUNNING HANDLER [apache.developer_configs : restart apache] *********************
changed: [localhost]                ## Apache was restarted

PLAY RECAP **********************************************************************
localhost                  : ok=17   changed=12   unreachable=0    failed=0    skipped=3    rescued=0    ignored=0

You have successfully assembled and run a complex playbook that combines multiple roles from different sources to create a complete web development environment!

Verify the SELinux and Apache Configuration on the RHEL Server

In this final step, you will verify that your Ansible automation has correctly configured the system. It's crucial to confirm that the services are running as expected and that the security policies (SELinux) have been applied correctly. You will use standard RHEL command-line tools to inspect the state of the system.

First, ensure you are in the main project directory.

cd ~/project

1. Verify SELinux Configuration

The SELinux modules from the collections were tasked with setting the SELinux mode to enforcing and allowing new ports for the http_port_t type.

Check the current SELinux status using the sestatus command.

sestatus

The output should show that SELinux is enabled and in enforcing mode.

SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

Next, use the semanage port command to verify that ports 9081 and 9082 have been added to the http_port_t context. You can pipe the output to grep to find the relevant lines.

sudo semanage port -l | grep http_port_t

You should see your custom ports listed among the default HTTP ports. The exact output may vary, but it will include the ports you defined.

http_port_t                    tcp      9082, 9081, 80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t            tcp      5988

This confirms that the SELinux modules have successfully updated the policy.

2. Verify Apache Service and Configuration

The infra.apache role installed and started the httpd service. Since systemctl is not available in this container environment, you can check for the running process using ps.

ps aux | grep httpd

You should see several httpd processes running, indicating the service is active.

root        8851  0.2  0.4  25652 16228 ?        Ss   09:31   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8852  0.0  0.1  25308  6044 ?        S    09:31   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8853  0.0  0.3 1443348 11364 ?       Sl   09:31   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8854  0.0  0.3 1443348 11480 ?       Sl   09:31   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8855  0.0  0.4 1574484 15848 ?       Sl   09:31   0:00 /usr/sbin/httpd -DFOREGROUND
labex       9298  0.0  0.0   6408  2176 pts/3    S+   09:31   0:00 grep --color=auto httpd

3. Verify Web Content Accessibility

Finally, the most important test is to see if the developer websites are accessible. Your apache.developer_configs role set up virtual hosts on ports 9081 and 9082. Use the curl command to request the content from each endpoint.

First, test the site for user jdoe on port 9081.

curl http://localhost:9081

The expected output is the content of the index.html file you created for this user.

Welcome to jdoe's dev space

Next, test the site for user jdoe2 on port 9082.

curl http://localhost:9082

You should see the corresponding welcome message.

Welcome to jdoe2's dev space

These successful curl commands confirm that Apache is correctly configured, the virtual hosts are working, and the SELinux policy is allowing traffic on the custom ports.

Congratulations! You have successfully built a complete Ansible automation project that combines a custom role, a role from a Git repository, and SELinux modules from Ansible collections to configure a secure, multi-tenant development web server.

Summary

In this lab, you will learn how to automate the configuration of a RHEL web server by leveraging the power and structure of Ansible Roles and Collections. You will begin by creating a custom role from scratch using the ansible-galaxy init command, which establishes a standardized directory structure for reusable automation content. This foundational step sets the stage for more complex automation tasks.

Building upon the custom role, you will then integrate external dependencies, including a role from a Git repository via a requirements.yml file and an official RHEL System Role from an Ansible Collection. Finally, you will assemble these different types of roles—custom, Git-based, and system—into a single playbook, execute it to configure the server, and verify the resulting Apache and SELinux settings to confirm the automation was successful.