Getting Started with Ansible

Page breadcrumbsEnd of page breadcrumbs

So, for the past week I’ve been using Ansible in anger. Genuine, bare knuckled, actually trying to get shit done with it. Oh yes, I’ve tinkered over the years, nothing serious. You know, to kick the tires. But I never really saw the point. I was pretty happy with puppet. But recently the team I work in decided as a group to adopt Ansible for our provisioning and management tasks. I think that it’s a good choice – for a bunch of reasons – but I don’t really want to go into them here.

What I want to do, is document a little of my journey, and explain what I’ve learned after a week or so of trying to use this thing for reals.

The thing you have to understand about Ansible is that it is not a scripting language. It looks a lot like a scripting language, but it’s not. It can use scripts just fine – you can write your own custom modules in python, and the “tasks” model of Ansible looks a lot like scripting, but if you start doing anything remotely complex in Ansible, you’re going to have a bad time.

Ansible doesn’t replace the scripting tools you have today. It doesn’t stop you from needing to use shell scripts to do things. What it will do, is provide you with a way to orchestrate those tools that you have, so that they work together in a (hopefully) harmonious way.

First, a note on Nomenclature

People get hung up on a lot of nomenclature used with Ansible. It really tripped me up, too.

  • Task – A task is a single Idempotent step in a Role or a Playbook. By “Idempotent”, they mean, you should be able to run each task several times and you still get the same result. Previous executions should not effect latter executions.
  • Role – In Ansible, a Role is a set of tasks that are applied when a role is included on a host, to perform some peice of work. That’s it. Don’t get hung up on the word. Just think of it as a library of tasks, which can also contains some custom python, all kinds of stuff.The idea is that you write a Role for different components of your application. For example, you might write a role for Sensu, Redis and RabbitMQ. Redis and RabbitMQ roles wouldn’t know anything about each other, and don’t need to. They also don’t know anything about Sensu. But Sensu would need to know where Redis and RabbitMQ are, and what authentication to use, etc. So you’d pass in some variables to the Sensu Role so that when it configures Sensu, it’s able to connect to the resources it needs.If you look at Ansible Galaxy you will find that roles are very strictly defined like this. They tell ansible how, but they don’t tell what.
  • Playbook – A playbook is a collection of tasks that are applied against a host, or a group of hosts. Roles, can also be applied during a playbook at any time.
  • Inventory – a file or a directory containing many files, that describe your environments. You can have many inventories and those inventories can also be dynamic (for example, with ec2.py) so that you don’t even have to manually maintain them.Inventories define groups of hosts, which are not hierarchical but are more like an overlapping venn diagram of hosts.

So, think of Playbooks as a way to structure your Roles such that the Tasks in the roles can be run over various hosts listed in your Inventory, which configuration taken from your Inventory group or host variables (more on that later).

Playbooks and Inventory tell Ansible what.

Roles tell ansible how.

Structuring your Ansible Project

I recommend that when you start fiddling with Ansible that you make a project with a directory structure like the following. This differs a little form Ansible’s Best Practices document, but there’s a reason for it: I don’t think having all your playbooks scattered in the root is a great idea.

ansible.cfg
inventory/
playbooks/
playbooks/adhoc/
playbooks/construct/
playbooks/deploy/
roles/
README.md

ansible.cfg should contain this:

[defaults]
stdout_callback=debug
stderr_callback=debug
roles_path = ~/.ansible/roles:./roles:/etc/ansible/roles

This will give you slightly better output when there are errors, and it will also make it so that when you run ansible from the project root directory, you will add ./roles as a path to search for roles.

Now, when you develop playbooks, to start with, use the adhoc directory.

Roles obviously go in the roles directory, and inventory can be split up into several subdirectories for production, development, etc.

The Nitty Gritty

So thats really the high level overview. I don’t want to get into the basics of ansible, because that’s really well covered elsewhere. What I want to talk about is the places where I screwed up and spent hours going down a rabbit hole, because I didn’t understand how Ansible works. The documentation isn’t much help either.

Jinja Templates

Oh, god. You are going to kind of loath these things once you work them out. Here’s the thing. Every “value” in a yaml file can be represented by an expression, like this:

"{{ some_variable }}"

You’ll see this scattered everywhere, and what you may not realise right away is that everything inside the quotes and curly braces is a separate little text templating language called Jinja2.

This means, if you want to do math, or you want to manipulate a string, or you want to add another key/value pair to an ansible map, you need to use a Jinja2 expression.

Often you will have something like this in a vars file or in a group_vars or something like that:

a_map_of_things:
  first_thing: "A string!"
  second_thing: "Another string"

Now, lets say you want to add to that map in Ansible with a new key value pair, then you’d have to do something like this:

- set_fact:
  a_map_of_things: "{{ {'third_thing': 'Third string!' } |combine(a_map_of_things) }}"

Ugh.Yup. Thats how you do it. I did mention this isn’t a scripting language, right?

Also, you have to know, variables do have a type, hidden to you. They can at least be an int and a string. I don’t know about other types yet. But if you want to massage a string (and it will silently massage some strings) into an int, the you pass it to |int. Thats a pipe character, not a lower case L. Just in case you’re wondering.

Go check out the filters documentation.

Maps and Arrays

So pay attention; yaml defines maps and arrays one way, and Jinja2 defines them another way. Because we don’t need consistency in our life.

Loops

Loops aren’t something that YAML can represent well, so they provide you with a field to add to your task called with_items to be able to loop over things. But what if you want to loop over a group of tasks? Well, you can’t use blockbecause it’s too hard to implement apparently.

But you can loop over include_tasks, so that’s cool. Just remember to use loop_control: loop_var: or if you need to loop inside a loop you’re gonna have a bad time.

Variable Scope

Talking about variable scope, it’s not immediately obvious, but there is no real “global variable”, You can set a variable in a Playbook and then use it in a lower scope, like a Role, but they don’t go the other way. And they do not go backwards.

If you want a “global” variable construct, that you can set, and modify anywhere, you will

add_host:
name: "GLOBAL_VARIABLE_BULLSHIT"
global_variable: 0

So, now, you can increment that crap anywhere in your executing playbook, just make sure you add_host with the variable if you change it so it’s made available outside of your scope. Yeah, I wish I was joking.

Fact Caching

OK, so I didn’t know about this. But apparently you can have ansible store facts in a fact cache. Ok, that’s a little less crap.

Variable Precedence

So, just to make things even more annoying you need to understand Variable Precedence.

The documentation is fine here. Just, you know, you may not notice it until hour 50. Like me. You idiot.

Conclusion

Ansible. Super powerful, it’s genuinely a good product, but it has some horrible flaws, like pretty much everything else out there on the market.

Here’s the thing; Ansible tried really really hard to not invent it’s own DSL like Puppet did. And that’s cool, I can respect that. They didn’t want Ansible to turn into a programming language.

But then, they went ahead and they are adding more and more programming language type features, but making them super opaque, hard to debug and really clumsy to use in a yaml file.

Honestly, at this point I kinda feel like if they just used python for the programming language bits, with yaml for “configuration” and just gave us a super nice API, that would be fine. This whole yaml programming language is a load of bullshit and just makes things harder to express.

Anyway. It’s not bad. I’m going to keep using it, because the built-in modules are pretty awesome. But I don’t think it’s the best thing since sliced cheese.