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.
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
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)
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):
--extra-vars
(always wins)Now we will refactor the previously created inventory file to take advantage of this.
Make sure the following content is in the file ~/ansible_2/inventory/hosts.ini
:
1 2 | [nginx_webservers]
10.100.0.2
|
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
|
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.
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.
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.
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:
group_vars/all/vars.yml
that is located in the same hierarchy as the inventory
directoryinventory/[prod, qa, or uat]/group_vars/all/vars.yml
. Useful for defining endpoints attached to a specific environment, for example.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
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!
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.
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.
ansible-vault decrypt path/to/file
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:
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.
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
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:
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)
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.
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.
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!