Ansible tag's simple requirements make it very easy to get started. Overall, it works extremely well, but once you get a bit deeper some things might end up causing discomfort. Here are 3 things I’ve learned about Ansible (or re-learned) the hard way.
Ansible tags are a powerful way to limit the amount of work that gets done. Generally, the playbooks will run all the way through because you made them idempotent, but sometimes it’s nice to just target a very specific part. After all, why run through all the database-related tasks when you’re simply looking to change a setting in Nginx?
Clearly, ansible tags are great for simplifying work, but they have a very clear limitation. Once a play matches a tag, it will also run every task that is part of that play regardless of other tags.
Let’s take a look at an example. The following is a very simple top-level play:
- include: sub.yml
tags: top
That file includes the following sub.yml
:
- hosts: localhost
tasks:
- debug: msg="sub and top"
tags: sub,top
- debug: msg="sub"
tags: sub
- debug: msg="blank"
Running ansible-playbook
without any --tags
produces all the debug message (that should be obvious since Ansible’s default is --tags all
). Running ansible-playbook
with --tags top
will also produce all the output:
$ ansible-playbook --tags top ./top.yml
PLAY [localhost] ***************************************************************
TASK [setup] *******************************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "sub and top"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "sub"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "blank"
}
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0
This information is mentioned in the documentation, though it stops far short of belaboring the point:
"All of these apply the specified tags to EACH task inside the play, included file, or role, so that these tasks can be selectively run when the playbook is invoked with the corresponding tags."
Thus, if you were hoping to use the ansible tag within the included play to continue to isolate things, you’ll be disappointed.
It is possible to further limit what happens at execution by adding --skip-tags
:
$ ansible-playbook --tags top --skip-tags sub ./top.yml
PLAY [localhost] ***************************************************************
TASK [setup] *******************************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "blank"
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
In this case, the ambiguous first task in sub.yml
matches and doesn’t. The effect is that it does not run.
Lastly, if the top level is skipped, nothing happens:
$ ansible-playbook --tags sub --skip-tags top ./top.yml
PLAY [localhost] ***************************************************************
PLAY RECAP *********************************************************************
There is still a lot of power in the --tags
, but I know I spent too much time figuring out why something did or did not run. As a rule of thumb, I try to use ansible tags with role and include statements. If things need to be treated sensitively, I generally set a boolean variable and default it to false
or no
rather than having things happen accidentally.
Working with hashes in can be tricky in Ansible due to the hash merge behavior of either replace
(the default) or merge
.
So what does that mean? Let’s use an example:
mj:
name: M Jay
job: hash merger
skill: intrepid
Say we want to augment that hash by adding another bit to mj
:
mj:
eye_color: blue
With the default behavior of replace
, you’ll end up with only mj
’s eye color. Setting hash_behaviour = merge
in the ansible.cfg
will get you a hash that has all of mj
’s attributes.
Now the problem with overriding the setting in the config file is that it might come back to bite you. If that file gets munged or it’s run on another system, etc., you’ll likely end up with some very strange effects. The way around it in Ansible 2 is to use the combine
filter to explicitly merge.
The documentation on this topic is pretty good. For our example above, it would look like this:
- hosts: localhost
vars:
var1:
mj:
name: M Jay
job: hash merger
skill: intrepid
var2:
mj:
eye_color: blue
tasks:
- debug: var=var1
- debug: var=var2
- set_fact:
combined_var: "{ { var1 \ combine(var2, recursive=True) } }"
- debug: var=combined_var
You’ll end up with the following:
$ ansible-playbook hash.yml
PLAY [localhost] ***************************************************************
TASK [setup] *******************************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
"var1": {
"mj": {
"job": "hash merger",
"name": "M Jay",
"skill": "intrepid"
}
}
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"var2": {
"mj": {
"eye_color": "blue"
}
}
}
TASK [set_fact] ****************************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
"combined_var": {
"mj": {
"eye_color": "blue",
"job": "hash merger",
"name": "M Jay",
"skill": "intrepid"
}
}
}
PLAY RECAP *********************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0
Again, my advice: don’t change the default, and learn to love the combine
filter.
There are multiple ways to set variables in Ansible: include_vars
, vars:
, passing them as part of the play and, of course, set_fact
. The thing to realize is that once you use set_fact
to set a variable, you can’t change it unless you use set_fact
again. I suppose it’s technically more correct to say that fact has higher precedence, but the effect is the same. Take the following example:
# note that the included play only contains a debug
# statement like the others listed in the example below.
- hosts: localhost
vars:
var1: foo
tasks:
# produces foo (defined in vars above)
- debug: msg="original value is "
# also produces foo
- include: var_sub.yml
# produces bar (since it's being passed in)
- include: var_sub.yml var1=bar
# produces foo (we're back to the original scope)
- debug: msg="value after passing to include is "
# now it get's interesting
- set_fact:
var1: baz
# produces baz
- debug: msg="value after set_fact is "
# also produces baz
- include: var_sub.yml
# baz again!!! since set_fact carries the precedence
- include: var_sub.yml var1=bar
# using set_fact we can change the value
- set_fact:
var1: bat
# the rest all produce bat
- debug: msg="value after set_fact is "
- include: var_sub.yml
- include: var_sub.yml var1=bar
The moral is that running with -vvv
or adding debug
tasks is a good idea when you’re seeing some odd behavior.
I was excited to learn that with Ansible version 2.2 it became possible to run roles as tasks. That may not sound like much, but it’s very powerful and could have saved me a lot of weird code I used to write and/or duplicate.
The magic incantation is include_role.
This new feature is very powerful and allows you to write smaller roles that are easy to include. For example, you can easily create a role to manage your database tables or Elasticsearch indexes. I’ve come across a few use cases. With this new play, it’s now possible to do things like:
- include_role: create-db
with_items:
- 'test1'
- 'tester'
- 'monkey'
- 'production'
It’s so powerful that I can’t believe it wasn’t there a long time ago.
Now, if only I could loop over block
s …