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.


