Being a Star in Galaxy

April 12th, 2015

Being a Star in Galaxy

This article is about the Ansible Galaxy. It describes a few things best practices I discovered while trying to write my roles as flexible as possible.

Use common sense

First of all, I'd say that it's important to name your role well. Or at least provide a nice description. Searching for "nginx" on Galaxy will show you what I mean, there are at least 40 roles to "install and configure Nginx"...

readers who are paying attention will see an F500 role that violates this very rule. We'll try to do better!

Also, please follow Semantic Versioning for your role updates. Ansible's galaxy install command is not very sophisticated but you can keep a fixed version of a role. When using other people's roles you really should audit the code and lock the version. You are after all running their code on your servers, perhaps even with escalated privileges...

Finally, prefix all your variables with the name of your role. You do not want to cause a variable clash with some other role by using is_secure or something generic:

  • my_role_is_secure: true (I hope)

Besides those three common-sense tips, here are 6 things you could to to increase re-usability:

Tip 1. Use plenty of defaults and switches

In a role, you can create a defaults/main.yml file with default values for all variables in your role. Besides variables in your tasks, if there is a template, create defaults for as many of the settings as you can. You might never change any of them yourself, but others might find it extremely frustrating to have to duplicate your role just because you left a value hardcoded.

# In a certain unnamed .ini template file

Should be:

display_errors={{ role_name_display_errors }}

Don't set defaults where it doesn't make sense though. If a consumer needs to provide certain info, document it and let the role break if they fail to RTFM.

Adding plenty of switches to turn certain behaviour on/off is also nice. Simply add a variable and a when statement to your tasks:

command: ""
when: something_cool_should_run

Tip 2. Set variable paths for all files and templates

When you have templates in your role, more often than not those are based on program defaults or best practices. Having every option in a default variables allows most templates to be used for all purposes, but sometimes it's just too much work to make everything configurable.

Having a variable path for templates allows your uses to override them quite easily, without breaking anyting:

-  template: "src=template-name.j2 dest=/some/destination"

is replaced with:

-  template: "src={{ some_template_path | default() }}template-name.j2 dest=/some/destination"

This defaults to your role/templates/template-name.j2, so nothing changes. But if a consumer of your role wants to override the template, they can do so by setting the some_template_path variable and creating their own version of the template.

Tip 3. Don't create a monolith

Roles should be small, and do one job. If there are multiple things to install, don't put those into a single role because they will be married forever. And you really only ever want to marry the right one, right? If you make roles small, you can easily compose them differently for different servers.

But sometimes, even a single program can benefit from multiple roles. If the program requires extensive configuration, or the configuration changes often, it can pay to create an installation and a configuration role:

role: install_nginx
role: configure_nginx (depends on install_nginx)

(to make it idempotent, your installation role could contain an option to remove the default configuration)

Another version of this is the settings role. In this case, you create an installation role that can install multiple versions of something - based on variables. You then create multiple settings roles, that contain the proper values to install a certain version:

role: install_ruby
role: install_ruby-1.9.3 (depends on install_ruby)
role: install_ruby-2.0.0 (depends on install_ruby)

I saw this technique used by Joshua Lund and liked it a lot

Tip 4. Create hooks with shell commands

Within roles, you can easily add a simple loop to run commands. This allows the consumer of your role to run some of their own commands at specific points in the role:

- command: "{{ item }}"
  with_items: list_of_commands

They set their commands in a list variable:

    - "app/console cache:clear"
    - "app/console assetic:dump"

Don't forget to set the list_of_commands variable as an empty list in your defaults!

As an added bonus, add an "environment" variable so their commands can behave different depending on external information.

environment: project_environment

Only use this technique if you explicitly want to limit the consumers to a set of commands. If you want to let them execute arbitrary tasks/ansible modules, use the next tip.

Tip 5. Create hooks by including task files

To have consumers of your role to write their own tasks, to be executed inside your role, add an include statement where you want to allow this. The include statement should take a variable that can be set to full path to a tasks file:

- include: "{{ hook_variable | default(lookup('pipe', 'pwd') ~ '/hooks/empty.yml') }}"

Because of how Ansible works, the 'default' and 'lookup' filters have to be used to point to an existing empty file. You need to create this empty file inside your role, inside the 'tasks' folder. In the example above, the path would be [role]/tasks/hooks/empty.yml

This include statement will do nothing by default, but if the consumer sets the hook_variable then their tasks are executed (using the playbok_dir variable makes it possible to use a relative path):

hook_variable: "{{ playbook_dir }}/hooks/tasks-file.yml" 

This is the most powerful way of allowing your role to be adapted to personal taste, but it's also complex to explain to the target audience of your role. Use it wisely.

Tip 6. Separate modules into roles

If you role contains a module, chances are some people will like the module but not the role. Ansible does not provide a way to load modules inside roles without running the role. In order to allow others to benefit from your module outside of the role, break out the module into it's own role, and depend on it yourself.

your_role (depends on your_role_module)

Now, your module is available separately with zero cost to you. You can see an example of this in our project_deploy role, that depends on project_deploy_module. The module role contains only the galaxy meta information and a library folder.

So, there you have it.

Now you've got some pointers on how to make better and more re-usable roles. I sincerely hope to save myself some time by using them in the future!

Ramon de la Fuente

Pointy haired boss