Ansible

2 - Handling the inventory

Introduction

This section will cover how to work with static inventory files. Dynamic inventory files are out of this scope.

When working with inventory files, regardless of the type, there are two main abstractions to consider: hosts management and variables management.

Hosts management

Inside the inventory file there can be groups, groups of groups, and variables. Since the variable precedence in Ansible is quite complex, we will handle the variables separately. There are many formats for a valid inventory file (depending on the inventory plugin being used). In this case we will focus in the INI-like format, since I believe it’s the easiest to work with when dealing with static inventories.

Consider the following content in the inventory file (taken from here):

[atlanta]
host1
host2

[raleigh]
host2
host3

[southeast:children]
atlanta
raleigh

In any inventory file there are two default groups: all (includes all of the hosts), and none (includes none of the hosts).

When defining a playbook we can target the right hostgroup in the hosts section. Also, we can target the hostgroup all, and then limit to the group of interest we want when running our playbook, like this:

(.venv) $ ansible-playbook -i inventory/hosts any_playbook_with_all_hosts.yml --limit atlanta

Note

Don’t actually run that. It’s just to illustrate how the command would be structured.

In our case let’s create an empty inventory file in the following path:

(.venv) $ mkdir -p ~/ansible_2/inventory
(.venv) $ touch ~/ansible_2/inventory/hosts

Let’s include the following content (yes, same file from part 1):

1
2
3
4
5
6
7
8
[nginx_webservers]
10.100.0.2

[all:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

Also create the role we’ll use in this section (again, same role from part 1):

(.venv) $ mkdir -p ~/ansible_2/roles/webservers-nginx/tasks/
(.venv) $ touch ~/ansible_2/roles/webservers-nginx/tasks/main.yml

Content is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
---

- name: install the nginx reverse proxy
  apt:
    name: nginx
    update_cache: yes

- name: enable nginx service
  systemd:
    name: nginx
    state: started
    enabled: yes

Variables management

This section covers the use of variables by our inventory (and playbooks). The areas covered are: variables precedence and secrets via the usage of ansible-vault files (encrypted files to conveniently place secrets)

Variable precedence

In Ansible there is a quite extensive variable precedence list. I have found that the easiest ones to work with are as shown below (the higher the number, the higher the precedence):

  1. inventory/group_vars/all/
  2. inventory/group_vars/group1/
  3. inventory/host_vars/host1
  4. roles/role1/defaults/
  5. roles/role1/vars/
  6. group_vars/all/
  7. --extra-vars (always wins)

Now we will refactor the previously created inventory file to take advantage of this.

Cleaning-up the current inventory file

  • Make sure the following content is in the file ~/ansible_2/inventory/hosts.ini:

    1
    2
    [nginx_webservers]
    10.100.0.2
    

Create files to place variables

  • Create a global group_vars file:

    (.venv) ansible_2 $ mkdir -p group_vars/all
    (.venv) ansible_2 $ touch group_vars/all/vars.yml
    
  • Make sure the content of group_vars/all/vars.yml is:

    1
    2
    3
    4
    ---
    
    ansible_connection: ssh
    ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
    
  • Create the inventory-level group_vars file:

    (.venv) ansible_2 $ mkdir -p inventory/group_vars/all
    (.venv) ansible_2 $ touch inventory/group_vars/all/vars.yml
    
  • Make sure the content of inventory/group_vars/all/vars.yml is:

    1
    2
    3
    4
    5
    ---
    
    ansible_ssh_port: 2200
    ansible_connection: ssh
    ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
    
  • Create the inventory group_vars/GROUP_NAME vars file:

    (.venv) ansible_2 $ mkdir -p inventory/group_vars/nginx_webservers
    (.venv) ansible_2 $ touch inventory/group_vars/nginx_webservers/vars.yml
    

    Note

    The name of this directory must match a hostgroup inside the inventory file

  • Make sure the content of inventory/group_vars/nginx_webservers/vars.yml is:

    1
    2
    3
    4
    5
    6
    7
    ---
    
    ansible_ssh_port: 22
    ansible_user: vagrant
    ansible_ssh_private_key_file: .vagrant/machines/default/virtualbox/private_key
    
    simple_auth_username: admin
    

Create the vagrant box

  • Initialize vagrant with an ubuntu image:

    (.venv) ~/ansible_2 $ vagrant init bento/ubuntu-16.04 --minimal
    

    Note

    This will create the file ‘Vagrantfile’.

  • Open the auto-generated Vagrantfile, and make sure the content looks like this:

    1
    2
    3
    4
    5
    6
    Vagrant.configure("2") do |config|
      config.ssh.port = 2200
      config.vm.box = "bento/ubuntu-16.04"
      config.vm.network "private_network", ip: "10.100.0.2"
      config.vm.hostname = "tutorial-2"
    end
    
  • Start the virtual machine

    (.venv) ~/ansible_2 $ vagrant up
    

    Note

    Time to get a cup of tea while this is done.

Run the playbook

Similar to part 1, run:

(.venv) ~/ansible_2 $ ansible-playbook -i inventory/hosts.ini webservers.yml

The output should be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

PLAY [nginx_webservers] ************************************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************************************
ok: [127.0.0.1]

TASK [webservers-nginx : install the nginx reverse proxy] **************************************************************************************************************
changed: [127.0.0.1]

TASK [webservers-nginx : enable nginx service] *************************************************************************************************************************
ok: [127.0.0.1]

PLAY RECAP *************************************************************************************************************************************************************
127.0.0.1                  : ok=3    changed=1    unreachable=0    failed=0   

In conclusion, same desired result as in part 1, but now using a nice layout for the inventory.

Multiple inventories

It’s a common design pattern to take into consideration order of precedence of the variables in order to create a directory structure that can support several environments easily. The final layout of the inventory will depend on how you handle your customers, or how many products you deploy on each environment. Let’s evaluate two common layouts.

Inventory layout 1: multiple environments

Consider the following inventory layout:

inventory
├── prod
│   ├── group_vars
│   │   ├── all
│   │   │   └── vars.yml
│   │   └── nginx_webservers
│   │       └── vars.yml
│   └── hosts
├── qa
│   ├── group_vars
│   │   ├── all
│   │   │   └── vars.yml
│   │   └── nginx_webservers
│   │       └── vars.yml
│   └── hosts
└── uat
    ├── group_vars
    │   ├── all
    │   │   └── vars.yml
    │   └── nginx_webservers
    │       └── vars.yml
    └── hosts
group_vars
└── all
    └── vars.yml

In this layout we see how different environments can be defined by just turning our single inventory file into a folder with many small inventories. Things to notice are:

  • Variables that apply to all environments can be specified in the group_vars/all/vars.yml that is located in the same hierarchy as the inventory directory
  • Variables that apply for all groups within an environment, can be specified in the files inventory/[prod, qa, or uat]/group_vars/all/vars.yml. Useful for defining endpoints attached to a specific environment, for example.
  • Variables that apply to a certain hostgroup, in the files inventory/[prod, qa, or uat]/group_vars/[hostgroup]/vars.yml. An extension of this approach is to also add the hostvars in parallel to the group_vars, but since that’s a bit tedious, you might want to automate that task.

Another variation of this layout is the following:

inventory
├── group_vars
│   ├── all
│   │   └── vars.yml
│   ├── nginx_webservers
│   │   └── vars.yml
│   ├── prod
│   │   └── vars.yml
│   ├── qa
│   │   └── vars.yml
│   └── uat
│       └── vars.yml
├── hosts-prod
├── hosts-qa
└── hosts-uat
group_vars
└── all
    └── vars.yml

Inventory layout 2: multiple environments, multiple customers or deployments

Consider the following inventory layout:

inventory/
├── customer1
│   ├── group_vars
│   │   ├── all
│   │   │   └── vars.yml
│   │   ├── nginx_webservers
│   │   │   └── vars.yml
│   │   ├── prod
│   │   │   └── vars.yml
│   │   ├── qa
│   │   │   └── vars.yml
│   │   └── uat
│   │       └── vars.yml
│   ├── hosts-prod
│   ├── hosts-qa
│   └── hosts-uat
├── customer2
│   ├── group_vars
│   │   ├── all
│   │   │   └── vars.yml
│   │   ├── nginx_webservers
│   │   │   └── vars.yml
│   │   ├── prod
│   │   │   └── vars.yml
│   │   ├── qa
│   │   │   └── vars.yml
│   │   └── uat
│   │       └── vars.yml
│   ├── hosts-prod
│   ├── hosts-qa
│   └── hosts-uat
└── customer3
    ├── group_vars
    │   ├── all
    │   │   └── vars.yml
    │   ├── nginx_webservers
    │   │   └── vars.yml
    │   ├── prod
    │   │   └── vars.yml
    │   ├── qa
    │   │   └── vars.yml
    │   └── uat
    │       └── vars.yml
    ├── hosts-prod
    ├── hosts-qa
    └── hosts-uat
group_vars
└── all
    └── vars.yml

It is pretty much the same as the last example of the multiple environments section, just adding another folder. At the end of the day the decision of the type of inventory layout will depend on the actual problem you’re trying to solve. Just keep in mind that this is very flexible, and that the variable precedence levels can come very handy.

For our purposes we’ll keep using the simple layout: a single environment:

inventory
├── group_vars
│   ├── all
│   │   └── vars.yml
│   └── nginx_webservers
│       └── vars.yml
└── hosts
group_vars
└── all
    └── vars.yml

Let’s talk about secrets now!

Vault and secrets

Ansible includes a tool that enables the encryption/decryption of files, making it very convenient to work with secrets. This tool is called Ansible Vault.

Normally makes more sense to just encrypt the files that contain sensitive data. However, with Ansible Vault you can encrypt any file.

The Ansible Vault has many features. In this tutorial we’ll just focus on encryption and decryption of files as normally this is enough to get started.

Encrypt a file

ansible-vault encrypt path/to/file

Note

By default if there are no saved passwords, the tool will prompt for a new password to be entered.

Decrypt file

ansible-vault decrypt path/to/file

In our case we will modify the current setup in the following way:
  • We will enable simple HTTP authentication in our nginx server
  • We will create a vault file to create a variable that will hold the password
  • We will encrypt that vault file so it’s safely stored in the repository

Including vault into our environment

  • Make sure the content of the file ~/ansible_2/roles/webservers-nginx/tasks/main.yml is:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    ---
    
    - name: check vars
      assert:
        that:
          - simple_auth_username != ''
          - simple_auth_password != ''
    
    - name: install the nginx reverse proxy
      apt:
        name: nginx
        update_cache: yes
    
    - name: enable nginx service
      systemd:
        name: nginx
        state: started
        enabled: yes
    
    - name: install python pip
      apt:
        name: python-pip
        update_cache: true
    
    - name: install python library 'passlib'
      pip:
        name: passlib
    
    - name: create htpasswd for HTTP basic authentication
      htpasswd:
        path: /etc/nginx/.htpasswd
        name: "{{ simple_auth_username }}"
        password: "{{ simple_auth_password }}"
        crypt_scheme: md5_crypt
    
    - name: Add basic HTTP authentication configuration on nginx
      blockinfile:
        path: /etc/nginx/sites-available/default
        marker: "        #### {mark} ANSIBLE MANAGED BLOCK #####"
        insertafter: '^\s+server_name _;'
        block: |
          #
                  auth_basic           "Administrator’s Area";
                  auth_basic_user_file /etc/nginx/.htpasswd;
    
    - name: reload nginx service
      systemd:
        name: nginx
        state: reloaded
    

    Note

    The new tasks are highlighted

    Things to notice here are:

    • The check of variables at the beginning will provide a fail-fast mechanism (won’t run further tasks if the vars are not provided)
    • Variables are normally rendered by enveloping them between double quotes (lines 33 and 34), and double curly braces (this is a convention from the Python templating engine Jinja)
    • List of modules used: assert, apt, systemd, pip, htpasswd, blockinfile.
  • Make sure the content of the file ~/ansible_2/roles/webservers-nginx/defaults/main.yml is:

    1
    2
    3
    4
    ---
    
    simple_auth_username: ''
    simple_auth_password: ''
    

    Note

    We give the blank value of the variables, so the role fails fast if the vars are not provided

  • Make sure the content of the file ~/ansible_2/inventory/group_vars/nginx_webservers/vars.yml is:

    1
    2
    3
    4
    5
    6
    7
    ---
    
    ansible_ssh_port: 22
    ansible_user: vagrant
    ansible_ssh_private_key_file: .vagrant/machines/default/virtualbox/private_key
    
    simple_auth_username: admin
    
  • Create the vault file for the host group nginx_webservers:

    (.venv) ansible_2 $ touch inventory/group_vars/nginx_webservers/vault.yml
    
  • Make sure the content of the file ~/ansible_2/inventory/group_vars/nginx_webservers/vault.yml is:

    1
    2
    3
    ---
    
    simple_auth_password: admin
    
  • Encrypt the vault file:

    (.venv) ansible_2 $ ansible-vault encrypt inventory/group_vars/nginx_webservers/vault.yml
    

    Note

    You should see ‘Encryption successful’ as a result of the operation. Remember this password!

  • Verify the content of the now encrypted file. You should see a similar output to the following:

    $ANSIBLE_VAULT;1.1;AES256
    31386330323961363237313632373938656130306531633263393635373338326564373233643966
    3262626132333530386330393962646639666238326334360a333163663537343336373263643561
    62333366633065343439363539613031393732323031336539636266383132373738613864653334
    6162366130393462360a633362356637373030363737313461356138343736336164393939313363
    61306435633961343435363831346663343936306532393035623831393733643537363161383833
    3734313034336431383463363834636362633032323936323336
    

After doing this we can run again the playbook as usual.

Run playbook using the usual command

Command:

(.venv) ansible_2 $ ansible-playbook -i inventory/hosts.ini webservers.yml

The output should be an error similar to the following:

PLAY [all] **************************************************************************************
ERROR! Attempting to decrypt but no vault secrets found

Run playbook asking for the vault password

Command:

(.venv) ansible_2 $ ansible-playbook -i inventory/hosts.ini webservers.yml --ask-vault-pass

Now Ansible will prompt for the vault password. After providing the password, now try to access the local nginx webserver http://10.100.0.2. It should prompt using the basic HTTP authentication dialog box (credentials are admin:admin), similar to this one:

Nginx basic HTTP auth

Nginx basic HTTP auth

Verify htpasswd file

If you want to verify how this .htpasswd file is looking in the server, you can do so by:

  • Running a simple SSH command as follows:

    (.venv) ansible_2 $ ssh -i .vagrant/machines/default/virtualbox/private_key vagrant@10.100.0.2 "cat /etc/nginx/.htpasswd"
    

    The file should have an output similar to:

    admin:$1$SIBL4POk$MlscIbwWALKAWY.TgbC3a.
    
  • Running an Ansible ad-hoc command to check the content of the file:

    (.venv) ansible_2 $ ansible -i inventory/hosts.ini -a "cat /etc/nginx/.htpasswd" all --ask-vault-pass
    

    The output should be similar to:

    Vault password:
    127.0.0.1 | CHANGED | rc=0 >>
    admin:$1$SIBL4POk$MlscIbwWALKAWY.TgbC3a.
    

    Note

    Notice how easy is to send ad-hoc remote commands using Ansible (taking advantage of the SSH configuration, we just need to focus on the remote action)

Run using a file with the vault password in it

Create a file and store the password in it:

(.venv) ansible_2 $ echo "vaultpassword" > vaultPasswordFile

Note

I’m assuming the password is ‘vaultpassword’

Run the playbook specifying the file:

(.venv) ansible_2 $ ansible-playbook -i inventory/hosts.ini webservers.yml --vault-password-file ./vaultPasswordFile

The playbook should run successfully.

Run using the vault password file environment variable

Export the location of the password file as an environment variable:

(.venv) ansible_2 $ export ANSIBLE_VAULT_PASSWORD_FILE=$( pwd )/vaultPasswordFile

Note

The value of ANSIBLE_VAULT_PASSWORD_FILE should be the absolute path to the file (hence we’re using ‘pwd’ to obtain it)

Run the playbook without specifying the vault password file:

(.venv) ansible_2 $ ansible-playbook -i inventory/hosts.ini webservers.yml

The playbook should run successfully.

Bonus: specify the ANSIBLE_VAULT_PASSWORD_FILE var without exporting it

Just run:

(.venv) ansible_2 $ ANSIBLE_VAULT_PASSWORD_FILE=$( pwd )/vaultPasswordFile ansible-playbook -i inventory/hosts.ini webservers.yml

Note

In modern shells (such as bash), you can pass any environment variable to a desired command


Until here you have the knowledge to spin up Ansible, and configure it according to your requirements. It is true that the role we just created was very simple, however there is extended documentation regarding roles, specially using Ansible Galaxy, which won’t be cover in this tutorial.

Let’s get to know more about Ansible in the next modules!