In this chapter you will continue to learn how to work with Ansible.
Objectives: In this chapter you will learn how to:
work with variables; use loops; manage state changes and react to them; manage asynchronous tasks.
ansible, module, playbook
Knowledge: Complexity:
Reading time: 30 minutes
In the previous chapter, you learned how to install Ansible, use it on the command line, or how to write playbooks to promote the re-usability of your code.
In this chapter, we can start to discover some more advanced notions of how to use Ansible, and discover some interesting tasks that you will use very regularly.
To save the return of a task and to be able to access it later, you have to use the keyword register inside the task itself.
Use of a stored variable:
- name: /home content
shell: ls /home
register: homes
- name: Print the first directory name
ansible.builtin.debug:
var: homes.stdout_lines[0]
- name: Print the first directory name
ansible.builtin.debug:
var: homes.stdout_lines[1]
Note
The variable homes.stdout_lines is a list of variables of type string, a way to organize variables that we had not yet encountered.
The strings that make up the stored variable can be accessed via the stdout value (which allows you to do things like homes.stdout.find("core") != -1), to exploit them using a loop (see loop), or simply by their indices as seen in the previous example.
We can use the example seen during the study of stored variables to improve it. Use of a stored variable:
- name: /home content
shell: ls /home
register: homes
- name: Print the directories name
ansible.builtin.debug:
msg: "Directory => {{ item }}"
loop: "{{ homes.stdout_lines }}"
A dictionary can also be used in a loop.
In this case, you will have to transform the dictionary into an item with what is called a jinja filter (jinja is the templating engine used by Ansible): | dict2items.
In the loop, it becomes possible to use item.key which corresponds to the dictionary key, and item.value which corresponds to the values of the key.
Let's see this through a concrete example, showing the management of the system users:
Many things can be done with the loops. You will discover the possibilities offered by loops when your use of Ansible pushes you to use them in a more complex way.
- name: "Reboot only CentOS version 6 and Debian version 7"
reboot:
when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or
(ansible_distribution == "Debian" and ansible_distribution_major_version == "7")
The conditions corresponding to a logical AND can be provided as a list:
- name: "Reboot only CentOS version 6"
reboot:
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "6"
You can test the value of a boolean and verify that it is true:
Handlers allow to launch operations, like restarting a service, when changes occur.
A module, being idempotent, a playbook can detect that there has been a significant change on a remote system, and thus trigger an operation in reaction to this change. A notification is sent at the end of a playbook task block, and the reaction operation will be triggered only once even if several tasks send the same notification.
For example, several tasks may indicate that the httpd service needs to be restarted due to a change in its configuration files. But the service will only be restarted once to avoid multiple unnecessary starts.
By default, SSH connections to hosts remain open during the execution of various playbook tasks on all nodes.
This can cause some problems, especially:
if the execution time of the task is longer than the SSH connection timeout
if the connection is interrupted during the action (server reboot for example)
In this case, you will have to switch to asynchronous mode and specify a maximum execution time as well as the frequency (by default 10s) with which you will check the host status.
By specifying a poll value of 0, Ansible will execute the task and continue without worrying about the result.
Here's an example using asynchronous tasks, which allows you to restart a server and wait for port 22 to be reachable again:
# Wait 2s and launch the reboot
- name: Reboot system
shell: sleep 2 && shutdown -r now "Ansible reboot triggered"
async: 1
poll: 0
ignore_errors: true
become: true
changed_when: False
# Wait the server is available
- name: Waiting for server to restart (10 mins max)
wait_for:
host: "{{ inventory_hostname }}"
port: 22
delay: 30
state: started
timeout: 600
delegate_to: localhost
You can also decide to launch a long-running task and forget it (fire and forget) because the execution does not matter in the playbook.
---
- hosts: ansible_clients
vars:
type: web
service:
web:
name: apache
rpm: httpd
db:
name: mariadb
rpm: mariadb-server
tasks:
- name: Print a specific entry of a dictionary
debug:
msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a specific entry of a dictionnaire] ********************************************************
ok: [192.168.1.11] => {
"msg": "The apache will be installed with the packages httpd"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Override the type variable using the command line:
ansible-playbook --extra-vars "type=db" display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a specific entry of a dictionary] ********************************************************
ok: [192.168.1.11] => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a specific entry of a dictionary
debug:
msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
Display the content of the service variable from the previous exercise using a loop.
Note
You will have to transform your service variable, which is a dictionary, to an item or a list with the help of the jinja filters dict2items or list as this:
{{ service | dict2items }}
{{ service.values() | list }}
With dict2items:
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable with a loop
debug:
msg: "{{item.key }} | The {{ item.value.name }} will be installed with the packages {{ item.value.rpm }}"
loop: "{{ service | dict2items }}"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'key': 'web', 'value': {'name': 'apache', 'rpm': 'httpd'}}) => {
"msg": "web | The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'key': 'db', 'value': {'name': 'mariadb', 'rpm': 'mariadb-server'}}) => {
"msg": "db | The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With list:
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable with a loop
debug:
msg: "The {{ item.name }} will be installed with the packages {{ item.rpm }}"
loop: "{{ service.values() | list}}"
~
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'name': 'apache', 'rpm': 'httpd'}) => {
"msg": "The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'name': 'mariadb', 'rpm': 'mariadb-server'}) => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Print the value of service.web only when type equals to web.
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable
debug:
msg: "The {{ service.web.name }} will be installed with the packages {{ service.web.rpm }}"
when: type == "web"
- name: Print a dictionary variable
debug:
msg: "The {{ service.db.name }} will be installed with the packages {{ service.db.rpm }}"
when: type == "db"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
"msg": "The apache will be installed with the packages httpd"
}
TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
$ ansible-playbook --extra-vars "type=db" display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0