Once you have Ansible installed and your inventory configured, playbooks are the primary way to describe and enforce the desired state of your infrastructure. A playbook is a YAML file that groups one or more plays, each targeting a set of hosts and executing an ordered list of tasks. This tutorial covers playbook anatomy, the most commonly used modules on RHEL 8, variables, handlers, and a brief introduction to roles, giving you the tools to automate complete server configurations reproducibly.
Prerequisites
- Ansible installed on RHEL 8 (see the previous tutorial in this series)
- A working inventory file with at least one reachable managed host
- Passwordless SSH authentication configured to all target hosts
- Familiarity with basic YAML syntax (indentation with spaces, not tabs)
Step 1 — Understand Playbook Structure
Every Ansible playbook starts with three dashes and contains one or more plays. Each play sets the scope (hosts), privilege options (become), optional variables, and a list of tasks.
# site.yml — minimal playbook skeleton
---
- name: Configure web servers
hosts: webservers
become: true
vars:
http_port: 80
max_clients: 200
tasks:
- name: Ensure Apache is installed
dnf:
name: httpd
state: present
- name: Ensure Apache is running and enabled
service:
name: httpd
state: started
enabled: true
Step 2 — Use Common Modules
Ansible ships with hundreds of modules. The following covers the most practical ones for RHEL 8 server automation.
---
- name: Demonstrate common modules
hosts: all
become: true
tasks:
# dnf — install / remove packages
- name: Install required packages
dnf:
name:
- git
- vim
- curl
state: present
- name: Remove an unwanted package
dnf:
name: telnet
state: absent
# copy — push a file to managed hosts
- name: Deploy a static config file
copy:
src: files/motd.txt
dest: /etc/motd
owner: root
group: root
mode: '0644'
# template — render a Jinja2 template
- name: Deploy nginx.conf from template
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
# user — manage local accounts
- name: Create a deploy user
user:
name: deploy
shell: /bin/bash
groups: wheel
append: true
state: present
# command / shell — run raw commands
- name: Run a one-off command
command: /usr/bin/systemctl daemon-reload
- name: Run a pipeline with shell
shell: "ps aux | grep httpd | wc -l"
register: httpd_count
changed_when: false
Step 3 — Add Variables and Extra Vars
Variables make playbooks reusable across environments. Define them inline, in separate files, or pass them at runtime with --extra-vars.
# group_vars/webservers.yml — variables applied to the webservers group
---
http_port: 80
document_root: /var/www/html
app_version: "2.4.1"
# Run the playbook with extra vars at the command line
ansible-playbook -i inventory site.yml
--extra-vars "app_version=2.5.0 document_root=/srv/www"
# Reference a variable inside a task
- name: Create document root
file:
path: "{{ document_root }}"
state: directory
owner: apache
group: apache
mode: '0755'
Step 4 — Use Handlers to Restart Services
Handlers are tasks that run only when notified by another task. This is the idiomatic way to restart a service only when its configuration changes, avoiding unnecessary restarts.
---
- name: Configure and restart nginx on change
hosts: webservers
become: true
tasks:
- name: Deploy nginx configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Deploy SSL certificate
copy:
src: files/server.crt
dest: /etc/nginx/ssl/server.crt
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
Step 5 — Run the Playbook
Use ansible-playbook to execute a playbook. Useful flags include --check for a dry run and --diff to preview file changes.
# Syntax check before running
ansible-playbook -i inventory site.yml --syntax-check
# Dry run — show what would change
ansible-playbook -i inventory site.yml --check --diff
# Full run targeting only the webservers group
ansible-playbook -i inventory site.yml --limit webservers
# Run with verbose output (-v, -vv, or -vvv for more detail)
ansible-playbook -i inventory site.yml -v
# Run only tasks tagged 'deploy'
ansible-playbook -i inventory site.yml --tags deploy
Step 6 — Organise Code with Roles
A role is a standardised directory structure that bundles tasks, handlers, variables, templates, and files into a reusable unit. Use ansible-galaxy init to scaffold one.
# Create a role scaffold
ansible-galaxy init roles/nginx
# Resulting directory tree:
# roles/nginx/
# tasks/main.yml
# handlers/main.yml
# templates/
# files/
# vars/main.yml
# defaults/main.yml
# meta/main.yml
# Reference the role in a playbook
# site.yml
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx
- firewall
- app_deploy
Conclusion
You can now write well-structured Ansible playbooks that use the dnf, service, copy, template, user, and shell modules, manage variables across environments, trigger handlers for controlled service restarts, and organise growing codebases into roles. These skills form the backbone of any Ansible-based automation strategy on RHEL 8.
Next steps: Manage Secrets with Ansible Vault on RHEL 8, Install and Configure Jenkins on RHEL 8, and Install Terraform on RHEL 8.