SoliDeoGloria.tech

Technology for the Glory of God

Ansible, Python, PEP668, and Virtual Environments

  • 3 minutes

Have you ever had one of those days where your troubleshooting resembles doing the foxtrot? Forward. Sideways. Backwards. Round in Circles. I had one of those last week.

My current customer is using Ansible for orchestration. As part of proving out the capabilities, and working through the best way to make it all work, I needed to set up my local environment.

Running macOS typically makes setup a breeze. Thanks to the wonderful Homebrew Pacakge Manager. Tools such as Ansible and Ansible Lint are just a brew install away.

But this is where the problems start. The brew recipies for Ansible and Ansible Lint install into Python virtual environments. This is a Good Thing™, but can then lead to confusion over which environment is being used. Sadly, I don’t have the exact error messages any more which led me to this conclusion.

The fix is relatively simple. Stop using Homebrew for installing Ansible and instead use Python’s pip packaging tool. But even better than pip is pipx, which can be installed happily with Homebrew.

Installing Ansible is then as simple as creating a new virtual environment and adding the tools.

# Install Ansible to a new virtual environment
pipx install --include-deps ansible

# Add ansible-lint to the virtual environment
pipx inject --include-apps ansible ansible-lint

Simples, no? Now we can run Ansible and everything will be unicorns and rainbows. Yeah, nah. It’s not quite that simple.

We are using the Azure Collection to perform tasks against Azure Resource Manager. Ansible Core does come with this collection pre-installed which is convienient. However, the Azure Collection has a list of prerequisite packages that need to be installed. Again, this should be a simple process of running pip install -r requirements.txt and Python will do its thing.

Again, it’s not quite that easy. Homebrew has subscribed to PEP 668 which marks the base installation as “externally managed” which prevents tools like pip from installing packages into the base. This is still a Good Thing™, and is easily overcome again with pipx

pipx runpip ansible install -r azure/azcollection/requirements.txt

Now we should be able to run our Azure commands from our Ansible playbooks locally, and everthing will finally be caffeinated rainbows and unicorn tears.

If only it was that simple.

To help reduce issues, Ansible will use the default installation of tools to do its work. This makes sense, especially when running against a remote target (which is what Ansible excels at). But when we want to run our commands against our local target, this isn’t quite so helpful. Remember above that, due to Homebrew’s subscription to PEP 668 we can’t install packages into the system Python instance. So when we run our playbook, we end up with an error like this

Traceback (most recent call last):
  File "/var/folders/nc/0t5_xbfx7m12094qw84bshh40000gn/T/ansible_azure.azcollection.azure_rm_virtualwan_info_payload_ty5y0qzx/ansible_azure.azcollection.azure_rm_virtualwan_info_payload.zip/ansible_collections/azure/azcollection/plugins/module_utils/azure_rm_common.py", line 232, in <module>
    from packaging.version import Version
ModuleNotFoundError: No module named 'packaging.version'

The system Python cannot find the Version package which was installed into our virtual environment as a prerequesite. So how do we make Ansible use the python it was called with, and not the system python? Thankfully this is simple to do, but finding the information was not easy. My thanks go to Will Thames in a post from 2018 where, while writing on connection: local vs delegate_to: localhost, provides the solution. It is simply a case of setting the ansible_python_interpreter variable for a playbook and telling it to use the same python for the steps as it used to start the playbook. While this could be done on a per-playbook basis, it’s easier to set it once in the group_vars/all.yaml file so it applies to all playbooks and hosts:

ansible_python_interpreter: '{{ ansible_playbook_python }}'

And now we can run our Ansible playbook and interact with Azure RM.