Ir para o conteúdo

Ansible Intermediate

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.

The variables

Note

More information can be found here.

Under Ansible, there are different types of primitive variables:

  • strings,
  • integers,
  • booleans.

These variables can be organized as:

  • dictionaries,
  • lists.

A variable can be defined in different places, like in a playbook, in a role or from the command line for example.

For example, from a playbook:

---
- hosts: apache1
  vars:
    port_http: 80
    service:
      debian: apache2
      rhel: httpd

or from the command line:

$ ansible-playbook deploy-http.yml --extra-vars "service=httpd"

Once defined, a variable can be used by calling it between double braces:

  • {{ port_http }} for a simple value,
  • {{ service['rhel'] }} or {{ service.rhel }} for a dictionary.

For example:

- name: make sure apache is started
  ansible.builtin.systemd:
    name: "{{ service['rhel'] }}"
    state: started

Of course, it is also possible to access the global variables (the facts) of Ansible (OS type, IP addresses, VM name, etc.).

Outsourcing variables

Variables can be included in a file external to the playbook, in which case this file must be defined in the playbook with the vars_files directive:

---
- hosts: apache1
  vars_files:
    - myvariables.yml

The myvariables.yml file:

---
port_http: 80
ansible.builtin.systemd::
  debian: apache2
  rhel: httpd

It can also be added dynamically with the use of the module include_vars:

- name: Include secrets.
  ansible.builtin.include_vars:
    file: vault.yml

Display a variable

To display a variable, you have to activate the debug module as follows:

- ansible.builtin.debug:
    var: service['debian']

You can also use the variable inside a text:

- ansible.builtin.debug:
    msg: "Print a variable in a message : {{ service['debian'] }}"

Save the return of a task

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.

Exercises

  • Write a playbook play-vars.yml that prints the distribution name of the target with its major version, using global variables.

  • Write a playbook using the following dictionary to display the services that will be installed:

service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server

The default type should be "web".

  • Override the type variable using the command line

  • Externalize variables in a vars.yml file

Loop management

With the help of loop, you can iterate a task over a list, a hash, or dictionary for example.

Note

More information can be found here.

Simple example of use, creation of 4 users:

- name: add users
  user:
    name: "{{ item }}"
    state: present
    groups: "users"
  loop:
     - antoine
     - patrick
     - steven
     - xavier

At each iteration of the loop, the value of the list used is stored in the item variable, accessible in the loop code.

Of course, a list can be defined in an external file:

users:
  - antoine
  - patrick
  - steven
  - xavier

and be used inside the task like this (after having include the vars file):

- name: add users
  user:
    name: "{{ item }}"
    state: present
    groups: "users"
  loop: "{{ users }}"

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:

---
- hosts: rocky8
  become: true
  become_user: root
  vars:
    users:
      antoine:
        group: users
        state: present
      steven:
        group: users
        state: absent

  tasks:

  - name: Manage users
    user:
      name: "{{ item.key }}"
      group: "{{ item.value.group }}"
      state: "{{ item.value.state }}"
    loop: "{{ users | dict2items }}"

Note

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.

Exercises

  • 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 a list with the help of the jinja filter list as this:

{{ service.values() | list }}

Conditionals

Note

More information can be found here.

The when statement is very useful in many cases: not performing certain actions on certain types of servers, if a file or a user does not exist, etc.

Note

Behind the when statement the variables do not need double braces (they are in fact Jinja2 expressions...).

- name: "Reboot only Debian servers"
  reboot:
  when: ansible_os_family == "Debian"

Conditions can be grouped with parentheses:

- 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:

- name: check if directory exists
  stat:
    path: /home/ansible
  register: directory

- ansible.builtin.debug:
    var: directory

- ansible.builtin.debug:
    msg: The directory exists
  when:
    - directory.stat.exists
    - directory.stat.isdir

You can also test that it is not true:

  when:
    - file.stat.exists
    - not file.stat.isdir

You will probably have to test that a variable exists to avoid execution errors:

  when: myboolean is defined and myboolean

Exercises

  • Print the value of service.web only when type equals to web.

Managing changes: the handlers

Note

More information can be found here.

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.

Handlers

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.

- name: template configuration file
  template:
    src: template-site.j2
    dest: /etc/httpd/sites-availables/test-site.conf
  notify:
     - restart memcached
     - restart httpd

A handler is a kind of task referenced by a unique global name:

  • It is activated by one or more notifiers.
  • It does not start immediately, but waits until all tasks are complete to run.

Example of handlers:

handlers:

  - name: restart memcached
    systemd:
      name: memcached
      state: restarted

  - name: restart httpd
    systemd:
      name: httpd
      state: restarted

Since version 2.2 of Ansible, handlers can listen directly as well:

handlers:

  - name: restart memcached
    systemd:
      name: memcached
      state: restarted
    listen: "web services restart"

  - name: restart apache
    systemd:
      name: apache
      state: restarted
    listen: "web services restart"

tasks:
    - name: restart everything
      command: echo "this task will restart the web services"
      notify: "web services restart"

Asynchronous tasks

Note

More information can be found here.

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.

Exercise results

  • Write a playbook play-vars.yml that print the distribution name of the target with its major version, using global variables.
---
- hosts: ansible_clients

  tasks:

    - name: Print globales variables
      debug:
        msg: "The distribution is {{ ansible_distribution }} version {{ ansible_distribution_major_version }}"
$ ansible-playbook play-vars.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print globales variables] ************************************************************************
ok: [192.168.1.11] => {
    "msg": "The distribution is Rocky version 8"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
  • Write a playbook using the following dictionary to display the services that will be installed:
service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server

The default type should be "web".

---
- 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   
  • Externalize variables in a vars.yml file
type: web
service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server
---
- 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