Ansible Cheat Sheet
Besides the many modules, there are actually not that many tricks to Ansible. I’m assuming you know a bit about Ansible and would like to explore the edges what can be done with Ansible.
Conditionals determine when a task should run. You can check to see if something is in the state it should be before executing the task.
- name: Do something ansible.builtin.debug: msg: "I will run if some_variable is defined." when: - some_variable is defined
Do something will on run if
some_variable is set. You can test many things.
Conditions can be applied to:
Good to know: a
when: statement accepts strings (
when: "" == "hello') or can accept lists:
- name: Do something ansible.builtin.debug: msg: "I will run if some_variable is defined." when: - some_variable is defined - somm_variable == "hello"
Determining what makes a task
Sometimes Ansible needs some help determining what the result of a task is. For example the
ansible.builtin.command module returns
changed, because Ansible assumes you’ve modified the node. In these cases, you will need to tell Ansible when it’s changed. The same can be used to
fail a task.
- name: Do something that does not change the system ansible.builtin.command: cmd: ls changed_when: no - name: Do something and help Ansible determine when the task is changed ansible.builtin.command: cmd: some_command_that_can_change_things register: my_command changed_when: - '"modified" in my_command.stdout' failed_when: - '"error" in my_command.stdout'
The first example never changes, in this case, that’s true;
ls is a read-only command.
The second example reports
modified can be found in the output. As you can see, you do need to use
register to save the output of the command.
error is found in the output.
Note: Ansible will fail by default if the
cmdreturns a non-0 exit code.
Notify and handlers
Handlers are used to run after some task changed. It’s useful to “gather” handlers and run them at the end. A classic example:
tasks: - name: Place a configuration. ansible.builtin.template: src: some_config.conf.j2 dest: /etc/some_config.conf notify: - Restart some_service handlers: - name: Restart some_service ansible.builtin.service: name: some_service state: restarted
In the above snippet, the task places a configuration and informs the handler
Restart some_service. This restart will occur at the end of a play.
There are some tricks:
It’s a list!
notify argument takes a list:
- name: Do something ansible.builtin.command: cmd: ls notify: - First handler - Second handler
I like to spread things vertically, it improves readability.
The order of the handlers is determined the order of the tasks as written in the
handlers. Take this example:
handlers: - name: First # Something - name: Second # Something - name: Third # Something
Using the task in the handlers above, the order will always be:
(And a handler can be skipped by not notifying it.)
You can chain handlers. Sometimes this helps to get a role idempotent.
tasks: - name: Do the first thing ansible.builtin.command: cmd: ls notify: - Do the second thing handlers: - name: Do the second thing ansible.builtin.command: cmd: ls notify: - Do the third thing - name: Do the third thing ansible.builtin.command: cmd: ls
You can create an not-so-infinite loop, by letting handler_one call handler_two and let handler_two call handler_one. This loops up to 3 times, not sure what prevents Ansible from an infinite loop, but thanks Ansible developers!
You can add a condition to a handler, just like any other task.
- name: I am a handler ansible.builtin.command: cmd: ls when: - some_variable is defined
Running handlers now
If required, you can tell Ansible to run the handlers right now. You’d typically wait until the end of the play, but there are certainly situations where the handlers need to run right now.
tasks: - name: Do something ansible.command.command: cmd: ls notify: - Some handler - name: Flush the handlers ansible.builtin.meta: flush_handlers - name: Do other things ansible.command.command: cmd: ls handlers: - name: Some handler ansible.command.command: cmd: ls
The order of the above tasks will be:
- Do something
- Flush the handlers
- Some handler
- Do other things
There are not many use-cases for
delegate_to, but some will apply. Consider this snippet:
- name: Do something to the API ansible.builtin.uri: url: "https://some.host.name/api/v1/user method: GET delegate_to: localhost
The example above gets information from an API. There is no specific need to run this API call on the targeted node. In such cases, you can use
The examples I can think of:
- Using the
- Copying files between nodes using the
Running a job just once
In some situations you want to do something to just one node from all targeted nodes.
tasks: - name: Do something ansible.builtin.command: cmd: ls run_once: yes
These situations may call for
run_once, there are probably many more:
- Selecting a host that will become a cluster leader/master/primary.
- Checking if a variable is set correctly.
delegate_toto do something once locally.
- Managing a cluster that’s also created in the same role.
Checking user input
When writing Ansible roles, checking user input ensures your role can work. Imagine you are asking for a list, but a user specifies a boolean.
# This is a list. my_variable_list: - one - two - three # This is a string, not a list. my_variable_list: hello
In the example above, the same varialbe is set twice (last one wins). The bottom one seems incorrect, as it’s not a list.
You can use at least two methods to verify if the input is correct; argument_specs or assert.
The documentation contains all details, but let’s take one example:
--- argument_specs: main: short_description: Install and configure Elasticsearch on your system. description: Install and configure Elasticsearch on your system. author: Robert de Bock options: elasticsearch_type: type: "str" default: oss description: "The license to use, `elastic` uses the \"Elastic\" license, `oss` uses the \"Apache 2.0\" license." choices: - elastic - oss elasticsearch_network_host: type: "str" default: "0.0.0.0" description: The IP address to bind on. elasticsearch_network_port: type: "int" default: 9200 description: The port to bind on. elasticsearch_discovery_seed_hosts: type: "list" default:  description: Provides a list of the addresses of the master-eligible nodes in the cluster. elasticsearch_cluster_initial_master_nodes: type: "list" default:  description: Sets the initial set of master-eligible nodes in a brand-new cluster.
Benefits of using arguments_specs include:
- Built in (since +- 2020)
- Can be used to generate docs.
A limitation is that you can only test a single variable, no combinations and the tests on that single variable are limited.
You can also
--- - name: Include assert.yml ansible.builtin.import_tasks: assert.yml run_once: yes delegate_to: localhost
--- - name: Test if elasticsearch_type is set correctly ansible.builtin.assert: that: - elasticsearch_type is defined - elasticsearch_type is string - elasticsearch_type in [ "elastic", "oss" ] quiet: yes - name: Test if elasticsearch_network_host is set correctly ansible.builtin.assert: that: - elasticsearch_network_host is defined - elasticsearch_network_host is string quiet: yes - name: Test if elasticsearch_http_port is set correctly ansible.builtin.assert: that: - elasticsearch_http_port is defined - elasticsearch_http_port is number - elasticsearch_http_port >=1 - elasticsearch_http_port <= 65536 quiet: yes - name: Test if elasticsearch_discovery_seed_hosts is set correctly ansible.builtin.assert: that: - elasticsearch_discovery_seed_hosts is defined - elasticsearch_discovery_seed_hosts is iterable quiet: yes - name: Test if elasticsearch_cluster_initial_master_nodes is set correctly ansible.builtin.assert: that: - elasticsearch_cluster_initial_master_nodes is defined - elasticsearch_cluster_initial_master_nodes is iterable quiet: yes
This does something similar as the
argument_specs described earlier.
- You can test more things of a single variable.
- You can set an error message.
- You can combine tests on multiple variables.
Determine the amount of nodes to run on simultanious.
Sometimes you want to limit the number of nodes being addressed at one time. One-host-at-a-time for example:
- name: Do something machine-by-machine hosts: web_servers serial: 1 ...
In the example above, only one node will be targeted at a time. This can be useful for updates to hosts behind a load balancer for example.
Host by host or as fast as possible
strategy of Ansible is
linear: Execute one task on targeted nodes, after that, continue with the next task:
TASK [Do step one] *** changed: [node_1] changed: [node_2] changed: [node_3] TASK [Do step two] *** changed: [node_1] changed: [node_2] changed: [node_3]
You can also instruct Ansible to “go as fast as possible” by setting the
free. This runs each task on targeted nodes, but does not wait until all hosts have completed a task. This also means there are limitations; when ordering of the tasks is important, this strategy can’t be used.
- name: Do something as fast as possible hosts: web_servers strategy: free tasks: - name: Update all software ansible.builtin.package: name: "*" state: latest notify: - Reboot handlers: - name: Reboot ansible.builtin.reboot:
The above sequence runs the jobs (
Update all software and
Reboot) as fast as possible; one node may be updating, the other can be rebooting, or even already be finished.
In a task-list it can be useful to group tasks. Likely because they share something. (A
when for example.) You can even group tasks because you like it, or if it increases readability.
- name: Do something to Debian when: - ansible_distribution == "Debian" notify: - Some Debian handler block: - name: Taks 1 ... - name: Task 2
The block (
Do something to Debian) shares a
when for both tasks.
- When the condition (
when) is met, two tasks will run.
- When any of the tasks is
changed, the handler
Some Debian handlerwill be called.
block statement can also be used to “catch” a possible failure.
- name: Do something hosts: web_servers become: no gather_facts: no tasks: - name: Start the webserver block: - name: Start httpd ansible.builtin.service: name: httpd state: started rescue: - name: Install httpd ansible.builtin.package: name: httpd always: - name: Show a message ansible.builtin.debug: msg: "Hello!"
Although the example above makes no sense, it does demonstrate how the
rescue statement works. “If starting httpd fails, install httpd.” (What’s incorrect here is that there can be many more reasons for httpd to fail, plus if it fails, httpd would be installed, but never started. Weird.
alwasy will run anyway, no matter what the first (
Start httpd) task does.
I’m not a big fan of
always, it increases complexity, can possible show red output (errors), which can throw of people. I would rather use some tasks sequence that first checks things, and based on the results, execute some action.
Wow, this is going to be a large chapter. Variables can be set and used in many ways in Ansible. Let’s cover a few edge-cases.
facts are details of a node, for example
ansible_distribution. These can be used anywhere to make desicions. A common way to use this:
- name: Do something on Debian systems ansible.builtin.debug: msg: "I am running ." when: - ansible_distribution == "Debian"
You can also map values to a distribution in
_package_to_install: Debian: x RedHat: - y - z package_to_install: ""
package_to_install variable can now be used in a regular task, for example:
- name: install package ansible.builtin.package: name: ""
You can also introduce a
_package_to_install: default: x Debian: y RedHat: z package_to_install: ""
This way of working allows the
tasks/main.yml to be readable.
Use a variable from another host
Image you have a couple of hosts and you’ve
register-ed something on
node-1 that you need on
- name: Set a variable on node-1 hosts: node-1 tasks: ansible.builtin.set_fact: some_variable: some_value - name: Get a variable from node-1 on node-2 hosts: node-2 tasks: ansible.builtin.debug: msg: "The value of some_variable on node-1 is: hostvars['node-1']['some_variable']
Sometimes you need a bit of complexity to finish a role or playbook. I would suggest to use this list, starting with “keep it simple”, going down to “hide your complexity here”.
default/main.yml(Or any other user-overwritable variable) - Keep this very simple. Users will interface with your code here.
handlers/*.yml- This can be a little more complex. Users are not expected to read this, some will peek here.
vars/main.yml- This is where complexity lives. Keep all your data, filters and lookups here when possible.
Ansible lint is always right.
Well, that being said, you can overrule ansible-lint by setting the ` # noqa` argument. (Mind you: space-space-hash-space-identifier)
- name: Update all software (yum) ansible.builtin.yum: name: "*" state: latest # noqa package-latest This role is to update packages.
Try to understand what Ansible lint is trying to tell you, likely you need to restructure your code until Ansible lint is happy.
- Optimize for readability.
- Spread things vertically when possible.