November 19, 2020

Managing infrastructure using Ansible Tower


This post was originally published oon the Steampunk blog.

Some of our previous posts focused on infrastructure management using Ansible. We will take things one step further today and create a rudimentary self-service catalog using Ansible Tower.

For those of you who are not familiar with Ansible Tower, it is a software that allows you to centralize all aspects of infrastructure management. Think of it as a user-friendly wrapper around Ansible with role-based access control. You can get more information about it on the official page.

We will start today’s post with a short analysis of an Ansible playbook that creates a single VM instance on AWS. Then, we will configure Ansible Tower so that we will be able to execute the said playbook. And for the grand finale, we will run the playbook and spend some money on AWS ;)

If you would like to follow along, you will need to have a working Ansible Tower installation. Probably the easiest way to get it up and running is to use the official Vagrant box and activate it with a trial license that you can obtain from the Red Hat.

Sample Ansible playbook

The Ansible playbook that we will be using today looks like this:

---
- hosts: all
  gather_facts: false

  tasks:
    - name: Create a VM
      steampunk.aws.ec2_instance:
        name: "{{ i_name }}"
        type: "{{ i_type }}"
        ami: ami-0e8286b71b81c3cc1
        key_pair: demo_key
        subnet: "{{ i_subnet }}"

Regardless if we are using Ansible Tower or not, we need to do a few things before we can run that Ansible playbook. We must:

  1. make sure Ansible has access to the boto3 Python package,
  2. install the steampunk.aws Ansible Collection,
  3. provide AWS credentials and region to the steampunk.aws.ec2_instance module via environment variables, and
  4. supply values for three variables that we use in the playbook.

We have some work to do, so we better start ;)

Creating a virtual environment

Ansible Tower executes Ansible in a virtual environment to isolate it from the rest of the system. By default, Ansible Tower has one predefined virtual environment containing dependencies for modules that are part of Ansible 2.9. Unfortunately, that environment does not include all dependencies that the steampunk.aws collection needs, so we will create our own.

We cannot create a new virtual environment from the web interface. Instead, we must ssh into the Ansible Tower and run a few commands from the terminal:

$ sudo yum install -y gcc python3-devel
$ sudo mkdir /opt/venvs
$ sudo python3 -m venv /opt/venvs/steampunk_aws
$ sudo /opt/venvs/steampunk_aws/bin/pip install psutil ansible boto3

If nothing went awry, we now have a new virtual environment in /opt/venvs/steampunk_aws with all required Python packages installed. But before we can use it, we must inform Ansible Tower about it. To do that, we must open the Ansible Tower’s web UI, log in, and navigate to the Settings -> System page. Once we add the /opt/venvs path to the custom virtual environments paths field and save the settings, we are all set.

Informing Ansible Tower about our virtual environment location.

Informing Ansible Tower about our virtual environment location.

Adding sample project

Before we can run the Ansible playbook in Ansible Tower, we must fetch it from the external source (Ansible Tower has no facilities for authoring playbooks). Once we navigate to the Projects page and click the green plus button, we will see something like this:

Ansible Tower project configuration.

Ansible Tower project configuration.

Feel free to change the configuration as you see fit, but make sure you set the SCM URL field’s value to https://github.com/xlab-steampunk/ansible-tower-examples.git and select the right Ansible environment. We highlighted those basic configuration settings in the screenshot.

Once we add the project, Ansible Tower will fetch the sources and install required Ansible collections. How does Ansible Tower know what collections our project needs? Because we listed them in the collections/requirements.yml file. Neat, right?

Supplying credentials

Supplying Ansible playbooks with credentials is probably the most complicated step of the whole process. Why? Because we must first define a custom credential type, and then create credentials of that type. But there is nothing a few screenshots will not solve ;)

Creating a custom credential type

We can add a custom credential type by navigating to the Credential Types page and clicking on the green plus button. Filling in the name and description values should not be too problematic, but the input and injector configuration fields are trickier.

Definition of custom credential type.

Definition of custom credential type.

The contents of the input configuration field in our case is the following YAML document:

fields:
  - id: aws_access_key
    label: AWS Access key
    type: string
  - id: aws_secret_key
    label: AWS Secret Key
    type: string
    secret: true
  - id: aws_region
    label: AWS region
    type: string
    choices: [ eu-central-1, eu-north-1 ]
required:
  - aws_access_key
  - aws_secret_key
  - aws_region

That YAML document informs Ansible Tower that our credential type has three required fields and that the aws_secret_key contains sensitive information that we would like to store encrypted.

The injector configuration describes how Ansible Tower should pass the credentials to the Ansible playbook. In our case, we want to use environment variables:

env:
  AWS_ACCESS_KEY: "{{ aws_access_key }}"
  AWS_SECRET_KEY: "{{ aws_secret_key }}"
  AWS_REGION: "{{ aws_region }}"

And with one final click on the save button, we are done.

Adding AWS credentials

Now that we defined our custom credential type, we can add our AWS credentials to Ansible Tower. Once we navigate to the Credentials page and click on the green plus button, Ansible Tower will present us with the following form:

Adding AWS credentials to Ansible Tower.

Adding AWS credentials to Ansible Tower.

Do note that we need to select the credential type before we see the type detail fields.

Once we click on the save button, we are ready for the next configuration step.

Define an inventory

Each ansible-playbook run executes tasks on one or more hosts. Because we will only contact a remote web API in our playbook, all we need is localhost. We can create one by navigating to the Inventories page, clicking on the green plus button, and selecting inventory from the dropdown menu.

Once we name the inventory, we must click on the save button before navigating to the Hosts tab. After we click on the green plus button again, we must fill the host details like this:

Defining localhost inventory.

Defining localhost inventory.

Make sure you copy the following variable definitions to the variables input field:

ansible_connection: local
ansible_python_interpreter: "{{ ansible_playbook_python }}"

If we do not set those variables correctly, Ansible will not find the packages installed in our virtual environment.

Add job template

An Ansible Tower job template is, at its core, a templated ansible-playbook run. Job templates define what playbook Ansible Tower will execute, what credentials and variables are available during the run, verbosity of the output, etc. In our case, the job template should look something like this:

Ansible Tower job template.

Ansible Tower job template.

In this dialog, we collect all of the information that we defined before:

  1. We pick the right inventory source.
  2. We select an appropriate project and a playbook from it.
  3. We inject our AWS credentials.

We highlighted those fields in the screenshot above. But there is still something missing: values for our Ansible playbook variables.

One option would be to define them in the extra variables field, but there is a better way of handling this: surveys. Surveys are dialogs that pop up right before we execute the job template, ask for some input data, and map the data to variables.

In our example, we created a survey with three prompts. In the image below, we show how we defined the instance type prompt. The other two prompts were defined similarly.

Definition of an instance type prompt.

Definition of an instance type prompt.

And with all of this behind us, we are now finally ready to create the AWS instance!

Running the job

Now it is finally time for the fun part ;) If we now navigate to the templates page and click on a rocket next to our job template, Ansible Tower will pop up a survey we defined earlier. Once we enter all required data and confirm our choices, Ansible Tower will run the playbook and display the output:

Ansible Tower job output.

Ansible Tower job output.

Why should I bother with all that?

To summarize real quick, these are the things that we had to perform to create our proof-of-concept self-service catalog. We:

  1. created a dedicated virtual environment containing libraries for talking to the AWS,
  2. imported our playbook into Ansible Tower,
  3. created a custom credential type and instantiated it,
  4. defined our inventory,
  5. added a job template and executed it.

If you live in a terminal and CI/CD systems are your best friends, all this clicking probably made you nauseous. But the result of all this clicking exercise is something almost anyone in the company can use. So at least in our eyes, things like this are worth the effort.

And at the end of the day, we can always write an Ansible playbook to automate Ansible Tower configuration ;) Or you can contact us and we will help you write that playbook and fill the service catalog with some real content.

Cheers!