Tips & tricks for Ansible

Ansible is a very powerful configuration management tool. It can be used to deploy settings and software to one or more (virtual) machines. Using a CM-tool ensures that every deployment is done in exactly the same way. This doesn’t only make your job as a system engineer a whole lot easier, it also results in a more stable environment since every machine is consistent and more predictable. I’m using Ansible intensively for quite some time now and found that it was time to write a post about some small, but usually hard to find or annoying to solve, problems or challenges.

Handlers

Handles will execute a certain action when a task ends with a status changed. Handlers are very powerful and useful since they aren’t executed every time they are called. Instead, the action required by the handler will be executed only once at the end of the play.

Keep all handlers in one file

Every role which you define in Ansible can contain a certain set of handlers. In a lot of cases, you end up with defining the same handler (for example: restart iptables or reload systemd) multiple times, for every role. Personally, I find it easier to have one file containing all my handlers and include that file from a specific role.

Create new roles with a main.yml in handlers/ containing the following:

Define all handlers in /etc/playbooks/roles/handlers.yml:

Define a handler which suits different environments

When you’re creating roles and deploy them to different types of systems or different operating system versions, soon or later you’ll run into the problem that, for example, a name of a service is different on another distribution (for example: Apache on RHEL-based distributions is called http or the change to mariaDB in CentOS 7 from MySQL in CentOS 6). Theoretically, you could define each task which requires such kind of handler multiple times but that’s not really helpful for creating clean and clear roles.

To overcome this problem: call two handlers, one for each possible scenario, from where you would normally call your handler in your task:

Define two handlers but put the condition to determine the action inside the handlers-file:

Variables

Variables are key in Ansible and when you know how to flexibly use variables, a whole lot of possibilities become available.

Set variables dynamically in a task

When you want to let a variable depend on another on or just on a fact, you can set the variable on the fly in the task using the variable. This can be done with set_fact:

When you want to set a default for a variable, you can do this as follows:

Specify multiple parameters in with_items

With_items for a task allows you to execute the task multiple times with different values. In some cases, you want to execute a task multiple times but use several different (but connected) values.

You can use the following example to specify multiple parameters in with_items:

Define empty lists

As with the previous example, you could also have cases where you use with_items combined with a list defined in the inventory. If the list isn’t defined, you will receive an error when executing the playbook. To overcome this problem but still not execute the task, you can define an empty list in the inventory when needed.

Imagine the following task:

When in a certain case, you do not need to create users, you can define the list as follows in the inventory

An alternative to this, is to check if the variable used in with_items is defined but that requires more work and makes the playbook less clear:

Substring a variabele in Ansible

It happens often that you only require a part of the value of a variable. In such case you can substring the value to get only parts of it. A use case could be version numbering. Sometimes a version is written as x.y and in another place as xy (for example package postgresql94-server install service postgresql-9.4.

The following task:

Gets you the following result:

Calculate variables

Another manipulation with variables that’s often used is calculating variable values. As with most of the examples here, it’s quite easy to use but not always easy to find the correct syntax:

Often, you get a result in a different format than you require. For example a float after dividing where you need a integer. You can round variables using round or convert them to int as follows:

Get the IP of a hostname inside a playbook

While this trick isn’t very Ansible-related it is something I use quite often. For some components, you require the IP-address of a hostname which you need to enter elsewhere, think of configuring a cluster or such. While I could define two variables myself, I find it better to define the hostname to use and search for the IP:

In the above example, the hostname is testmachine.test.dom but it could be a public hostname or a variable.

Use subitems in lists

One things which I find very powerful but at the same time also complex, is the use of nested lists. Complex nested lists can be used with with_subelements but the more simple version is by using a list that defines key-value items.

Imagine the following list:

The values in the list can be used in a with_items loop in a very user-friendly way:

Templates

Templates are mainly used to create or modify files that have small parameters at the destination. Templates are written as Jinja2 and there is good documentation available. Nevertheless, here are some examples:

Looping a list

Using lists is also possible in templates. To do so, use the following example:

Imagine the list as I used in the example above. To use this list in a template:

This will generate a file containing the following:

{{loop.index}} gives you the index of the loop
{{loop.last}} can be used to test if this is the last item in the loop (for example to close some brackets).

To combine lists in a loop as above, you can use the following syntax:

Various

Execute an action only when another action was executed

It happens often that something only needs to be done if a previous step was executed. For example, run a first initialization of product only when it was just installed.

This can be realized by using a a register and the .changed of that register.

A small example:

Manage known_hosts with Ansible

Doing things via Ansible often requires a different approach since you’re running everything as batch and can’t give input to questions or confirmations. To allow access to a host over SSH, it needs to be added to the known_hosts for the user accessing the system.

You can use the following example to maintain known_host-entries

Execute a script and check the execution

A lot of functionality exists in the Ansible modules but sometimes you need to execute a small script on a target-machine in order to initiate something or just because it’s needed.

One way is to set the return code of the script (Ansible considers everything >0 as failed) but not all scripts handle return codes as they should.

With the following example, you can set the status of the task depending on the output of the script that was ran:

Open one or more ports with iptables

As most of you have noticed, there isn’t any Ansible module for iptables and there are no plans to make one. There are good reasons to not have such module but the work needs to be done so this is how I open up ports in the firewall with Ansible:

To open up one port:

To open up a list of ports:

The handler reload iptables is just a restart of iptables:

Install SELinux modules idempotent

Ansible comes with a module to control SELinux booleans but in some cases, a custom SELinux is required. Since there isn’t any module to do this (I should find some time and create a pull request for this), I use the following method to idem-potently add SELinux modules:

Configure SELinux ports idempotent

Same thing as above is valid to allow non-standard ports in SELinux in a way that is safe to execute multiple times:

Include specific task files

Altough this is well documented, I wanted to mention it here because it can come in very handy.

Imagine you want to create a role to deploy different Oracle Java versions. The role is defined in /etc/ansible/playbooks/java and there is a variable for every version to determine what to install.

One way would be to go trough all steps for every version and add a version-condition to all of them. Another way is to use includes:

The java-role will have a tasks/main.yml which is executed by default can contains statements like this:

This allows you to keep a neat main.yml and specific task-files in /tasks/oracle_java_1*.yml where you are more specific. The condition only needs to be written on the include statement and not for every statement in the included file.

I also use this for roles which differ a lot between different distributions.

Use host-spefic files or templates

As with the above example, this is also documented but very useful.

Image a situation where you need to copy a configuration file in XML that’s completely different for every host you’re deploying to. You can’t really define 100’s of lines as a variable for every line in the file and there is no way that you find enough similarities to create a template. For such case, it can be easy to create a file or folder with the name of the host to deploy to and use the name of the host in the path:

Replace or remove a word in a line with lineinfile

This one is more of a regular expressions and syntax thing but took me some time to figure out correctly.

To replace words in an existing, unpredictable, line of a file, you can use a regex with backrefs.

Example: remove a kernel parameter

Check if a file or directory exists

Checking if a file or directory exists sound a little weird but if you think a little further, it can come in very useful to check if something has been installed or if a certain script has already been executed. In other ways, to make non-repeatable scripts or commands idempotent.

Example: move a file if it exists

stat.isdir can also be used to check if something is a directory

Example: check if a custom program is installed to prevent multiple executions of the installer (another way to do the same as above):

More tips & tricks

Hopefully the above examples and small explanations can help you to get even more out of Ansible. Feel free to mention common issues with or without their solution because I’m sure the above list is far from complete.

5 thoughts on “Tips & tricks for Ansible

  1. Great article and one of the few for Ansible tricks and tips. I like the iptables suggestion. I use UFW which is still super easy to handle with the ‘ufw’ module.

Leave a Reply

Your email address will not be published. Required fields are marked *