Package Reusable Automation as a Role

AnsibleBeginner
Practice Now

Introduction

Roles package a working automation unit so consumers can supply data without duplicating implementation. You will create a role with public defaults, render host-specific pages, keep configuration validation and reload behavior inside the role, and reuse it across two inventory groups. Real managed files and HTTP responses will verify that the role is actually invoked.

Create and Invoke a Role with Defaults

In this step, you will move website automation into a role and invoke it through a short playbook using reusable default inputs.

A role groups related tasks, default variables, templates and handlers under a standard directory. Its tasks/main.yml is the task entry point; defaults/main.yml supplies low-precedence values that consumers can override. Template sources go under templates, and handlers/main.yml will later hold change-driven service actions.

The prepared inventory already places web1 in canary and web2 in stable, beneath the sites parent group. Nginx is installed as a prerequisite. Inspect the groups:

cd /home/labex/project
ansible-inventory --graph

Create the role directories with mkdir -p. Multiple path arguments create each named directory:

mkdir -p roles/managed_site/tasks roles/managed_site/defaults roles/managed_site/templates roles/managed_site/handlers

Prefix public role variables with site_ to make their purpose clear. Start with shared defaults that any consumer can use:

cat > roles/managed_site/defaults/main.yml <<'YAML'
site_environment: training
site_port: 8080
site_maintenance: false
YAML

The page template uses the role input together with each target's name and gathered distribution:

cat > roles/managed_site/templates/page.txt.j2 <<'JINJA'
host={{ inventory_hostname }}
environment={{ site_environment }}
distribution={{ ansible_facts.distribution }}
JINJA

Create the role's task entry point. Inside a role, src: page.txt.j2 resolves under that role's templates directory; the destination remains an absolute managed path:

cat > roles/managed_site/tasks/main.yml <<'YAML'
- name: Prepare the role-managed website directory
  ansible.builtin.file:
    path: /var/www/role-site
    state: directory
    owner: root
    group: root
    mode: "0755"

- name: Render the role-managed page
  ansible.builtin.template:
    src: page.txt.j2
    dest: /var/www/role-site/index.html
    owner: root
    group: root
    mode: "0644"
YAML

Invoke the role with the roles keyword. The surrounding play selects targets, gathers facts and requests become; the role supplies the reusable tasks:

cat > site.yml <<'YAML'
- name: Apply the managed site role with defaults
  hosts: sites
  gather_facts: true
  become: true
  roles:
    - managed_site
YAML

Validate and run the role through its consumer playbook:

ansible-playbook site.yml --syntax-check
ansible-playbook site.yml

Inspect the actual role-created pages:

ansible sites -m ansible.builtin.command -a 'cat /var/www/role-site/index.html'

Both should show environment=training from the default, with their own host names and distribution=Ubuntu. Merely creating role directories would not execute the role or create these managed files.

Keep Templates and Handlers Inside the Reusable Role

In this step, you will make the role configure and run the website, including candidate validation and its own reload handler.

Put the complete nginx template beside the page source. It uses the role's site_ inputs, the previously practiced upper filter and a Boolean branch:

cat > roles/managed_site/templates/nginx.conf.j2 <<'JINJA'
events {}
http {
    server_tokens off;
    server {
        listen {{ site_port }};
        root /var/www/role-site;
        index index.html;
        add_header X-Environment "{{ site_environment | upper }}" always;
{% if site_maintenance %}
        add_header X-Maintenance "enabled" always;
{% else %}
        add_header X-Maintenance "disabled" always;
{% endif %}
    }
}
JINJA

Append role tasks that validate the rendered configuration before replacement and explicitly keep nginx started. The changed configuration notifies the role's named handler:

cat >> roles/managed_site/tasks/main.yml <<'YAML'

- name: Render and validate the role-managed nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: /usr/sbin/nginx -t -c %s
  notify: Reload managed site

- name: Keep the role-managed service running
  ansible.builtin.service:
    name: nginx
    state: started
YAML

Write the handler in the role's handler entry point rather than duplicating it in every consumer playbook:

cat > roles/managed_site/handlers/main.yml <<'YAML'
- name: Reload managed site
  ansible.builtin.service:
    name: nginx
    state: reloaded
YAML

Run the unchanged consumer playbook. The role now supplies its complete deployment behavior:

ansible-playbook site.yml

Both hosts should start their own site on default port 8080. Inspect each target through its own loopback endpoint:

ansible sites -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8080/'

Each body should identify its own host and environment=training. Headers should include X-Environment: TRAINING and X-Maintenance: disabled. The output comes from running services configured by the role, rather than a controller-only demonstration.

Reuse the Same Role with Group Variable Overrides

In this step, you will apply the same role to two group contexts without editing its reusable defaults or task files.

Role defaults deliberately have low precedence. Inventory group variables can override these public inputs while retaining the same implementation. Keep defaults/main.yml unchanged and create each group's consumer data:

cat > group_vars/canary.yml <<'YAML'
site_environment: development
site_port: 8081
site_maintenance: true
YAML
cat > group_vars/stable.yml <<'YAML'
site_environment: production
site_port: 8082
site_maintenance: false
YAML

Replace the consumer playbook with two plays that invoke the same managed_site role. Each group receives its own resolved inputs. Do not duplicate the role directory for each environment:

cat > site.yml <<'YAML'
- name: Apply the shared role to canary
  hosts: canary
  gather_facts: true
  become: true
  roles:
    - managed_site

- name: Apply the shared role to stable
  hosts: stable
  gather_facts: true
  become: true
  roles:
    - managed_site
YAML

Validate and apply both contexts:

ansible-playbook site.yml --syntax-check
ansible-playbook site.yml

Both environments should load their changed configuration through the role-local handler. Inspect the actual endpoints at their new configured ports:

ansible canary -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8081/'
ansible stable -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8082/'

Canary should identify web1 and development, with DEVELOPMENT/enabled headers. Stable should identify web2 and production, with PRODUCTION/disabled headers. These differences come from group inputs, while one shared role still owns defaults, tasks, templates and handlers.

Repeat the same consumer playbook:

ansible-playbook site.yml

Both recap rows should report changed=0, unreachable=0 and failed=0, with no handler scheduled. A reusable role must be invoked and produce correct converged managed state; a directory scaffold alone is not a deployment.

Summary

You organized defaults, tasks, templates and handlers into one invoked role. Low-precedence defaults supported consumer overrides, and two inventory groups reused the same implementation to produce distinct real services. Candidate validation, change-driven reloads and unchanged repeat execution remained part of the reusable unit.