The second post in the Ansible: Control Structures series describes using Ansible core components to emulate subroutines - procedures and "packages."

The whole paradigm of Ansible is the opposite of some core programming languages principles - encapsulation and isolation. Any task on the same host has access to all the process facts. All facts from different sources are compiled with the single pool accessible for all tasks in the playbook. If you have some fact declared in multiple places: inventory, play, or role, Ansible will calculate the final value using a precedence list. If you haven't learned it before, take a moment and study it for good. So, why do we discuss variables instead of subroutines? Because you should always keep in mind:

  • There is no true encapsulation. Even you consider a variable as local actually it's available for all subsequent tasks and roles. Same for global facts: something from another play could overwrite your defaults.
  • There are no procedure parameters. Instead, they are variables with all the consequences. So, naturally, you cannot return anything from your "subroutines."

With this necessary warning, let's name the ways you can simulate subroutines without custom modules.  There are only a few:

  • include_tasks -  This module allows you dynamically execute a set of tasks in a predefined order. The task points to a list of tasks in a separate file. The list of tasks should be a valid YAML file and contains tasks only. This module enables selections and iterations in Ansible. Even without encapsulation, you can pass parameters to the task and use them inside a task list. Here is a sample code:
- hosts: all
  vars: 
    my_playbook_var: yes
  tasks:
     - name: Call Subrotine with Parameters
       include_tasks:  tasks-to-run.yml
       vars:   
         my_task_var: "{{ not my_plabook_var|bool }}"
         
Include tasks with parameters (procedure)
  • Ansible roles - The language structure allows you to encapsulate a complex set of tasks and enrich the core set of modules with the environment- or product-specific actions. Playbook offers two places for role calls: roles part of the Ansible play, and the task include_role. The Ansible engine executes roles listed in the roles section, prior execute tasks section, while the include_role task allows you to execute a role among the other play tasks.    
- hosts: all
  vars: 
    my_playbook_var: yes
  roles: 
    - my_awesome_role1
      vars:
        my_role_var: "{{ not my_playbook_var|bool }}"
  taks:
    - debug:
        msg: "I go between my awesome roles"
    - include_role: 
        name: my_awesome_role2
      vars:
        my_role_var: "{{ not my_playbook_var|bool }}" 
    - debug: "I go after my_awesome_role2"    
Use Roles in Ansible Play

And just a few points for the conclusion:

  • All ansible variables, aka facts, are available within the playbook. The current variable value calculates for targets using fact's precedence list;
  • You can simulate encapsulation use role-specific variable names. However, you can lose some of the most powerful Ansible features.      
  • You can simulate procedures and packages with a set of tasks in the separate file and Ansible roles (standalone or within collections);
  • Modules include_tasks and include_role enables precise control over the execution task order;
  • You can "pass" parameters to roles and subtasks using the vars clause.