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.