Skip to content

Converting Playbook Into Role


This is the moment promised at the start of this section: the “Full Synthesis” deployment playbook from the close of the common modules section, refactored into a proper, reusable role — piece by piece, with the reasoning behind each decision made explicit.

Step 1: Scaffold The Role

ansible-galaxy role init roles/myapp

Step 2: Identify What Should Be Configurable — defaults/main.yml

Everything hardcoded in the original playbook that a reasonable user might want to change for their own deployment:

roles/myapp/defaults/main.yml
app_dir: /etc/myapp
app_owner: alice
app_group: alice
app_dir_mode: '0755'
max_connections: 100
health_check_port: 8080
health_check_retries: 10
health_check_delay: 3
service_name: myapp

Every one of these was a hardcoded literal in the original playbook — the directory path, the tuning value, the health check port. Now they’re genuinely configurable, at the lowest precedence, exactly as the defaults chapter established.

Step 3: Identify What’s Internal — vars/main.yml

One value in the original playbook is derived from another, and shouldn’t be set independently without breaking that relationship:

roles/myapp/vars/main.yml
conf_d_dir: "{{ app_dir }}/conf.d"

conf_d_dir only makes sense as a subdirectory of app_dir — letting someone override it independently, disconnected from wherever app_dir actually points, would break the role’s own internal assumptions. This is exactly the kind of value the high-precedence vars/main.yml is for.

Step 4: Move The Tasks — tasks/main.yml

Every task from the original playbook, now referencing role variables instead of hardcoded values:

roles/myapp/tasks/main.yml
- name: Ensure app directory exists
  ansible.builtin.file:
    path: "{{ app_dir }}"
    state: directory
    owner: "{{ app_owner }}"
    group: "{{ app_group }}"
    mode: "{{ app_dir_mode }}"

- name: Install Apache (Debian)
  ansible.builtin.apt:
    name: apache2
    state: present
  when: ansible_facts['os_family'] == 'Debian'

- name: Install Apache (RedHat)
  ansible.builtin.dnf:
    name: httpd
    state: present
  when: ansible_facts['os_family'] == 'RedHat'

- name: Deployment attempt
  block:
    - name: "Copy discovered config: {{ item | basename }}"
      ansible.builtin.copy:
        src: "{{ item }}"
        dest: "{{ conf_d_dir }}/{{ item | basename }}"
      loop: "{{ query('fileglob', role_path + '/files/configs/*.conf') }}"
      notify: Restart app

    - name: Deploy main config from template
      ansible.builtin.template:
        src: app.conf.j2
        dest: "{{ app_dir }}/app.conf"
      notify: Restart app

    - name: Ensure a specific tuning setting is correct
      ansible.builtin.lineinfile:
        path: "{{ app_dir }}/app.conf"
        regexp: '^max_connections'
        line: "max_connections = {{ max_connections }}"
      notify: Restart app

  rescue:
    - name: Alert on any deployment failure
      ansible.builtin.debug:
        msg: "Deployment failed on {{ inventory_hostname }}"
      changed_when: true
      notify: Send alert

- name: Wait for the app to respond after any restart
  ansible.builtin.command: "curl -s -o /dev/null -w '%{http_code}' http://localhost:{{ health_check_port }}/health"
  register: health_check
  until: health_check.stdout == "200"
  retries: "{{ health_check_retries }}"
  delay: "{{ health_check_delay }}"

Step 5: Move The Handlers — handlers/main.yml

roles/myapp/handlers/main.yml
- name: Restart app
  ansible.builtin.systemd:
    name: "{{ service_name }}"
    state: restarted

- name: Send alert
  ansible.builtin.debug:
    msg: "ALERT: deployment failed on {{ inventory_hostname }}"

Step 6: Move The Template

app.conf.j2 moves into roles/myapp/templates/, and template’s src: app.conf.j2 in the tasks above needs no path at all — the automatic role-relative resolution from earlier in this section handles it.

A New Consideration: fileglob Inside A Role

Look closely at the copy task above — query('fileglob', role_path + '/files/configs/*.conf') is doing something worth understanding, not just copying blindly. copy and template’s src: parameters get automatic role-relative resolution for free, but a lookup plugin like fileglob doesn’t get that same treatment — it’s a general-purpose function, not tied to the role’s own file-resolution mechanism. role_path, a magic variable holding the current role’s own directory, makes the reference explicit: {{ role_path }}/files/configs/*.conf reaches into this specific role’s files/ directory deliberately, rather than relying on whatever the ambient relative-path resolution would otherwise assume.

Step 7: The Playbook, Reduced To Almost Nothing

site.yaml
- name: Full Application Deployment
  hosts: servers
  roles:
    - myapp

Everything the original play did is still happening — it’s just packaged, reusable, and configurable now instead of hardcoded in one long play.

The Full Before And After

Deploying a second, genuinely different application with the exact same role, using mechanism two from the variables-passing chapter:

site.yaml
- name: Deploy Two Apps With The Same Role
  hosts: servers
  roles:
    - role: myapp
      app_dir: /etc/webapp
      service_name: webapp
      health_check_port: 8080
    - role: myapp
      app_dir: /etc/apiapp
      service_name: apiapp
      health_check_port: 9090

This is the entire point of this section, now fully real: the same install-configure-restart-verify workflow, applied twice, to two genuinely different applications, with zero duplicated logic.

Best Practices

  • Refactor a working playbook into a role once you find yourself wanting to apply the same pattern more than once — this exact motivation, from the start of this section, is the signal to act on.
  • Sort every hardcoded value into defaults/ or vars/ deliberately, based on whether it’s genuinely something a user should configure, or something the role’s own logic depends on staying consistent.
  • Use role_path explicitly for lookup plugins that need to reach into the role’s own files, since lookups don’t get the same automatic resolution copy/template’s src: enjoys.
Last updated on