Ansible tips’n’tricks: configuring the dynamic inventory plugin for Oracle Cloud Infrastructure

As yet another preparation for a talk I’m about to give later this year I ran into a documentation bug concerning the use of the Ansible Dynamic Inventory plugin for Oracle Cloud Infrastructure. It has since been fixed, but it prompted me to write the entire procedure down anyway as I couldn’t really make sense of it at first. That’s probably more of a reflection on me than on the docs, which I found generally to be quite good.

My environment

In case you find this post via a search engine, this is the kit I was using when I put this post together:

  • A Debian 10 (Buster) VM where all the fun takes place
  • Ubuntu 20.04 LTS hosting the VM
  • VirtualBox 6.1.16

If you use an Oracle Linux VM in OCI, or an Oracle Linux VM the steps to configure the inventory plugin are different as most software can be installed using yum. This post shows the manual steps. If I find the time I’ll put another post together describing how to install the Ansible inventory plugin on Oracle Linux.

Ansible

Debian 10 ships with Ansible 2.7 so I decided to try installing Ansible 2.10 via pip3. I seem to have read somewhere that a modern Ansible distribution is a prerequisite for the Dynamic Inventory Plugin, although I can’t find the source now. Nevertheless, there are interesting new features in 2.10 worth trying, so I went with it in the end.

Installing Ansible 2.10 is via pip is quite simple, and it resulted in the desired version. The only downside was that I am now responsible for keeping Ansible current. I went with the distribution’s python3 interpreter and pip3. The same goes for virtualenv you will see referenced later.

I followed the documented steps and installed Ansible into my user account:

vagrant@ocidev:~$ pip3 install ansible --user
Collecting ansible
Downloading 

...

Installing collected packages: PyYAML, MarkupSafe, jinja2, pyparsing, 
packaging, ansible-base, ansible
 Successfully installed MarkupSafe-1.1.1 PyYAML-5.3.1 ansible-2.10.3 
ansible-base-2.10.3 jinja2-2.11.2 packaging-20.4 pyparsing-2.4.7

Note the --user flag as part of the command, it is very important! After having added /home/vagrant/.local/bin/ to the PATH, I had access to my favourite automation toolkit.

vagrant@ocidev:~$ ansible --version
ansible 2.10.3
   config file = None
   configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
   ansible python module location = /home/vagrant/.local/lib/python3.7/site-packages/ansible
   executable location = /home/vagrant/.local/bin/ansible
   python version = 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0]

On to the next step, the installation of the OCI Python SDK.

OCI Python SDK

The installation is straight forward as well. To stay in line with the vast majority of bloggers describing the installation I decided to create a virtualenv environment.

vagrant@ocidev:~$ virtualenv python_oci_sdk
Running virtualenv with interpreter /usr/bin/python2
New python executable in /home/vagrant/python_oci_sdk/bin/python2
Also creating executable in /home/vagrant/python_oci_sdk/bin/python
Installing setuptools, pkg_resources, pip, wheel…done.
vagrant@ocidev:~$

Once the virtualenv exists it’s time to enable it and install the OCI SDK:

vagrant@ocidev:~$ . python_oci_sdk/bin/activate

(python_oci_sdk) vagrant@ocidev:~$ pip3 install oci
Collecting oci 
...
Successfully installed cffi-1.14.3 configparser-4.0.2 cryptography-2.8 
oci-2.23.4 pyOpenSSL-19.1.0 pycparser-2.20 python-dateutil-2.8.1 
pytz-2020.4

After the installation completed, you need to ensure you have a working configuration as explained in the official documentation set. There are many blog posts out there describing the process, and I won’t repeat it here. I found a post of a colleague of mine, Christoph, to be really good. And there are many more other good posts out there, too!

Ansible Galaxy

Before I can make use of the Dynamic Inventory Plugin I need to install the oracle.oci collection from Ansible galaxy:

(python_oci_sdk) vagrant@ocidev:~$ ansible-galaxy collection install oracle.oci
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'oracle.oci:2.9.0' to '/home/vagrant/.ansible/collections/ansible_collections/oracle/oci'
Downloading https://galaxy.ansible.com/download/oracle-oci-2.9.0.tar.gz to /home/vagrant/.ansible/tmp/ansible-local-1723ne90h04l/tmprgpltva8
oracle.oci (2.9.0) was installed successfully
(python_oci_sdk) vagrant@ocidev:~$

This concludes the preparation.

Configuring the Ansible Dynamic Environment Plugin

With all the software in place it’s time to configure the Dynamic Inventory plugin. As I said before I was a little confused after having read the documentation.

Configuration files

You will need to create 2 configuration files:

  • The Dynamic Inventory Plugin Configuration File (for lack of a better description)
  • The well-known ansible configuration file (ansible.cfg)

Dynamic Inventory Plugin Configuration File

First of all, you need to create a YAML file as inventory source. According to the docs, in its simplest form, all it requires is this line (spoiler: we will put a a lot more into the file):

plugin: oracle.oci.oci

However, it’s not quite as simple. The file name you save this as has to follow a specific form, or else you get an error:

(python_oci_sdk) vagrant@ocidev:~/ansible$ ansible-inventory --graph -vvv -i oci.yml
ansible-inventory 2.10.3
config file = /home/vagrant/ansible/ansible.cfg

[...]

Using /home/vagrant/ansible/ansible.cfg as config file
ansible_collections.oracle.oci.plugins.inventory.oci 
declined parsing /home/vagrant/ansible/oci.yml as it did not pass its verify_file() method

Name your file in the form someIdentifier.oci.yml, only changing someIdentifier, leave the rest as it is.

There are many more things to put into the config file as you will see in a minute, we aren’t done yet :)

Ansible.cfg

For the above YAML file to have any effect you need a local ansible.cfg file white-listing the use of the plugin. My local ansible.cfg has the following contents:

(python_oci_sdk) vagrant@ocidev:~/ansible$ cat ansible.cfg 
[defaults]
stdout_callback = debug

[inventory]
enable_plugins = oracle.oci.oci

Actually only the part under the inventory tag is important. I do like to have some more human readable output which is why I change the stdout_callback.

Caveats

The use of the Dynamic Inventory plugin is a great time saver, but comes with a certain risk. If you aren’t careful, you can end up running playbooks against far too many hosts. Clever Identity and Access Management (IAM) is a must to prevent accidents. And don’t ever use hosts: all in your playbooks! Principle of least privilege is key.

I always follow a principle of least privilege, and created a new IAM user for this blog post. The user merely has the privileges to manage resources in his own compartment, everything else is off limits. In addition I’m filtering on the compartment and the project’s tags.

Limiting scope to a specific compartment

Now I need access to the docs: the Oracle inventory plugin is documented in the web, but thankfully there is also a version for consumption at the console window:

(python_oci_sdk) vagrant@ocidev:~/ansible$ ansible-doc -t inventory oracle.oci.oci | head -n 10
> ORACLE.OCI.OCI    (/home/vagrant/.ansible/collections/ansible_collections/oracle/oci/plugins/inventory/oci.py)

        Get inventory hosts from oci. Uses a .oci.yaml (or
        .oci.yml) YAML configuration file.

OPTIONS (= is mandatory):

- cache
        Toggle to enable/disable the caching of the inventory's source
        data, requires a cache plugin setup to work.

...

Using the docs I came up with the following inventory source:

(python_oci_sdk) vagrant@ocidev:~/ansible$ cat ougdemo-compartment.oci.yml 
plugin: oracle.oci.oci

hostname_format: "fqdn"

filters:
 defined_tags: { "project": { "name": "simple-app" } } 

regions:
- eu-frankfurt-1

compartments:
- compartment_ocid: "ocid1.compartment.oc1..aa...a"
  fetch_hosts_from_subcompartments: true
(python_oci_sdk) vagrant@ocidev:~/ansible$ 

With the inventory in place as shown, it’s possible to graph the resources:

(python_oci_sdk) vagrant@ocidev:~/ansible$ ansible-inventory -i ougdemo-compartment.oci.yml --graph
...
@all:
  |--@IHsr_EU-FRANKFURT-1-AD-2:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@IHsr_EU-FRANKFURT-1-AD-3:
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |--@Oracle-Tags#CreatedBy=ougdemouser:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@Oracle-Tags#CreatedOn=2020-11-08T10_35_08.490Z:
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@Oracle-Tags#CreatedOn=2020-11-08T10_35_08.765Z:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |--@Oracle-Tags#CreatedOn=2020-11-08T10_35_08.896Z:
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |--@all_hosts:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@ougdemo-department:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@project#name=simple-app:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@region_eu-frankfurt-1:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@tag_role=appserver:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |--@tag_role=bastionhost:
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@ungrouped:
(python_oci_sdk) vagrant@ocidev:~/ansible$  

Note how the use of tags (both free form as well as defined) allows me to group resources easily. Tagging is really important in the cloud ;)

Summary

The Ansible Dynamic Inventory Plugin is a great time saver, allowing me to create and configure Infrastructure as Code without having to manually update an Ansible inventory file each time a Terraform script completed. Using tags and appropriate Identity and Access Management (IAM)/security/access control policies in conjunction with a suitable filter configuration in the config file it is possible to manage cloud resources with very little manual effort.

A failure to adhering to industry best known methods comes at the risk of running Ansible playbooks against the wrong hosts, something that has to be ruled out by means of a secure configuration.