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
includes/check_ansible_version.yml like so:
We then included this at the top of each playbook with:
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
become syntax in our playbooks with the upgrade to the
- 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/ansible_version_check.py, relative to our playbooks:
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
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
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!