The Anatomy of Roles in TripleO-Ansible

The Anatomy of Roles in TripleO-Ansible

TripleO-Ansible roles are built to be simple to understand, easily documented, and tested. In this post, I will go over the parts of a TripleO-Ansible role and why they're important.

What is an Ansible Role

Roles are simple and well documented by Ansible,

Roles are ways of automatically loading certain vars_files, tasks, and handlers based on a known file structure. Grouping content by roles also allows easy sharing of roles with other users.

When it comes to TripleO-Ansible, roles are structured outcomes in a reusable format.

TripleO-Ansible Role Structure

The role structure falls into 6 parts

  1. Defaults
  2. Vars
  3. Tasks
  4. Meta
  5. Molecule
  6. Documentation

While templates, files, libraries, and handlers are all part of the TripleO-Ansible roles, the core features and functionality of TripleO-Ansible roles are captured in these principal components.


Defaults

Defaults are all options that are intended to be adjusted by an operator. When defining a role, all of the options should have a prefix, including the role name. Comments should be made within the defaults file explaining how each option is used.

For example, if I created a new role name tripleo-example, I would prefix my default options with tripleo_example. While this creates long variable names, it ensures there are no unintended variable collisions and makes it clear where a given default is being used.

Example option names
tripleo_example_endpoint: https://example.com
tripleo_example_workers: 10

There are no limits to the number of options one can set within the defaults/main.yml file. However, all options should be sensible, follow the guidelines and best practices for the application or operational task being automated, and result in a usable system at the end of execution.

Vars

Vars contain parameters which are not operator-facing. While vars can be modified by the operator they're not intended to be modified. Any modification to vars would fall under "advanced usage" making such a modification not supported.

The primary vars file is vars/main.yml; this file will load whenever the role is invoked. Additional variable files may be useful when developing a role supporting things like multiple operating systems, CPU architectures, or a mutually exclusive application matrix. While there are numerous variable loading patterns in Ansible, TripleO-Ansible deploys a multi-distro aware task in all new roles. The multi-distro aware task allows developers to create roles without having to figure out how to build in support for multiple distros later.

# found within the "vars/" path. If no OS files are found the task will skip.
- name: Gather variables for each operating system
  include_vars: "{{ item }}"
  with_first_found:
    - skip: true
      files:
        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower }}.yml"
        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
        - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
        - "{{ ansible_distribution | lower }}.yml"
        - "{{ ansible_os_family | lower }}-{{ ansible_distribution_version.split('.')[0] }}.yml"
        - "{{ ansible_os_family | lower }}.yml"
  tags:
    - always
The multi-distro capabilities are built into the TripleO-Ansible role skeleton.
tree tripleo-example
├── defaults
│   └── main.yml
├── tasks
│   └── main.yml
└── vars
    ├── fedora-28.yml
    ├── main.yml
    ├── redhat-8.yml
    └── redhat.yml

In this example role, we can see that there are 4 variable files, with three of them named after a supported GNU/Linux distro. This is done so each role can store parameters on a per-operating-system basis. These files can then reuse keys with different values making it possible to have a single set of tasks that easily support multiple operating systems.

Tasks

Tasks are stanzas which execute various modules. The primary task file is tasks/main.yml. This task file, while typically the most critical file, may not be the only file in a given task hierarchy. Depending on the role scope and size, it may be best to break tasks up into smaller files which are included when different conditions are met. In practice, we find two patterns to be the most adventitious.

  • execution

The execution pattern is where all tasks contained within a given role are housed within the one tasks/main.yml file. This pattern is best suited for small roles with a relatively small number of tasks.

  • router

The router pattern is where most tasks are contained within files outside of the tasks/main.yml file. Task files are then included from tasks/main.yml or within other sub-task-files as needed. This pattern is best suited for larger, more complex roles, which may have different code paths potentially reactive to user input or environmental facts.

There are no guidelines on which pattern to use. It is up to the role developer to determine which pattern works best for the given situation. That said, in TripleO-Ansible, most roles follow the router pattern, even when small. This was intentionally done so that the roles are uniform with one another, and can easily be extended.

The file tasks/main.yml should also compliment the defaults and vars files by loading any additional options from either of these directories and running sanity checks on operator input.

It is useful to build in failure conditions into a role's tasks/main.yml. Failure conditions halt a role's execution in the event there's invalid user input. These tasks act as sanity checks to guard the system from incorrect user input.
- name: Check rule set
  fail:
    msg: >-
      `{{ item['rule_name'] }}` firewall rule cannot be created. TCP or UDP rules
      for INPUT or OUTPUT need sport or dport defined.
  when:
    - ((item['rule']['proto'] | default('tcp')) in ['tcp', 'udp']) and
      (item['rule']['dport'] is undefined) and
      ((item['rule']['chain'] | default('INPUT')) != 'FORWARD') and
      ((item['rule']['table'] | default('filter')) != 'nat')
  loop: "{{ firewall_rules_sorted }}"

In this example, a task is looking for firewall rules missing parameters. It is also looking for parameters known to fail. If the condition is met the task will fail and return information as to why it failed.

Meta

Within the meta directory, there is usually only one file, meta/main.yml. This single file contains information about the role and any dependencies it may have. While this file is typically untouched after a role has been created, it is necessary. The metafile can be very useful when trying to understand what a given role supports, and what, if any, dependencies it may have.

Molecule

The molecule directory exists specifically for testing via the test runner, molecule. Each sub-directory within the molecule directory represents one test scenario. When a role is created only one directory is required, default. The default test directory is used to execute a role test using just the default variables. When a role is generated, a molecule config is built using a basic set of provisioning and execution playbooks. The generated scenario will run tests using docker, however, this is not the only option. It is up to the role developer to devise a test strategy that makes sense for a given role.

In some cases, docker works perfectly. In other cases, it is the wrong solution entirely. Consult the driver documentation for all available drivers.
Documentation

Documentation of a role is critical to understanding what it does, what the available options are, and how to consume a given role. TripleO-Ansible generates documentation automatically for all roles and plugins created within the code repository, so long as there's a documentation stub available for a given resource. For example, to generate role documentation for the tripleo-example role, a file named doc/source/roles/role-tripleo-example.rst is needed with the following content.

======================
Role - tripleo-example
======================

.. ansibleautoplugin::
   :role: tripleo_ansible/roles/tripleo-example

The plugin ansibleautoplugin, scans a role and document everything it knows how. The generated documentation will address any nested plugins, all the defaults, all of the vars, all of the molecule tests, and any important tasks. While this documentation plugin is thorough, there are times when additional information is useful to add to the generated documentation.

All additional documentation should be written in reStructuredText and saved within the documentation file.

Generating a new role

While my previous post covers creating a new role in more depth, its worth mentioning here that TripleO-Ansible has an Ansible playbook which generates a new role skeleton with all of the building blocks covered in this post.

(tripleo-development) $ ansible-playbook -i 'localhost,' \
                                         role-addition.yml \
                                         -e ansible_connection=local \
                                         -e role_name=tripleo-example

This Ansible playbook generates the role, create the molecule default tests, and provides a documentation stub. Once the generation is complete, a developer can begin adding tasks, cleaning up the generated role, and removing anything that may not be of use.

While I covered the anatomy of a proper role, not every role needs all of the parts generated by the above playbook. Some roles will need handlers, some will need templates, others will need files, have dependencies, and others still will need multiple molecule tests to validate complete functionality. The generated role creates everything needed to get started and focuses on roles that are easy to use and reuse; however, if there's something not needed to remove it. It is up to the role developer to decide what is right. We want our roles complete, and extensible, but there's no need to keep empty files around just for the sake of completeness.

KISS

Keeping roles simple is a lot harder than it sounds, and its the job of everyone in our community to review each other's work. There are times when complexity is needed, and there are other times when it just unnecessary overhead. While Ansible is well equipped with some fantastic foot guns, our tooling aims to buck the trend in configuration management by not shooting our operator's feet. Avoiding foot guns is why we have role generators, auto-documentation plugins, syntax checks, lint checks, scenario tests, and molecule tests all built-in. We're doing everything we can ease the development process while also making sure all released tooling meets or exceeds the high standards enterprises have come to expect.

That's all folks

I hope this role breakdown was insightful and further clarifies some of the components of TripleO-Ansible. We're building tools to setup and operate OpenStack clouds, which is no simple task, but the implementation of our tooling aims to be simple. Easy executing and understand is our goal while providing robust full-featured application delivery platform operators can trust. The roles, plugins, and tests are new but were forged from the enterprise technology stack.  Today's advancements will power tomorrows clouds and while it's a fine line we're walking, between chaos and harmony, the TripleO community is doing a great job. We're simplifying our codebase and marching toward an easier future with multi-operation tooling built for scale.

Mastodon