First steps with Ansible

Łukasz Zosiak

Introduction

Ansible is an open source powerful automation tool. It is used for configuration management, application deployment, orchestration and task automation. Created for multi-device management since day one, Ansible deals with all system and its dependencies, rather than just managing one component at a time. Ansible uses agentless architecture, as opposed to other configuration management tools (e.g., Puppet, Chef). Ansible through SSH connects to nodes and transfers small programs called ‘Ansible modules’ to them. Then these modules are executed and deleted when accomplished.

As mentioned before, Ansible works against multiple nodes at the same time. Information about managed systems is stored in Ansible inventory. By default, inventory is stored in /etc/ansible/hosts, where hosts can be grouped in INI-style sections.

An example of Ansible inventory file, let’s call it static_inv:

[production]
prod1 ansible_port=1111
prod2 ansible_port=2222
 
[testing]
test1
test2
test3
test4

[testing:vars]
username=admin
password=password
proxy=proxy.example.com

Here are two groups defined – production and testing. Multiple nodes are present underneath both groups. Variable ansible_port is set for every host from production group. Finally, set of testing group variables is defined in the testing:vars section.

You can verify your inventory using simple command:

$ ansible all -i static_inv -m debug -a "var=hostvars[inventory_hostname]"

It should print all nodes with its variables.

Most infrastructure can be managed by such custom files. However, there are situations where more sophisticated approach is needed. For instance you are using external data source to store information about your infrastructure like database. You do not want to maintain that information both in Ansible and in database at the same time. Other applications are also using information from database, so you cannot get rid of it. Another example are complex systems, where managing large files might be impractical and error-prone. What to do in such cases?

Dynamic inventory

Ansible provides convenient way to deal with complex inventory structures – dynamic inventories. Any executable file is accepted by Ansible as inventory file, as long as it passes output to Ansible in JSON format. To achieve the same result as with inventory file from introduction Ansible expects below structure from dynamic inventory file:

{
    "production" : {
        "hosts" : [
            "prod1",
            "prod2"
        ]
    },
    "testing" : {
        "hosts" : [
            "test1",
            "test2",
            "test3",
            "test4"
        ],
        "vars" : {
            "username" : "admin",
            "password" : "password",
            "proxy" : " proxy.example.com "
        }
    },
    "_meta" : {
        "hostvars" : {
            "prod1" : {
                "ansible_port" : "1111"
            },
            "prod2" : {
                "ansible_port" : "2222"
            }
        }
    }
}

Ansible calls dynamic inventory script with the argument --list. Returned JSON should have a dictionary of groups containing hosts list and vars dictionary, with group variables. Passing host variables can be implemented in two ways. First one is to define _meta dictionary when inventory invoked with --list option. That Second way is to handle --host option in dynamic inventory. In such a case JSON with variables corresponding to passed host should be returned. Both solutions will be presented in that article.

Creating Ansible dynamic inventory in Python

To implement dynamic inventory in Python let’s create dynamic_inv.py file:

#!/usr/bin/python

import os
import sys
import argparse
import json


parser = argparse.ArgumentParser()
parser.add_argument('--list', action='store_true', help='Creates dynamic inventory of all hosts')
parser.add_argument('--host', action='store', help='Not supported, host variables returned in _meta dictionary')
cli_args = parser.parse_args()


def get_default_inventory():
    return {"_meta": {"hostvars": {}}}


def get_inventory():
    return {
        'production': {
            'hosts': ['prod1', 'prod2'],
        },
        'testing': {
            'hosts':['test1', 'test2', 'test3', 'test4'],
            'vars': {
                'username': 'admin',
                'password': 'password',
                'proxy': 'proxy.example.com'
            }
        },
        '_meta': {
            'hostvars': {
                'prod1': {
                    'ansible_port': 1111
                },
                'prod2': {
                    'ansible_port': 2222
                }
            }
        }
    }


def main():
    if cli_args.list:
        inventory = get_inventory()
    elif cli_args.host:
        # Not supported, since _meta info returned with `--list`.
        inventory = get_default_inventory()
    else:
        inventory = get_default_inventory()
    inventory_dump = json.dumps(inventory,  indent=4)
    print(inventory_dump)


if __name__ == '__main__':
    main()

Now take a look at a script with the same functionality, but with handling option --host and without _meta dictionary in returned JSON:

#!/usr/bin/python

import os
import sys
import argparse
import json

parser = argparse.ArgumentParser()
parser.add_argument('--list', action='store_true', help='Creates dynamic inventory of all hosts')
parser.add_argument('--host', action='store', help='Returns host variables')
cli_args = parser.parse_args()


def get_inventory():
    return {
        'production': {
            'hosts': ['prod1', 'prod2'],
        },
        'testing': {
            'hosts': ['test1', 'test2', 'test3', 'test4'],
            'vars': {
                'username': 'admin',
                'password': 'password',
                'proxy': 'proxy.example.com'
            }
        },
    }


def get_hostvars(host):
    hostvars = {
        'prod1': {
            'ansible_port': 1111
        },
        'prod2': {
            'ansible_port': 2222
        }
    }

    return hostvars.get(host) or {}


def main():
    if cli_args.list:
        inventory = get_inventory()
    elif cli_args.host:
        inventory = get_hostvars(cli_args.host)
    else:
        inventory = {}
    inventory_dump = json.dumps(inventory, indent=4)
    print(inventory_dump)


if __name__ == '__main__':
    main()

The only change you would have to make to adapt above scripts to real-life problems is changing get_inventory() and get_hostvars() methods for something reflecting your business logic – API calls, pulling data from database or other external data source.

Ansible uses specified python interpreter to handle all python scripts. If there is a need to run inventory file with different interpreter it can be done by specifying path in shebang.

You can check your dynamic inventory similar to static inventory:

$ ansible all -i dynamic_inv.py -m debug -a "var=hostvars[inventory_hostname]"

Mixing static and dynamic inventory files

It is worth mentioning that Ansible can use multiple inventory sources at the same time. It is also possible to combine static and dynamic inventories in the same ansible run. To do so you have to collect all inventory files in one directory and set that directory as ansible inventory. You can use command line option -i or inventory parameter in ansible.cfg. In such directory all executable files will be handled as dynamic inventories. Files with: ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo extensions will be ignored. You can modify that list with parameter inventory_ignore_extensions in ansible.cfg. All other files will be interpreted as static inventories. Subdirectories group_vars and host_vars will be treated in a normal way.

Summary

Ansible helps with complex IT infrastructure management. Dynamic inventories create Ansible host configuration on the fly and allow to pull data from external sources. They are easy to maintain and can be implemented in plenty programming languages, if they only return JSON in correct format.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami