Ansible

Ansible is a cool tool that lets you manage lots of Linux servers and even some other devices. All with just a bunch of yaml. Although not necessarily as it supports a nice load of formats for everything. Yaml being the most common, but “inventories” are usually ini format. But still you should be able to do everything in json if you really want to.

Configure ansible.cfg

This is the main Ansible config. Ansible lets you have global stuff in /etc/ansible or project stuff in the working directory. Anyway you’ll likely want an ansible.cfg with your common global configuration options for Ansible itself.

Example global ansible.cfg

This example ansibe.cfg lists a collection of the options I think are useful.

You may also generate a file with all options available commented out. With comments describing them for most of the options by;

1
ansible-config init --disabled -t all > ansible.cfg

  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
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
[defaults]
roles_path = ~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
library = ~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
inventory = ~/.ansible/hosts,/etc/ansible/hosts

collections_path = ~/.ansible/collections:/usr/share/ansible/collections
# This may be "ignore" or "fail", "warnging" is the default
collections_on_ansible_version_mismatch = warning
collections_scan_sys_path = True

# Very useful, this will issue a warning if you run a command that
# Ansible thinks there is an existing module to do the same job!
command_warnings = True
forks = 42

# The default for gathering is "impicit", meaning Ansible won't cache facts
gathering = smart
gather_timeout = 13

# May specify "paramiko", "ssh" or "smart" for choosing based of OS and ssh versions
transport = smart
use_persistent_connections = True
private_key_file = ~/.ssh/id_rsa
pipelining = ANSIBLE_PIPELINING

# Task timeout, 0 = no timeout
task_timeout = 0
# Connection timeout
timeout = 7

# Run plays as fast as possible
strategy = free

# This gives better performance at the expense of CPU load,
# so I guess you'd want it low on your workstation or laptop,
# but higher on like an Ansible tower or similar automation setup using Ansible
internal_poll_interval = 0.000001
poll_interval = 3

system_tmpdirs = /tmp, /var/tmp
remote_tmp = ~/.ansible/tmp
local_tmp = ~/.ansible/tmp

jinja2_native_warning = True
jinja2_native = True

# This one is only really useful to make sure no secrets are logged on servers
# when running Ansible tower or other automation systems using Ansible
no_log = False
# Logging is disabled on the client if "log_path" is empty
log_path =
# Target hosts logging facility
syslog_facility = LOG_USER
no_target_syslog = False

# Must be one from python [zipfile](https://docs.python.org/3/library/zipfile.html) built-in library that is supported
# on both the client and the target servers Ansible is going to run on, ZIP_DEFLATED is the default
module_compression = ZIP_LZMA
# ZIP_STORED = no compression
# ZIP_DEFLATED = normal zip, requires zlib python module
# ZIP_BZIP2 = bzip2, requires bz2 python module
# ZIP_LZMA = lzma, requires lzma python module

# Default module to run with the "ansible" command if not -m [module] is specified
module_name = command

# What will null variables in templates show?
null_representation =

# Let's make some errors warnings instead
invalid_task_attribute_failed = False
error_on_missing_handler = False
string_conversion_action = warn

# The same as the -v command line arg
verbosity = 0

# Extra output goodness!~
show_task_path_on_failure = True
display_args_to_stdout = True
display_skipped_hosts = True
show_per_host_start = True
check_mode_markers = True
show_custom_stats = True
display_ok_hosts = True

# This may be useful
retry_files_enabled = False

yaml_valid_extensions = .yml, .yaml, .json

display_failed_stderr = True

[persistent_connection]
command_timeout = 42
connect_retry_timeout = 7
connect_timeout = 60

[connection]
# This only works if your become supports not using a tty,
# but gives "significant performance improvement when enabled."
pipelining = True

# Eye-candy!
[colors]
changed = bright yellow
console_prompt = white
debug = gray
deprecate = purple
diff_add = green
diff_lines = cyan
diff_remove = red
error = red
highlight = white
ok = green
skip = cyan
unreachable = bright red
verbose = blue
warn = bright purple

[selinux]
# Enable this if running SELinux
libvirt_lxc_noseclabel = False
# We want this to be empty unless maybe nfs target? it may be a list of filesystems
special_context_filesystems =

[diff]
always = True
context = 3

[inventory]
enable_plugins = host_list, script, auto, yaml, ini, toml

[paramiko_connection]
host_key_auto_add = True

Example project ansible.cfg

1
2
3
4
5
6
7
[default]
private_key_file = ~/.ssh/ansible_rsa
remote_user = ansible
inventory = all-machines

[privilege_escalation]
become = yes

Inventory

I think it’s super neat that Ansible lets you use several file formats for most things. Inventories being the one that supports most formats, I’m pretty sure. As you may just have a text file list, or YAML, INI, TOML and JSON to use host groups.

You may even have scripts that generate the target hosts with groups and everything. If you opt for the script inventory route that script has to output the inventory in JSON format. There is also the possibility of creating your own inventory plugin with python.

For the script based inventory you have to support command-line arguments “–list” and “–host ”. Those have to output the whole inventory in JSON format for “–list”. And the host vars for “–host ”.

The simplest inventories

These are of course just lists of hosts, they could be in a text file or specified from the command-line as a comma separated list. Then you’ve got INI file based inventories, and it quickly may become advanced after that. Even with the INI based ones you may group hosts together.

Next up is three examples that effectively gives the same inventory. Although the text file won’t give host groups.

A text file inventory list

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
lost-islands
toten
hamar
lofoten
narvik
gulf-of-oman
deploy
bergen
molde
hangar-22
myrkdalen
backup.skyid.no
monitoring
smokeping.skyid.no
mercury
scrapmetal
dawnbreaker
propaganda

INI inventory

Now host groups come into the picture and the rest of the examples will all give the same inventory.

 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
50
51
52
53
54
55
56
[prod]
lost-islands

[prod:children]
prod_frontend
prod_backend
prod_vpn
prod_db
monitor
backups
misc

[prod_frontend]
hamar
toten

[prod_backend]
lofoten
narvik

[prod_vpn]
bergen
molde

[prod_db]
hangar-22

[monitor]
smokeping.skyid.no
monitoring

[backups]
backup.skyid.no
myrkdalen

[misc]
gulf-of-oman
deploy

[dev:children]
dev_frontend
dev_backend
dev_vpn
dev_db

[dev_frontend]
mercury

[dev_backend]
scrapmetal

[dev_vpn]
dawnbreaker

[dev_db]
propaganda

Example YAML inventory

This one will give you the exact inventory as above.

 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
all:
  children:
    prod:
      hosts:
        lost-islands:
      children:
        prod_frontend:
          hosts:
            hamar:
            toten:
        prod_backend:
          hosts:
            lofoten:
            narvik:
        prod_vpn:
          hosts:
            bergen:
            molde:
        prod_db:
          hosts:
            hangar-22:
        monitor:
          hosts:
            smokeping.skyid.no:
            monitoring:
        backups:
          hosts:
            backup.skyid.no:
            myrkdalen:
        misc:
          hosts:
            gulf-of-oman:
            deploy:
    dev:
      children:
        dev_frontend:
          hosts:
            mercury:
        dev_backend:
          hosts:
            scrapmetal:
        dev_vpn:
          hosts:
            dawnbreaker:
        dev_db:
          hosts:
            propaganda:

Example python inventory script

This one is kinda just stupid as it just json dumps a dict containing the whole inventory. To do some real magic with this you’d want to make that whole dict generated by code. Very useful if for some reason your inventory is highly dynamic.

 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/python3
from json import dumps

inv = {
  'prod_frontend': {
    'hosts': ['toten', 'hamar']
  }, 'prod_backend': {
    'hosts': ['lofoten', 'narvik']
  }, 'prod_vpn': { # vpn/radius
    'hosts': ['bergen', 'molde']
  }, 'prod_db': {
    'hosts': ['hangar-22']
  }, 'prod': {
    'hosts': ['lost-islands'], # monitoring and internal DNS
    'children': [
      'prod_frontend', 'prod_backend', 'misc',
      'prod_vpn', 'prod_db', 'backups', 'monitor'
    ]
  },
  'misc': {
    'hosts': ['gulf-of-oman', 'deploy']
  }, 'backups': {
    'hosts': ['myrkdalen', 'backup.skyid.no']
  }, 'monitor': {
    'hosts': ['monitoring', 'smokeping.skyid.no']
  },
  'dev_frontend': { 'hosts': ['mercury'] },
  'dev_backend': { 'hosts': ['scrapmetal'] },
  'dev_vpn': { 'hosts': ['dawnbreaker'] },
  'dev_db': { 'hosts': ['propaganda'] },
  'dev': {
    'children': [
      'dev_frontend', 'dev_backend', 'dev_vpn', 'dev_db'
    ]
  },
  "_meta": {
    # since we're not doing hostvars this will make it a whole lot faster
    # as Ansible won't have to run this script with --host for each one
    "hostvars": {}
  }
}

def inventory():
  return dumps(inv, indent=2)

def group_vars(host):
  for group in inv.values():
    if host in group.get('hosts', []):
      return dumps(group.get('vars', {}), indent=2)
  return '{}'

if __name__ == '__main__':
  from argparse import ArgumentParser
  argparse = ArgumentParser()
  argparse.add_argument(
    '-l', '--list', action='store_true',
    help='Print the inventory to stdout')
  argparse.add_argument(
    '-v', '--host', type=str,
    help='Print host vars for a specific host')
  args = argparse.parse_args()
  if args.list:
    print(inventory())
  elif args.host:
    print(group_vars(args.host))
  else:
    argparse.print_help()

Inventory summary

So the takeaway in my opinion is that INI or TOML based inventories are best. As the YAML based ones are no fun working with. JSON is also not optimal to manually work with, but a script generating that JSON may be very useful. Again if you have the need of a potentially very dynamic inventory.

There is also the inventory parameters, these let you override lots of settings on a per-host basis. Here is a super simple inventory to test plays/playbooks against a docker container;

1
2
[docker]
docker-test ansible_connection=docker ansible_user=root ansible_host=ansible

You may use that inventory and run plays agains whatever container is named “ansible”. Or change the “ansible_host” parameter to the name of whatever container you’d like to use.

These optional parameters are “ansible_” something. And you may control everything from connection and

INVENTORY PARAMETERS!