Ansible Recommended Patterns

It can be tricky to figure things out when structuring new projects. You might set out to make things as comprehensive as possible, to accommodate future expansion, but this raises the barrier to entry and can leave you in a quandry about where things should go. Or you might opt for the lean approach, making things super simple and extending as you go, but you don’t want to set yourself up for big refactoring sessions later on.

These are a collection of recommendations that I have for new Ansible projects. They can equally be applied to existing projects, depending on your needs and the effort involved. At the end I’ve also linked to a skeleton repository that you’re free to clone and make your own.

Use wrapper scripts

It’s good to use tools in the way they were intended and in an ideal world we wouldn’t need wrapper scripts, but conversely with orchestration tools like Ansible you want to standardize the way you use it for your project or organization. Your Ansible project might also need to run in multiple places, like your CI server, and wrapper scripts will help to encapsulate the runtime environment and settings.

Everything in one repository

I’m a big proponent of infrastructure and orchestration monorepos. In terms of Ansible, this means your playbooks, variables, scripts, roles, plugins, inventory scripts and configuration all resides together and is version controlled in the same repository. This makes it possible to deploy your code anywhere, on any platform, with root access or not, and be confident that all you need to execute is in one place.

Lean playbooks

Playbooks are your point of entry when running Ansible repeatedly, and they need to be easily readable and understandable without having to read hundreds of line of role invocations and arbitrary tasks. They should be short, to the point, and the roles that are called in your playbooks should represent the broad actions the playbook is taking. Don’t repeat stuff in each playbook, rather make composable, includable playbooks and chain them together using include: (Ansible <2.5) or import_playbook: (Ansible 2.5+).

Roles represent business logic

The real reason I encourage writing your own roles is that your roles should represent your own business logic and your own operational requirements. If you are installing nginx, what is it for? If it’s for a reverse proxy layer, then build a role for deploying your nginx reverse proxies. Don’t be afraid of having multiple roles install the same packages if they are for different reasons, and don’t be dazzled by the complicated, does-everything role on Ansible Galaxy. It probably requires you to provide a huge amount of input just to get the feature you want, when you’d be much better served by baking that feature into your own role. Use role dependencies wisely, and remember that you can inject conditionals and parameters just about everywhere. But most importantly, codify your own business logic.

Namespace your group variables

One issue with group variables is that you can’t declare a set of variables that apply to an intersection of groups. This is fine to begin with, but before long you’ll have groups based on project, server role, location, etc and you’ll want finer grained control over your group variables. My preferred approach is to namespace group vars by project and to enable dictionary merging. The setting to use in ansible.cfg is hash_behaviour = merge. With this in place, you can set variables like this…

# group_vars/tag_project_atom/main.yml

atom:
  repo: git@github.com:acme/atom.git
  user: projatom
# group_vars/tag_role_app/atom.yml

atom:
  packages:
    - nginx
# group_vars/tag_env_production/atom.yml

atom:
  autoscaling:
    instance_count: 5
    instance_type: m4.large

… and they will all be merged into a single atom dictionary, from which you can reference relevent attributes for hosts in the respective groups.

If you ever need to dynamically address the atom dictionary itself, you can use the syntax hostvars[ansible_hostname][project] where project is a supplied extra variable or such.

Whilst on the topic of group variables, I thoroughly recommend putting your group var files into subdirectories, e.g. group_vars/all/main.yml. This helps to prune your variables and keep them in check, and avoid the hell that is a 1000 line configuration file.

Don’t be afraid to write plugins

Especially if you don’t code much Python, the prospect of writing a plugin for Ansible sounds rather daunting. But most Ansible plugins are super simple, almost one-liner scripts. If you find yourself chaining several Jinja filters together to manipulate something into the format you want, then you might consider putting that functionality into a plugin.

To make a filter plugin, copy this boilerplate into filter_plugins/my-plugin.py (relative to your playbook).

def noop(val):
    return val

class FilterModule(object):
    def filters(self):
        return {
            'noop': noop,
        }

This noop plugin literally does nothing to the value you provide. Use it like this:

- name: Print thing
  debug:
    msg: "{{ 'foo' | noop }}"

You can find some other useful filter plugins on my GitHub profile.

Skeleton Project

When starting a new Ansible project, I usually start with my skeleton project, and as I pick up new patterns, or improve my approach, I go back and update my skeleton project so that newer projects can benefit. It’s available on GitHub and I recommend using it specifically if you’re starting out with Ansible on AWS.