don't worry, it's probably fine

How to write an ansible role for Ansible Galaxy

27 Mar 2014

ansible

Ansible is the “new kid on the block” of configuration management software, promoting a push-based system with orchestration as a first-class citizen, as opposed to more entrenched options such as puppet which has orchestration in the form of MCollective - an entirely different project. With the recent beta opening of Galaxy, Ansible’s answer to Puppet’s Forge, I’ve written a tutorial to take you from writing a simple module (called a role) to publishing it on Galaxy.

Creating your first role

The role I’ve selected for this demonstration is very simple: install sqlite on a machine, and have the option of provisioning databases. Unlike heavy-duty databases, sqlite only requires a client to manipulate databases and no server is needed to be setup.

From version 1.4 of Ansible and higher, the ansible-galaxy command is available which provides the option of downloading roles from Galaxy as well as setting up the scaffolding for a new role. Let’s start by creating a role called sqlite.

ansible-galaxy init sqlite

You should get a message saying sqlite was created successfully and a new directory called sqlite with a bunch of subfolders.

Inside an Ansible role

Just from the start, there are no fewer than 7 different subfolders in a role. Some of these aren’t necessary but this depends on the complexity of the role in question. We won’t need all of these in this tutoirial, but we’ll cover them all anyway.

  • defaults: main.yml in this directory is where the default out-of-the-box configuration exists (for a database server, this could be data directories, port to listen on, and others). We’ll say that all sqlite databases should exist under /var/lib/sqlite, and that we have a (for now) empty list of databases to set up
---
sqlite\_dir: /var/lib/sqlite
sqlite\_dbs: []

It’s good practice to prefix your variables with the role name ($role_$var), as we have.

  • files: Here we store files that aren’t templates or assets for the role. We have none of these for our sqlite role.

  • handlers: Handlers are tasks which can be set up to run on particular events, these are typically restarting a service or similar activity on configuration change. Again, we have none of these.

  • meta: main.yml under the meta subfolder is where we specify information about our module: who created it (us!), a description of the module, any other Galaxy dependencies it has, and so on. It also contains an explicit list of which platforms/versions it supports: we will come back to this later, but for now our main.yml looks like this

galaxy\_info:
  author: Alex Wilson
  description: Ansible role to create sqlite databases
  license: MIT
  min\_ansible\_version: 1.4
  categories:
  - database
  - database:sql
dependencies: []
  • tasks: here we store the task definitions for our role, which we’ll cover in the next section

  • templates: this is where templates that get rendered by ansible get placed. These are jinja templates with access to the Ansible variables and vars that get passed in.

  • vars: We store variables in yaml files under var, which we can load from tasks. In the example that follows, package names differ across distributions/versions so we need to be able to install the right packages, and we achieve this by having a separate .yml file for each distribution family which gets loaded as the first task (as Ansible will know the OS that it is running on).

Creating tasks for installing sqlite

On Debian-based distros, the sqlite client is available in the sqlite3 package (there is a sqlite package, but to demonstrate vars, we will treat them differently), and on distros served by rpm, there is a sqlite package.

In tasks/main.yml we start by importing the OS-specific vars (in this case the package name) and then install depending on OS, by loading vars using the Ansible-provided ansible_os_family variable.

- name: Add OS specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Install RedHat sqlite package
  yum: name={{ item }} state=installed
  with_items: sqlite_pkg
  when: ansible_os_family == 'RedHat'

- name: Install Debian sqlite package
  apt: name={{ item }} state=installed update_cache=yes
  with_items: sqlite_pkg
  when: ansible_os_family == 'Debian'

Finally, we create all of the dbs we specify in our role declaration.

- name: Create sqlite directory.
  file: >
      path={{ sqlite_dir }}
      owner={{ ansible_user_id }}
      group={{ ansible_user_id }}
      mode=0755
      state=directory

- name: Create sqlite dbs
  file: >
      name={{ sqlite_dir }}/{{ item }}
      owner={{ ansible_user_id }}
      group={{ ansible_user_id }}
      mode=0644
      state=directory
  with_items: sqlite_dbs

These two blocks form our file at tasks/main.yml

Testing the module

The convention is to include a test.yml that uses our role, so let’s try it out.

- hosts: localhost
  vars_files:
    - 'defaults/main.yml'
  tasks:
    - include: 'tasks/main.yml'

And we should end up with something that looks like this (here I actually run it as a playbook, it’s also considered good practice to “lint” the role with ansible-playbook --syntax-check -i inventory test.yml)

$ sudo ansible-playbook test.yml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [Add OS specific variables] *********************************************
ok: [localhost]

TASK: [Install RedHat sqlite package] *****************************************
skipping: [localhost]

TASK: [Install Debian sqlite package] *****************************************
ok: [localhost] => (item=sqlite3)

TASK: [Create sqlite directory.] **********************************************
changed: [localhost]

TASK: [Create sqlite dbs] *****************************************************
skipping: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0

Awesome!

Publishing to github and Galaxy

Now that we have a working role, we can commit the module to github (the Ansible documentation covers this), and follow the Ansible instructions for pulling down a repository. Ansible Galaxy allows sign on with multiple providers (such as Github, Facebook), and you can see this module here!

Writing your own re-usable Ansible roles is easy, and with Galaxy’s rating system we can share and use the best modules, enabling our infrastucture code to become better and less bespoke. Repository for this module is on github and comments/pull requests are welcome.