Ansible playbooks are YAML files that describe the desired state of a set of managed servers using a series of tasks. Advanced playbook authoring goes beyond simple task lists to include: roles (reusable, shareable task bundles with a standard directory structure), handlers (tasks triggered only when a change occurs, such as restarting Nginx only when its configuration changes), variables and templates (Jinja2-powered configuration file generation from variables), conditionals and loops (applying tasks selectively or iteratively), and Ansible Vault (encrypting sensitive variables like passwords and API keys). This guide covers these intermediate to advanced patterns for writing production-quality Ansible playbooks on RHEL 9 to automate common server administration tasks.
Prerequisites
- Ansible installed on RHEL 9 with SSH access to managed nodes
Step 1 — Handlers (Conditional Service Restarts)
# handlers only trigger when a task reports 'changed'
# /srv/ansible/nginx-config.yml
---
- name: Configure Nginx
hosts: webservers
become: true
tasks:
- name: Deploy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s' # Validate before replacing
notify: Restart Nginx # Trigger the handler only if changed
- name: Deploy site configuration
copy:
src: mysite.conf
dest: /etc/nginx/conf.d/mysite.conf
notify: Reload Nginx
handlers:
- name: Restart Nginx
systemd:
name: nginx
state: restarted
- name: Reload Nginx
systemd:
name: nginx
state: reloaded
Step 2 — Variables, Templates, and Loops
# Jinja2 template: /srv/ansible/templates/vhost.conf.j2
server {
listen {{ http_port | default(80) }};
server_name {{ server_name }};
root /var/www/{{ server_name }};
}
# Playbook using loops to create multiple vhosts
---
- hosts: webservers
become: true
vars:
vhosts:
- name: site1.example.com
port: 80
- name: site2.example.com
port: 8080
tasks:
- name: Create document roots
file:
path: /var/www/{{ item.name }}
state: directory
owner: nginx
loop: "{{ vhosts }}"
- name: Deploy vhost configs
template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/{{ item.name }}.conf
loop: "{{ vhosts }}"
vars:
server_name: "{{ item.name }}"
http_port: "{{ item.port }}"
notify: Reload Nginx
Step 3 — Ansible Vault (Encrypted Secrets)
# Create an encrypted variables file
ansible-vault create /srv/ansible/group_vars/all/vault.yml
# Enter and confirm vault password
# Add variables in the editor:
# vault_db_password: 'SecurePass123!'
# vault_api_key: 'abc123'
# Reference vault variables in playbooks:
# db_password: "{{ vault_db_password }}"
# Run playbook with vault password
ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass
# Or use a password file:
ansible-playbook -i inventory.ini playbook.yml --vault-password-file ~/.vault_pass
Step 4 — Roles (Reusable Task Bundles)
# Create a role structure
ansible-galaxy init roles/nginx
# roles/nginx/
# tasks/main.yml — tasks
# handlers/main.yml — handlers
# templates/ — Jinja2 templates
# defaults/main.yml — default variable values
# vars/main.yml — role-specific variables
# Use the role in a playbook
---
- hosts: webservers
become: true
roles:
- nginx
- { role: certbot, domain: example.com } # Role with variables
Conclusion
Advanced Ansible playbooks on RHEL 9 use handlers to minimise service disruptions (services only restart when configuration actually changes), Ansible Vault to keep credentials out of version control, and roles to build a library of reusable, tested automation code. The most important operational practice is storing all playbooks and inventory in a Git repository — this creates an audit trail of every infrastructure change and allows rolling back to a known-good configuration by reverting a commit and re-running the playbook.
Next steps: How to Install Ansible on RHEL 9, How to Install Terraform on RHEL 9, and How to Install Jenkins on RHEL 9.