I have got a huge problem with structuring my inventory and how ansible merges groups in inventories.
Imagine an environment where the same three roles of servers stand in separate customer environments (e.g. backend, frontend, database). Those are also roles, which have roles-folders where tasks that should be executed, with default variables are collected. As suggested here: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html
My target is to have playbooks, which apply roles and their tasks, e.g. for upgrading the backend, frontend, and database separately PER CUSTOMER.
All in all, I would think about an inventory structure like that:
- inventories/
- customer1/
- inventory.yml
- customer2/
- inventory.yml
- customer3/
- inventory.yml
And one inventory.yml may contain e.g. the following structure:
all:
children:
customer1:
children:
frontend:
hosts:
fe01.customer1.com:
backend:
hosts:
be01.customer1.com:
database:
hosts:
db01.customer1.com:
The same would be in the other inventory files for customer2 and customer3 just exchanging the customer1.com domain respectively.
Question 1:
Why is it, and is this behavior anyhow changeable, that if I take the whole inventory as one when I use the following command:
shell> ansible customer1:&frontend --list-hosts
That I receive ALL hosts that are in any frontend group, although I have explicitly chosen customer1 AND frontend as selector groups:
fe1.customer1.com
fe1.customer2.com
fe1.customer3.com
Question 2:
How would you structure your inventory to share variables between all customers, but also be able to target each customer solely, by using the -i <inventory_file> parameter, for a really big amount of customers?
e.g.:
shell> ansible -i inventories/customer1 fe --list-hosts
Thanks in advance, everybody have a nice day, and stay healthy!
Tim
Given the inventory
shell> tree inventories
inventories
├── customer1
│ └── hosts.yml
├── customer2
│ └── hosts.yml
└── customer3
└── hosts.yml
3 directories, 3 files
shell> cat inventories/customer1/hosts.yml
all:
children:
customer1:
children:
frontend:
hosts:
fe01.customer1.com:
backend:
hosts:
be01.customer1.com:
database:
hosts:
db01.customer1.com:
shell> cat inventories/customer2/hosts.yml
all:
children:
customer2:
children:
frontend:
hosts:
fe01.customer2.com:
backend:
hosts:
be01.customer2.com:
database:
hosts:
db01.customer2.com:
shell> cat inventories/customer3/hosts.yml
all:
children:
customer3:
children:
frontend:
hosts:
fe01.customer3.com:
backend:
hosts:
be01.customer3.com:
database:
hosts:
db01.customer3.com:
Q: "If I take the whole inventory I receive all hosts in any frontend group. Why is it? Is this behavior anyhow changeable?"
(Note: In the context of this question any references to the group customer* are irrelevant. See the outputs of ansible-inventory and the note below.)
A: It is because Ansible can combine inventory from multiple sources. See Manage Ansible Inventory with dynamic and static hosts and vars. In particular:
Ansible has several inventory plugins. You can combine them as multiple -i options or specify a directory containing multiple inventories.
This behavior is not changeable. Instead, you can configure DEFAULT_HOST_LIST. If you set it to the directory inventories ('the whole inventory as one' in your terminology) you get
shell> ANSIBLE_INVENTORY=inventories ansible-inventory --list --yaml
all:
children:
customer1:
children:
backend:
hosts:
be01.customer1.com: {}
be01.customer2.com: {}
be01.customer3.com: {}
database:
hosts:
db01.customer1.com: {}
db01.customer2.com: {}
db01.customer3.com: {}
frontend:
hosts:
fe01.customer1.com: {}
fe01.customer2.com: {}
fe01.customer3.com: {}
customer2:
children:
backend:
hosts:
be01.customer1.com: {}
be01.customer2.com: {}
be01.customer3.com: {}
database:
hosts:
db01.customer1.com: {}
db01.customer2.com: {}
db01.customer3.com: {}
frontend:
hosts:
fe01.customer1.com: {}
fe01.customer2.com: {}
fe01.customer3.com: {}
customer3:
children:
backend:
hosts:
be01.customer1.com: {}
be01.customer2.com: {}
be01.customer3.com: {}
database:
hosts:
db01.customer1.com: {}
db01.customer2.com: {}
db01.customer3.com: {}
frontend:
hosts:
fe01.customer1.com: {}
fe01.customer2.com: {}
fe01.customer3.com: {}
ungrouped: {}
If you set it to the directory inventories/customer* you get
shell> ANSIBLE_INVENTORY=inventories/customer1 ansible-inventory --list --yaml
all:
children:
customer1:
children:
backend:
hosts:
be01.customer1.com: {}
database:
hosts:
db01.customer1.com: {}
frontend:
hosts:
fe01.customer1.com: {}
ungrouped: {}
This explains the output of the below commands
shell> ANSIBLE_INVENTORY=inventories ansible --list-hosts customer1:\&frontend
hosts (3):
fe01.customer1.com
fe01.customer2.com
fe01.customer3.com
shell> ANSIBLE_INVENTORY=inventories/customer1 ansible --list-hosts customer1:\&frontend
hosts (1):
fe01.customer1.com
Note: The inventory pattern customer1:&frontend is redundant. This pattern means: 'The hosts in the group customer1 that are also members of the group frontend'. You can omit the customer1 group because the group frontend is a subset of customer1. The commands below give the same results
shell> ANSIBLE_INVENTORY=inventories ansible --list-hosts frontend
shell> ANSIBLE_INVENTORY=inventories/customer1 ansible --list-hosts frontend
Related
I have a simple Ansible dynamic inventory for AWS servers that looks like this.
---
plugin: aws_ec2
regions:
- eu-west-2
keyed_groups:
- key: tags.Name
hostnames:
# A list in order of precedence for hostname variables.
- ip-address
compose:
ansible_host: _Applications_
ansible_user: "'ubuntu'"
This works fine, except that I also have another instance that's Redhat.
Which means that when I try to do a simple ping command on all the hosts, it fails as the username ubuntu is only valid on one of the servers.
Is there a way to set group my inventory file so that I can add in the ec2-user username for a specific group maybe based on it's tag or something else.
I could do this easily with my static inventory but I'm not sure how to do this with a dynamic inventory.
I've tried setting my Ansible inventory as an environment variable
export ANSIBLE_INVENTORY=~/Users/inventory
And placed my aws_ec2.yamlin the inventory directory along with a group vars directory containing my different groups with default usernames in each of the different groups
username: ubuntu
username: ec2-user
and then setting my inventory file as such
compose:
ansible_user: "{{ username }}"
But when Ansible tries to connect, it's using an admin username and not what's set in my group vars.
Is there a way to set the different usernames needed to connect to the different type of servers?
Per the example for the constructed plugin, you can use the keyed_group feature to create groups by ansible_distribution:
keyed_groups:
# this creates a group per distro (distro_CentOS, distro_Debian) and assigns the hosts that have matching values to it,
# using the default separator "_"
- prefix: distro
key: ansible_distribution
And then set ansible_user inside groups_vars/distro_Ubuntu.yaml and group_vars/distro_RedHat.yaml.
Also from the documentation, this requires fact caching to operate (because otherwise Ansible doesn't know the value of ansible_distribution at the time it's processing the keyed_groups setting).
I don't have access to AWS at the moment, but here's how I'm testing everything locally. Given an inventory that looks like:
$ tree inventory
inventory/
├── 00-hosts.yaml
└── 10-constructed.yaml
Where inventory/00-hosts.yaml looks like:
all:
hosts:
host0:
ansible_host: localhost
And inventory/10-constructed.yaml looks like:
plugin: constructed
strict: false
groups:
ipmi_hosts: ipmi_host|default(false)
keyed_groups:
- prefix: "distro"
key: ansible_distribution
And ansible.cfg looks like:
[defaults]
inventory = inventory
enable_plugins = constructed
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ./.facts
The first time I run this playbook:
- hosts: all
gather_facts: true
tasks:
- debug:
var: group_names
The output of the debug task is:
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [host0] => {
"group_names": [
"ungrouped"
]
}
But because of the fact gathering and caching performed by the previous playbook run, the second time I run it the output is:
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [host0] => {
"group_names": [
"distro_Fedora"
]
}
Similarly, before the first playbook run ansible-inventory --graph outputs:
#all:
|--#ungrouped:
| |--host0
But after running the playbook once, I get:
#all:
|--#distro_Fedora:
| |--host0
|--#ungrouped:
I've bundled this all into an example repository.
I am looking for a way to build an inventory group that includes all those hosts that haven't been put into another group.
In my case, the groups identify when a particular server should be updated - either on Mondays, Wednesdays, Fridays, or "any day, it doesn't matter". I do not want to explicitly enumerate the hosts, as that is manual work and error-prone.
all:
hosts:
host1[1-100].example.com:
children:
updateMonday:
hosts:
host1.example.com:
host4.example.com:
updateWednesday:
hosts:
host2.example.com:
host5.example.com:
updateFriday:
hosts:
host3.example.com:
host6.example.com:
updateAnyday:
children:
# This expresses what I want but does not work
!updateMonday:
!updateWednesday:
!updateFriday:
I am using additional groups not shown in this example, so I can't simply use the "ungrouped" group to do what I want.
Edit: I do not want to modify the playbooks I use, because I have quite a few of them, and I also need ad-hoc commands to honor the new group.
Create a group with all hosts, e.g. updateAll
all:
hosts:
host[1-100].example.com:
children:
updateAll:
hosts:
host[1-100].example.com:
updateMonday:
hosts:
host1.example.com:
host4.example.com:
...
In the first play create dynamically the group updateAnyday and use it in the second play
- hosts: all
tasks:
- add_host:
name: "{{ item }}"
groups: updateAnyday
loop: "{{ groups.updateAll|
difference(groups.updateMonday)|
difference(groups.updateWednesday)|
difference(groups.updateFriday) }}"
run_once: true
- hosts: updateAnyday
tasks:
...
(Not tested)
Q: "I don't want to modify the playbook."
A: Create a file with the list of the hosts updateAnyday.txt, e.g. use the playbook above. Then run your playbook updateAnyday.yml with limited inventory, e.g.
shell> ansible-playbook updateAnyday.yml --limit #updateAnyday.txt
See Patterns and ansible-playbook flags.
You can create a play and change the host with the group combinations. For instance, all the hosts except the updateMonday, updateWednesday, and updateFriday would be:
hosts: all:!updateMonday:!updateWednesday:!updateFriday
You can also dynamically create the group using add_host or group_by commands.
I am having a strange issue. I have a 'hosts' file with multiple groups in it.
For some reason, when I am using 'ansible-playbook playbook.yml -l GROUPNAME', ansible applies the playbook to all hosts in the 'hosts' file.
hosts file
all:
children:
GROUP1:
children:
webservers:
hosts:
hostname1:
sqlservers:
hosts:
hostname2:
GROUP2:
children:
webservers:
hosts:
hostname3:
hostname4:
GROUP3:
children:
webservers:
hosts:
hostname5:
hostname6:
sqlservers:
hosts:
hostname7:
ansible log:
Positional arguments: playbook.yml
verbosity: 4
connection: smart
timeout: 10
become_method: sudo
tags: ('all',)
inventory: ('hostsfile',)
subset: GROUP1
forks: 5
1 plays in playbook.yml
In the 'playbook.yml' file I do have 'hosts: all' but in a different job I don't have this issue.
Ansible does notice the Subset I indicated but is not filtering it, instead it is running the playbook successfully against all hosts.
I combed through the web for the entire day yesterday and could not find the problem.
Any help will be greatly appreciated.
I think I found the solution to this problem on my own.
By default, Ansible is parsing the hosts file using a set of inventory plugins until it will succeed.
By specifying the plugin in 'ansible.cfg':
[inventory]
enable_plugins = yaml
The problem was solved, I am not sure why.
https://docs.ansible.com/ansible/latest/plugins/inventory.html
I have a role which I would like to run multiple times with different vars files, I am currently doing the following:
- hosts: localhost
pre_tasks:
include_vars: "vars/vars1.yml"
roles:
- my_role
- hosts: localhost
pre_tasks:
include_vars: "vars/vars2.yml"
roles:
- my_role
Is there a less boilerplate way to do this? I know it is possible to parameterise roles but I can't find anything in the ansible documentation regarding running a role multiple times and calling a different include_vars each time.
i wanted to do something similar a while back and i ended up having two groups in my inventory
[group1]
localhost1
[group2]
localhost2
and then in group_vars i had different values. In your case that would be
# file: group_vars/group1/main.yml
include_file: vars/vars1.yml
and
# file: group_vars/group2/main.yml
include_file: vars/vars2.yml
Then, you can modify your playbook to something like this
- hosts: all
pre_tasks:
include_vars: "{{ include_file }}"
roles:
- my_role
and finally, execute your playbook for both groups
ansible-playbook pb.yml -l group1,group2
and it should take care of both instalations
I have a host in 2 groups : pc and Servers
I have 2 group_vars (pc and servers) with, in each the file packages.yml
These files define the list of packages to be installed on pc hosts and on servers hosts
I have a role to install default package
The problem is : only the group_vars/pc/packages.yml is take into account by the role task, packages from group_vars/servers/packages.yml are not installed
Of course what I want is installation of packages defined for pc and servers
I do not know if it is a bug or a feature ...
Thanks for your help
here is the configuration :
# file: production
[pc]
armen
kerbel
kerzo
[servers]
kerbel
---
# packages on servers
packages:
- lftp
- mercurial
---
# packages on pc
packages:
- keepassx
- lm-sensors
- hddtemp
It's not a bug. According to the docs about variable precedence, you shouldn't define a variable in multiple places and try to keep it simple. Michael DeHaan (Ansible's lead dev) responded to a similar question on this topic:
Generally I find the purpose of plays though to bind hosts to roles, so the individual roles should contain the package lists.
I would use roles as it's a bit cleaner IMO.
If you really want (and this is NOT the recommended way), you can set the hash_behaviour option in ansible.cfg:
[defaults]
hash_behaviour = merge
This will cause the merging of two values when a hash (dict) is redefined, instead of replacing the old value with the new one. This does NOT work on lists, though, so you'll need to create a hash of lists, like:
group_vars/all/package.yml:
packages:
all: [pkg1, pkg2]
group_vars/servers/package.yml:
packages:
servers: [pkg3, pkg4]
Looping though that in the playbook is a bit more complex though.
If you want to use such scheme. You should set the hash_behaviour option in ansible.cfg:
[defaults]
hash_behaviour = merge
In addition, you have to use dictionaries instead of lists. To prevent duplicates I recommend to use names as keys, for example:
group_vars/servers/packages.yml:
packages:
package_name1:
package_name2:
group_vars/pc/packages.yml:
packages:
package_name3:
package_name4:
And in a playbook task (| default({}) - for an absent "package" variable case):
- name: install host packages
yum: name={{ item.key }} state=latest
with_dict: packages | default({})
Create a dictionary of the variables per group and merge the lists on your own. For example, create a project for testing
shell> tree .
.
├── ansible.cfg
├── group_dict_create.yml
├── group_vars
│ ├── all
│ │ └── group_dict_vars.yml
│ ├── pc
│ │ └── packages.yml
│ └── servers
│ └── packages.yml
├── hosts
└── pb.yml
4 directories, 7 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
[pc]
armen
kerbel
kerzo
[servers]
kernel
shell> cat group_vars/pc/packages.yml
packages:
- keepassx
- lm-sensors
- hddtemp
shell> cat group_vars/servers/packages.yml
packages:
- lftp
- mercurial
shell> cat pb.yml
- hosts: armen,kerbel,kerzo
pre_tasks:
- import_tasks: group_dict_create.yml
tasks:
- debug:
var: my_packages
Declare variables in group_vars/all/group_dict_vars.yml
shell> cat group_vars/all/group_dict_vars.yml
group_vars_dir: "{{ inventory_dir }}/group_vars"
group_names_all: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'group_names')|
flatten|unique }}"
group_dict_str: |
{% for group in group_names_all %}
{{ group }}: {{ lookup('vars', 'groupvars_' ~ group) }}
{% endfor %}
_group_dict: "{{ group_dict_str|from_yaml }}"
my_packages: "{{ group_names|map('extract', group_dict, 'packages')|
flatten|unique }}"
group_vars_dir: The directory group_vars can be used either in the directory where the inventory or the playbook comes from. In this example, these directories are identical and we set it to inventory_dir. In the loop, the task include_vars will read all YAML and JSON files from group_vars/<group> and will store the variables in the dictionary groupvars_<group>, where <group> are items of group_names_all.
group_names_all: This is a list of all groups the hosts are members of. See group_names
group_dict_str: Create the string with the YAML structure of the dictionary
_group_dict: Convert the string to YAML
my_packages: Merge the lists of packages from the groups the host is a member of. If needed, use this variable as a pattern of how to merge other variables.
Create a block of tasks that creates the dictionary and writes the file
shell> cat group_dict_create.yml
- name: Create dictionary group_dict in group_vars/all/group_dict.yml
block:
- name: Create directory group_vars/all
file:
state: directory
path: "{{ group_vars_dir }}/all"
- include_vars:
dir: "{{ group_vars_dir }}/{{ item }}"
name: "groupvars_{{ item }}"
loop: "{{ group_names_all }}"
- debug:
var: _group_dict
when: debug|d(false)|bool
- name: Write group_dict to group_vars/all/group_dict.yml
copy:
dest: "{{ group_vars_dir }}/all/group_dict.yml"
content: |
group_dict:
{{ _group_dict|to_nice_yaml(indent=2)|indent(2) }}
- include_vars:
file: "{{ group_vars_dir }}/all/group_dict.yml"
delegate_to: localhost
run_once: true
when: group_dict is not defined or group_dict_refresh|d(false)|bool
If the dictionary group_dict does not exist (the file group_vars/all/group_dict.yml has not been created yet) it will create the dictionary, write it to the file group_vars/all/group_dict.yml, and include it in the play. You can refresh group_dict by setting group_dict_refresh=true if you change the variables in group_vars/<group>.
shell> cat group_vars/all/group_dict.yml
group_dict:
pc:
packages:
- keepassx
- lm-sensors
- hddtemp
servers:
packages:
- lftp
- mercurial
The results, stored in the variable my_packages, are merged lists of packages by groups
TASK [debug] *********************************************************
ok: [kerbel] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
- lftp
- mercurial
ok: [armen] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
ok: [kerzo] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
Notes:
Best practice is running group_dict_create.yml separately for all hosts and letting other playbooks use created group_vars/all/group_dict.yml
The framework described here is idempotent.
The framework should be easily extendable to other use cases of merging variables by groups.
Example. Add variable users to group_vars/<group>
shell> cat group_vars/pc/users.yml
users:
- alice
- bob
shell> cat group_vars/servers/users.yml
users:
- carol
- dave
, and add variable my_users to group_vars/all/group_dict_vars.yml
my_users: "{{ group_names|map('extract', group_dict, 'users')|flatten|unique }}"
Refresh the dictionary group_dict for all hosts
shell> ansible-playbook pb.yml -l all -e group_dict_refresh=true
gives
ok: [armen] =>
my_users:
- alice
- bob
ok: [kerbel] =>
my_users:
- alice
- bob
- carol
- dave
ok: [kerzo] =>
my_users:
- alice
- bob