3 things I’ve Learned about Ansible (the hard way)

Ansible’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.

Tags Don’t Go as Far as You’d Expect

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, 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.ymlPLAY [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 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.ymlPLAY [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 tags with role and include statements. If things need to be treated sensitively, I generally set a boolean variable and default it to falseor no rather than having things happen accidentally.

A Hash is a Hash until You Trample on it

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.

set_fact is Very Sticky

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.

The New Thing: Running Roles as a Task!

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 blocks …

👏 If you enjoyed this story, clap it up! 👏

uptime 99 is a Fairwinds publication about everything cloud, Kubernetes, DevOps, and more. If you’d like to write for uptime 99, email us!

Fairwinds — The Kubernetes Enablement Company | Editor of uptime 99

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store