Restricting the Ansible Version in Use


At YPlan we love Ansible. We use it for controlling our various AWS instances and other services, as well as with Vagrant for development environments that are in sync. This way if we want to upgrade a system package or piece of configuration, it needs only be written once in Ansible before being applied everywhere.

One natural problem here though is that Ansible itself needs keeping in sync across the various places that use it. For our Jenkins server that’s not too hard, as we control how things are installed on it already, via, of course, an Ansible role. But for developer machines Ansible runs outside of Vagrant and is thus installed on the host machine. It’s possible to run it inside the Vagrant virtual machine, but there are other playbooks to run for deploying production services that are better run on the host, so we’d prefer it just be set up in one place.

We could just go with Ansible upgrades whenever they come out. They’re relatively good at keeping backwards compatibility, and the near 100% backwards compatibility of Version 2 was impressive, but not perfect. We find that with each upgrade there’s normally some small breakage in a module, or a corner case as to how one particular role or playbook does things. Unfortunately a small breakage can bring all the developers in our team to a stop.

We realized that in the ideal world, everyone in our organization would use the exact same version of Ansible, and change only when we’re ready to upgrade. So we looked for a way of making Ansible fail if it wasn’t the current internal version.

The first draft of this code was to do it in plain Ansible. We had a playbook include file includes/check_ansible_version.yml like so:

- name: check ansible version
  hosts: localhost
  connection: local
  gather_facts: no
    yplan_ansible_version: '1.9.6'
    - name: check ansible version
      when: ansible_version.string != yplan_ansible_version
        msg: >
          Please use Ansible {{ yplan_ansible_version }} only. Install with:
          pip install ansible=={{ yplan_ansible_version }}
      tags: [always]

We then included this at the top of each playbook with:

- include: includes/check_ansible_version.yml

# ... the rest of the playbook

This worked quite well but it required a boilerplate line at the top of every playbook that could easily be forgotten. Another issue arose when we moved from sudo to become syntax in our playbooks with the upgrade to the 1.9 series - the old version of Ansible crashed parsing the new become keyword since it wasn’t recognized, so the playbook never started, and the message never appeared - right when it was needed!

The second implementation, and the one we’re still using, is as a callback plugin. This is loaded by Ansible as it starts, giving us a hook to run code and check the current version. We made it compatible with both Ansible 1 and 2 so that it would work across that upgrade too (the plugin architecture changed in 2).

We saved the following as a callback plugin at callback_plugins/, relative to our playbooks:

# -*- encoding:utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import sys

import ansible

    # Version 2.0+
    from ansible.plugins.callback import CallbackBase
except ImportError:
    CallbackBase = object

def print_red_bold(text):
    print("\x1b[31;1m" + text + "\x1b[0m")

class CallbackModule(CallbackBase):
    def __init__(self):
        # Can't use `on_X` because this isn't forwards compatible with Ansible 2.0+
        required_version = ""  # Keep synchronized with group_vars/all/main.yml
        if ansible.__version__ != required_version:
                "YPlan restriction: only Ansible {version} is supported. "
                "You should install it with:\n\n"
                "\tpip install ansible=={version}\n".format(version=required_version)

If Ansible starts with the wrong version compared to the plugin, it dies with a pretty red message telling the user to install the latest version. We also made it clear that this is an organization-wide restriction with the phrasing “YPlan restriction” - when we first rolled it out, developers didn’t know where the message came from and started googling it!

One slight annoyance is that we already have the required version in a variable in group_vars/all/main.yml, but we had to duplicate it here. It’s probably possible to read the variable using the Ansible machinery, but since that interface might also change between versions we decided it would be simpler to just duplicate the variable in our plugin code and add a note to keep it synchronized.

In conclusion, this version lock has helped us out immensely with upgrading Ansible. Before moving to a new version we can test all the playbooks and find what needs updating, rather than have it happen randomly on developer machines or CI. I hope it helps you too!

Further Reading

Make your development more pleasant with Boost Your Django DX.

Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts: