Intermediate

Ansible Playbooks, Variables, and Handlers

{% for host in groups['all'] %} Host: {{ host }} Hostname: {{ hostvars[host]['ansible_facts']['hostname'] }} OS: {{ hostvars[host]['ansible_facts']['distribution'] }} Memory: {{ hostvars[...

Table of Contents

  1. Introduction and training overview
  2. Building Advanced Playbooks
  1. Module 2: Ansible Facts and Variables
  1. Module 3: Handlers and Templates
  1. Ansible Vault
  1. Training summary
  2. Command cheat sheet

1. Training Introduction and Overview

This course focuses on creating smarter Ansible playbooks by adding logic and flexibility. It covers the following themes:

  • Conditionals and loops: control when and how tasks execute.
  • Variables and facts: adapt the playbooks to each managed host.
  • Templates: generate dynamic configuration files.
  • Handlers: manage changes reactively (e.g. restart a service only if the configuration has changed).
  • Ansible Vault: secure secrets and sensitive data.

At the end of this training, you will be able to write more powerful and dynamic Ansible playbooks.


2. Building Advanced Playbooks

2.1 Structure of an Ansible Playbook

Each Ansible playbook is built from a few key components that define what to do, where to do it, and how to do it.

ComponentDescription
PlaysHigh-level sections that associate tasks with target hosts.
TasksIndividual actions (install a package, edit a file, etc.).
VariablesMake playbooks flexible by avoiding hard-coding.
HandlersSpecialized tasks that run only when notified (e.g.: restarting a service after a config change).
RolesAllows you to organize tasks, variables, templates and other elements into reusable units.
TagsAllows you to control which parts of a playbook run — useful for targeting or ignoring specific tasks.

These components form the foundation of structure and automation with Ansible.

2.2 Components of a Play

Each play in a playbook contains four essential elements:

  1. Target hosts (hosts): the systems where the tasks will run.
  2. Tasks: the list of actions to perform on these systems.
  3. Variables: defined directly in the play or via external files.
  4. Execution options: privilege escalation (become), remote user (remote_user), collection of facts (gather_facts), etc.
---
- name: Exemple de play
  hosts: webservers
  become: true
  gather_facts: true
  vars:
    http_port: 80
  tasks:
    - name: Installer httpd
      dnf:
        name: httpd
        state: present

2.3 Tasks

tasks are the most basic unit of work in an Ansible playbook. Each task represents a unique action:

  • Install a package
  • Copy a file
  • Start a service

Task characteristics:

  • Defined with a hyphen (-) and a name (name), which is not required but improves readability.
  • Run sequentially in the exact order they are written.
  • Stop if a task fails, unless instructed otherwise.
tasks:
  - name: Installer httpd
    dnf:
      name: httpd
      state: present

  - name: Démarrer httpd
    service:
      name: httpd
      state: started
      enabled: true

2.4 The Roles

roles are a powerful way to organize playbooks. They allow you to break down tasks, variables, templates and handlers into a dedicated folder structure. A role is essentially a reusable directory containing everything related to a specific function (eg: configuring a web server or a database).

Using roles makes your playbooks clean, modular and reusable.

- name: Déploiement serveur web
  hosts: webservers
  roles:
    - httpd
    - firewall

2.5 Tags

tags give you control over which tasks the playbook should execute. This is particularly useful when you want to rerun only part of a playbook — for example, restarting a service or updating a configuration file without restarting the whole thing.

  • Assign a tag to any task with the keyword tags.
  • Selectively run tasks by tag with the --tags option in CLI.
  • Skip tasks with --skip-tags.
tasks:
  - name: Installer httpd
    dnf:
      name: httpd
      state: present
    tags: install

  - name: Créer un utilisateur
    user:
      name: webuser
      state: present
    tags: user

Running with tags:

# Exécuter seulement les tasks tagguées "user"
ansible-playbook playbook.yml --tags user

# Ignorer les tasks tagguées "user"
ansible-playbook playbook.yml --skip-tags user

2.6 Demo: Setting up the lab environment

Control node prerequisites

Before installing Ansible, the control node must meet the following minimum requirements:

  • RHEL 9.0 or newer
  • Python 3.8 or newer
  • Internet access to retrieve packets
  • A valid Red Hat subscription (even the free developer account)

Installation and configuration steps

1. Check current user and hosts

id
cat /etc/hosts

The /etc/hosts file must contain the IP entries for node1 and node2.

2. Register the system with Red Hat

subscription-manager register
# Saisir le nom d'utilisateur et mot de passe Red Hat
subscription-manager repos --list

3. Install Ansible on the controlling node

dnf list ansible-core
dnf install ansible-core

4. Register managed nodes (node1 and node2)

# Se connecter à node2 via SSH
ssh node2
sudo subscription-manager register
sudo dnf list httpd   # Valider l'accès au dépôt
exit

# Répéter pour node1
ssh node1
sudo subscription-manager register
sudo dnf list httpd
exit

5. Create the ansible user on all nodes

# Sur le nœud de contrôle
sudo useradd ansible
sudo passwd ansible  # Mot de passe : redhat

# Sur node1
ssh node1
sudo useradd ansible
sudo passwd ansible  # Mot de passe : redhat
exit

# Sur node2
ssh node2
sudo useradd ansible
sudo passwd ansible  # Mot de passe : redhat
exit

6. Configure SSH keys for passwordless authentication

# Se connecter en tant qu'utilisateur ansible
su - ansible
cd ~

# Générer la paire de clés SSH
ssh-keygen

# Copier la clé publique vers node1 et node2
ssh-copy-id node1  # Mot de passe : redhat
ssh-copy-id node2  # Mot de passe : redhat

# Valider la connexion sans mot de passe
ssh node1
exit
ssh node2
exit

7. Configure passwordless sudo for ansible user

# Sur node1
sudo vim /etc/sudoers.d/ansible

File contents:

ansible ALL=(ALL) NOPASSWD:ALL
# Sur node2
sudo vim /etc/sudoers.d/ansible

Same content:

ansible ALL=(ALL) NOPASSWD:ALL

Enable:

sudo systemctl restart crond  # Doit fonctionner sans demander de mot de passe

8. Create the inventory and validate the Ansible lab

mkdir ~/ansible
cd ~/ansible
vim inventory

Inventory contents:

node1
node2

Validate with ad hoc commands:

ansible node1 -i inventory -m ping
ansible node2 -i inventory -m ping

Expected result: pong for each node.


2.7 Demo: Playbooks with multiple Plays

Objective

Create a playbook containing multiple plays targeting different hosts, and use tags to control execution.

Setting up

mkdir plays
cd plays
vim inventory

Inventory contents:

node1
node2
vim ansible.cfg

Contents of the ansible.cfg file:

[defaults]
inventory = inventory

[privilege_escalation]
become = true
become_method = sudo
become_user = root

Creation of the playbook with several plays

vim multiple.yml
---
- name: Multiple plays
  hosts: node1
  tasks:
    - name: Install httpd on node1
      dnf:
        name: httpd
        state: present

    - name: Create user on node1
      user:
        name: node1user
        state: present
      tags: user

- name: Second play
  hosts: node2
  tasks:
    - name: Install httpd on node2
      dnf:
        name: httpd
        state: present

    - name: Create user on node2
      user:
        name: node2user
        state: present
      tags: user

Important: The indentation level of the second play is identical to the first play — the YAML structure must be respected.

Running the playbook

# Exécution normale (toutes les tasks)
ansible-playbook multiple.yml

# Exécuter seulement les tasks tagguées "user"
ansible-playbook multiple.yml --tags user

# Ignorer les tasks tagguées "user"
ansible-playbook multiple.yml --skip-tags user

Results:

  • With --tags user: only the “Create user on node1” and “Create user on node2” tasks are executed.
  • With --skip-tags user: only the tasks “Install httpd on node1” and “Install httpd on node2” are executed.

2.8 Conditionals and Loops (theory)

Conditionals (when)

Conditionals in Ansible allow you to control which tasks or plays must execute according to specific criteria. You can base these conditions on:

  • Playbook variables
  • Variables saved from previous tasks
  • Ansible facts collected from the target system

Conditions can evaluate strings, numbers, or Booleans, providing great flexibility for making automation context-aware.

Typical use cases:

  • Run tasks only on a specific OS type.
  • Skipping steps when a service is already running.
  • Change behavior depending on environment (dev, prod).

Examples of conditionals:

tasks:
  # Créer un utilisateur seulement si la variable new_user est définie
  - name: Créer l'utilisateur si défini
    user:
      name: "{{ new_user }}"
      state: present
    when: new_user is defined

  # Redémarrer nginx seulement si restart_nginx est true
  - name: Redémarrer nginx si nécessaire
    service:
      name: nginx
      state: restarted
    when: restart_nginx == true

Numerical comparison:

  # Afficher un avertissement si l'utilisation dépasse 80%
  - name: Vérifier l'utilisation du disque
    debug:
      msg: "AVERTISSEMENT : utilisation élevée du disque"
    when: disk_usage > 80

Checking list membership:

  # Ajouter l'utilisateur seulement s'il n'est pas dans la liste
  - name: Ajouter devuser si absent
    user:
      name: devuser
      state: present
    when: "'devuser' not in user_list"

Multiple conditions:

  # Appliquer la config seulement si environment=prod ET service_enabled=true
  - name: Appliquer la configuration de production
    template:
      src: prod.conf.j2
      dest: /etc/app/config.conf
    when:
      - environment == "prod"
      - service_enabled == true

Loops

Loops in Ansible allow you to repeat a task several times, each time with a different input. Instead of writing separate tasks for each element, we define the task once and pass it a list of values.

Advantages: – Cleaner, More Efficient Playbooks

  • Ability to use simple lists, dictionaries, or complex data structures

Loop use cases:

  • Installation of several packages: avoids repeating the same task for each package.
  • User creation: define a list of user names or attributes.
  • Starting/activating multiple services: useful for configuring a complete application stack.
  • Management of several files: copy config templates or define permissions.

Simple loop example:

- name: Créer plusieurs utilisateurs
  user:
    name: "{{ item }}"
    state: present
  loop:
    - alice
    - bob
    - charlie

Loop over a list of dictionaries:

- name: Créer des utilisateurs avec des groupes
  user:
    name: "{{ item['name'] }}"
    groups: "{{ item['groups'] }}"
    state: present
  loop:
    - { name: alice, groups: developers }
    - { name: bob, groups: operations }

Combination of loops and conditionals:

- name: Créer seulement les utilisateurs actifs
  user:
    name: "{{ item.name }}"
    state: present
  loop:
    - { name: alice, enabled: true }
    - { name: bob, enabled: false }
  when: item.enabled

In this example, only alice will be created because bob has enabled: false.


2.9 Demo: Conditionals and Loops

Setting up

mkdir conditionals
cd conditionals
vim inventory
node1
node2
vim ansible.cfg
[defaults]
inventory = inventory

[privilege_escalation]
become = true
become_method = sudo
become_user = root

Creating the playbook with conditionals and loops

vim conditionals.yml
---
- name: Using conditionals
  hosts: node1
  vars:
    create_user: true          # Booléen
    user_name: andrei           # Chaîne de caractères
    users:                      # Liste
      - ana
      - jim
      - guest
    user_count: 2               # Entier

  tasks:
    # Task 1 : Créer un utilisateur via un conditionnel
    - name: Create a user using a conditional
      user:
        name: "{{ user_name }}"
        state: present
      when: create_user

    # Task 2 : Créer des utilisateurs depuis une liste inline
    - name: Create users from a list
      user:
        name: "{{ item }}"
        state: present
      loop:
        - user1
        - user2
        - user3

    # Task 3 : Créer des utilisateurs depuis une variable liste en excluant "guest"
    - name: Create users from a list variable skipping the user guest
      user:
        name: "{{ item }}"
        state: present
      loop: "{{ users }}"
      when: item != "guest"

    # Task 4 : Comparaison d'entiers
    - name: Integer comparison
      debug:
        msg: "We are creating more than one user"
      when: user_count > 1

Execution and expected results

ansible-playbook conditionals.yml

Observed results:

  1. Task 1 (Create a user using a conditional): the user andrei is created because create_user = true.

  2. Task 2 (Create users from a list): three iterations — item successively takes the values ​​user1, user2, user3. Three users created.

  3. Task 3 (Create users from a list variable skipping the user guest): the users ana and jim are created, but guest is ignored thanks to the conditional when: item != "guest".

  4. Task 4 (Integer comparison): the message “We are creating more than one user” is displayed because user_count = 2 > 1.


3. Ansible Facts and Variables

3.1 Ansible Variables (theory)

Ansible variables are one of the most important mechanisms for making your playbooks dynamic and reusable.

What is an Ansible variable?

These are simply named values that you define once and reuse throughout your playbook. Instead of hard-coding values ​​like usernames, ports, or file paths, you define them as variables and reference them wherever needed.

Advantages:

  • Separate logic from data.
  • Playbooks easier to maintain and update.
  • A single value change propagates everywhere.

3.2 Variable precedence hierarchy

Variable precedence is fundamental to understand. Here is the order of priority, from lowest to highest:

PrioritySource of variablesDescription
1 (lowest)Group variables in inventoryDefined directly in the inventory file
2group_vars/ directoryVariable files by group
3Host variables in inventorySet directly in inventory per host
4host_vars/ directoryVariable files per host
5Host factsAutomatically collected by Ansible at runtime
6Play-level variables (vars:)Defined in playbook, override inventory values ​​
7Task level variablesDeclared in a specific task, very high precedence
8 (highest)Extra variables (-e)Placed on the command line, always win

Golden rule: Extra variables (-e) always have absolute priority.

# Exemple d'extra variable en ligne de commande
ansible-playbook playbook.yml -e "http_port=8080"

3.3 Defining variables in a Playbook

Inline variables in the vars section:

---
- name: Mon play
  hosts: webservers
  vars:
    username: alice
    home_dir: /home/alice
  tasks:
    - name: Créer l'utilisateur
      user:
        name: "{{ username }}"
        home: "{{ home_dir }}"
        state: present

External variable files via vars_files:

---
- name: Mon play avec variables externes
  hosts: webservers
  vars_files:
    - vars/common_users.yml
    - vars/app_config.yml
  tasks:
    - name: Utiliser une variable du fichier externe
      debug:
        msg: "Port HTTP : {{ http_port }}"

File vars/common_users.yml:

---
http_port: 80
app_name: myapp

Reference a variable in a task:

tasks:
  - name: Afficher le nom d'utilisateur
    debug:
      msg: "L'utilisateur est {{ username }}"

3.4 Variables in inventory

It is possible to define variables directly in the inventory file, but this is not recommended for complex configurations (better to use group_vars and host_vars).

# Variable spécifique à un hôte
demo.example.com ansible_user=joe

# Groupe avec variables de groupe
[servers]
web1.example.com
web2.example.com

[servers:vars]
user=admin

3.5 Group_vars and host_vars folders

Ansible automatically loads variables from the special group_vars/ and host_vars/ directories, matching them to the group and host names in your inventory.

Recommended structure:

projet/
├── ansible.cfg
├── inventory
├── group_vars/
│   └── webservers     # Variables pour le groupe "webservers"
├── host_vars/
│   ├── node1          # Variables spécifiques à node1
│   └── node2          # Variables spécifiques à node2
└── playbook.yml

File group_vars/webservers:

create_default_users: true
default_users:
  - name: alice
    uid: 1101
  - name: bob
    uid: 1102

File host_vars/node1:

host_users:
  - name: dev1
    uid: 2001

File host_vars/node2:

host_users:
  - name: ops1
    uid: 2002

3.6 Dictionaries and Magic Variables

Dictionaries (maps)

Dictionaries are a powerful way to group related values ​​under a single variable.

vars:
  user_info:
    name: alice
    uid: 1001
    shell: /bin/bash

Access to dictionary fields:

# Notation point
"{{ user_info.name }}"
"{{ user_info.uid }}"

# Notation crochets
"{{ user_info['name'] }}"
"{{ user_info['shell'] }}"

Dictionaries are particularly useful when using loops or templates.

Magic Variables

Magic variables are built-in variables automatically available when running a playbook:

VariesDescription
inventory_hostnameCurrent host name in inventory
group_namesList of groups to which the current host belongs
groupsDictionary of all groups and their member hosts
hostvarsDictionary of variables accessible by host
tasks:
  - name: Afficher le nom d'inventaire
    debug:
      msg: "Cet hôte s'appelle {{ inventory_hostname }}"

  - name: Afficher les groupes de cet hôte
    debug:
      msg: "Groupes : {{ group_names }}"

  - name: Accéder aux vars d'un autre hôte
    debug:
      msg: "IP de node1 : {{ hostvars['node1']['ansible_default_ipv4']['address'] }}"

3.7 Demo: Working with Variables

Project structure

mkdir -p group_vars host_vars vars

Final tree:

variables/
├── ansible.cfg
├── inventory
├── group_vars/
│   └── webservers
├── host_vars/
│   ├── node1
│   └── node2
├── vars/
│   └── common_users.yml
├── variables.yml
└── manage_users.yml

Inventory with group

vim inventory
[webservers]
node1
node2

Group variable files

vim group_vars/webservers
create_default_users: true
default_users:
  - name: alice
    uid: 1101
  - name: bob
    uid: 1102

Host variable files

vim host_vars/node1
host_users:
  - name: dev1
    uid: 2001
vim host_vars/node2
host_users:
  - name: ops1
    uid: 2002

External variables file

mkdir vars
vim vars/common_users.yml
---
user_list:
  - name: admin
    uid: 4001
    group: wheel
    create: true
  - name: guest
    uid: 4002
    group: wheel
    create: false

Variables Demo Playbook

vim variables.yml
---
- name: Show variables
  hosts: webservers
  vars_files:
    - vars/common_users.yml
  vars:
    play_var: "This was defined in the playbook"

  tasks:
    - name: Show group users
      debug:
        var: default_users

    - name: Show host users
      debug:
        var: host_users

    - name: Show conditional user list
      debug:
        var: user_list

    - name: Show play-level variable
      debug:
        var: play_var
ansible-playbook variables.yml

User Management Playbook with Variables

vim manage_users.yml
---
- name: Managing users with variables
  hosts: webservers
  vars_files:
    - vars/common_users.yml

  tasks:
    # Créer les utilisateurs de niveau groupe (alice et bob)
    - name: Create group-level users
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        state: present
      loop: "{{ default_users }}"
      when: create_default_users

    # Créer les utilisateurs spécifiques à chaque hôte
    - name: Create host-specific users
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        state: present
      loop: "{{ host_users }}"

    # Créer conditionnellement les utilisateurs de la liste
    - name: Conditionally create users from list
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.group }}"
        state: present
      loop: "{{ user_list }}"
      when: item.create
ansible-playbook manage_users.yml

Expected results:

  • On node1 and node2: users alice and bob are created (webservers group).
  • On node1: user dev1 is created (specific host_vars).
  • On node2: user ops1 is created (specific host_vars).
  • On both nodes: only admin is created from user_list (create: true) — guest is ignored (create: false).

3.8 Ansible Facts (theory)

What is Ansible Facts?

Ansible facts are information collected automatically from each managed host. They include system properties like:

  • IP Address
  • OS version
  • Number of CPUs
  • Amount of memory
  • Architecture (x86_64, arm64, etc.)
  • Network interfaces
  • And much more…

Key Features:

  • Collected at the start of execution of a playbook (unless disabled with gather_facts: false).
  • Ansible uses the setup module behind the scenes to collect them.
  • Once collected, they are stored as variables and usable in playbooks, conditionals or templates.
  • Provide real-time data about your infrastructure, allowing automation to dynamically adapt.

Access to facts

Facts can be accessed by dot notation or bracket notation:

# Notation crochets (recommandée)
ansible_facts['hostname']
ansible_facts['architecture']
ansible_facts['distribution']
ansible_facts['distribution_version']
ansible_facts['memtotal_mb']

# Exemples de valeurs retournées
# ansible_facts['architecture'] => 'x86_64'
# ansible_facts['distribution'] => 'RedHat'
# ansible_facts['memtotal_mb'] => 4096

Collect facts

Two main methods:

1. Ad hoc command with the setup module:

# Afficher tous les facts d'un hôte
ansible node1 -m setup

# Rediriger les facts vers un fichier pour exploration
ansible node1 -m setup > node1facts.txt

2. gather_facts option in the playbook:

---
- name: Mon play
  hosts: all
  gather_facts: true   # Activé par défaut
  tasks:
    # Les facts sont disponibles dans toutes les tasks
---
- name: Mon play sans facts
  hosts: all
  gather_facts: false  # Désactiver si non nécessaire (améliore les performances)
  tasks:
    # Attention : les variables ansible_facts ne seront PAS disponibles !

Warning: If gather_facts: false and your playbook uses ansible_facts variables, the playbook will fail immediately because these variables will be undefined.


3.9 Demo: Working with Facts

Explore facts with an ad hoc command

# Afficher tous les facts de node1
ansible node1 -m setup

# Sauvegarder les facts dans un fichier pour analyse
ansible node1 -m setup > node1facts.txt
vim node1facts.txt  # Explorer la structure parent-enfant

Create a playbook using facts

vim facts.yml
---
- name: Facts playbook
  hosts: node1
  gather_facts: true

  tasks:
    - name: Show hostname
      debug:
        msg: "Hostname is {{ ansible_facts['hostname'] }}"

    - name: Show OS
      debug:
        msg: "OS is {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"

    - name: Check memory
      debug:
        msg: "This system has more than 1GB of RAM"
      when: ansible_facts['memtotal_mb'] > 1000
ansible-playbook facts.yml

Results:

  • The hostname is displayed.
  • The distribution and its version are displayed.
  • RAM message is displayed if the memory is greater than 1000 MB.

Demonstration of the effect of gather_facts: false

---
- name: Facts playbook (sans facts)
  hosts: node1
  gather_facts: false   # Désactivé !

  tasks:
    - name: Show hostname
      debug:
        msg: "Hostname is {{ ansible_facts['hostname'] }}"  # ERREUR !
ansible-playbook facts.yml

Result: The playbook fails from the first task because ansible_facts['hostname'] is undefined. This illustrates the importance of keeping gather_facts: true (the default) when facts variables are used.


4. Handlers and Templates

4.1 Ansible Handlers (theory)

What is a Handler?

handlers in Ansible are special tasks designed to respond only when triggered. They are notified by other tasks, typically when these tasks result in a change (eg: update of a configuration file).

Operation:

  1. A task modifies something and reports a changed status.
  2. The task notifies a handler via the notify directive.
  3. The handler is put in queue (no immediate execution).
  4. After all tasks in the play, the notified handlers execute.

Full example

---
- name: Configure Apache
  hosts: webservers
  tasks:
    - name: Deploy httpd configuration
      template:
        src: httpd.conf.j2
        dest: /etc/httpd/conf/httpd.conf
      notify: Restart apache   # Notifie le handler seulement si changed

  handlers:
    - name: Restart apache     # Même nom que dans notify
      service:
        name: httpd
        state: restarted

Key Features of Handlers

CharacteristicDescription
Conditional triggerA handler only runs if a task reports changed. If nothing changes, the handler does not rotate.
Uniqueness of executionEven if multiple tasks notify the same handler, it only executes once per play.
Unique nameEach handler must have a unique name to be referenced by notify.
PositioningDefined separately, at the bottom of the playbook or inside a role.
Delayed executionAll notified handlers run after all tasks in the play.

Ideal use case: Reboot or configuration reload operations that should only occur when necessary.


4.2 Demo: Using Handlers

Creation of the playbook with a handler

vim handlers.yml
---
- name: handlers
  hosts: node1

  tasks:
    - name: Install httpd
      dnf:
        name: httpd
        state: latest

    - name: Start httpd
      service:
        name: httpd
        state: started

    - name: Modify config
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        line: "# Managed by ansible"
        insertafter: EOF
      notify: restart httpd   # Notifie le handler si le fichier change

  handlers:
    - name: restart httpd     # Même niveau d'indentation que tasks
      service:
        name: httpd
        state: restarted

Indentation note: The handlers: section has the same indentation level as the tasks: section.

Execution and behavior

First run:

ansible-playbook handlers.yml

Result:

  • httpd is installed (changed or ok if already present).
  • httpd is started.
  • The line is added to the configuration file (changed).
  • The restart httpd handler is notified and executed at the end of play.

Second execution:

ansible-playbook handlers.yml

Result:

  • All tasks return ok (green) — the desired state has already been reached.
  • The handler is NOT running because no task reported changed.

This illustrates Ansible’s idempotence: rerunning the playbook does not cause unwanted side effects.


4.3 Ansible Templates (theory)

What is an Ansible Template?

Ansible templates are simple text files that can include Jinja2 syntax. This allows you to embed variables, use conditionals, or even create loops inside your configuration files.

  • Deployed via the template module in Ansible.
  • Allow dynamic content generation.
  • Conventional extension: .j2 (for Jinja2).

Why use templates?

Resolved issue: Personalization.

Use casesDescription
Custom configurations per serverEach server has its own configurations: hostnames, IP addresses, open ports. Templates generate these files dynamically based on variables and facts.
Multi-environmentsA dev server can use a lightweight database, while the prod server connects to a hardened external database. Same base file, different injected values.
ReusabilityWrite the template once, reuse it across environments by injecting host-specific data.

4.4 Jinja2 Syntax in Templates

Variables

# Injecter une variable
Server: {{ inventory_hostname }}
Hostname: {{ ansible_facts.hostname }}
OS: {{ ansible_facts.distribution }}

Loops (for)

# Itérer sur une liste d'utilisateurs
{% for user in users %}
{{ user.name }} - {{ user.uid }}
{% endfor %}

# Itérer sur les interfaces réseau
{% for iface in ansible_facts.interfaces %}
Interface: {{ iface }}
{% endfor %}

Conditionals (if)

# Afficher un avertissement si la mémoire est insuffisante
{% if ansible_facts.memtotal_mb < memory_limit %}
WARNING: Not enough memory!
{% endif %}

# Inclure SSL seulement si activé
{% if enable_ssl %}
SSLCertificateFile /etc/ssl/cert.pem
SSLCertificateKeyFile /etc/ssl/key.pem
{% endif %}

Access to hostvars (magic variable)

# Accéder aux infos de tous les hôtes de l'inventaire
{% for host in groups['all'] %}
Host: {{ host }}
Hostname: {{ hostvars[host]['ansible_facts']['hostname'] }}
OS: {{ hostvars[host]['ansible_facts']['distribution'] }}
Memory: {{ hostvars[host]['ansible_facts']['memtotal_mb'] }} MB
{% endfor %}

4.5 Demo: Using Ansible Templates

Creation of the template file

vim mytemplate.j2
Host Inventory
==============

## Current Host Summary

Host: {{ inventory_hostname }}
Hostname: {{ ansible_facts.hostname }}
OS: {{ ansible_facts.distribution }}
Memory: {{ ansible_facts.memtotal_mb }} MB
{% if ansible_facts.memtotal_mb < memory_limit %}
WARNING: Not enough memory!
{% endif %}

Interfaces:
{% for iface in ansible_facts.interfaces %}
  - {{ iface }}
{% endfor %}

## Summary for all hosts

{% for host in groups['all'] %}
Host: {{ host }}
Hostname: {{ hostvars[host]['ansible_facts']['hostname'] }}
OS: {{ hostvars[host]['ansible_facts']['distribution'] }}
Memory: {{ hostvars[host]['ansible_facts']['memtotal_mb'] }} MB
Interfaces:
{% for iface in hostvars[host]['ansible_facts']['interfaces'] %}
  - {{ iface }}
{% endfor %}
{% endfor %}

Note: Using groups['all'] instead of group['all'] is important — a typo results in an Ansible error.

Creation of the deployment playbook

vim deploy.yml
---
- name: Deploy template
  hosts: all
  vars:
    memory_limit: 30000   # Limite en MB pour le check mémoire

  tasks:
    - name: Deploy host inventory report
      template:
        src: mytemplate.j2
        dest: "/home/ansible/{{ inventory_hostname }}_report.txt"

Execution

ansible-playbook deploy.yml

Checking on managed nodes

ssh node1
ls /home/ansible/
cat /home/ansible/node1_report.txt

Contents of the file generated on node1:

  • Information on the current host (hostname, OS, memory).
  • Warning message if memory is below limit.
  • List of network interfaces.
  • Information about all hosts in inventory.

Key points:

  • The template module deploys the .j2 file replacing all variables/conditionals/loops with their actual values.
  • Each host receives a custom file with {{ inventory_hostname }} in the destination file name.
  • Errors in templates (e.g. variable not defined) cause task to fail — Ansible cannot interpret the template.

5. Ansible Vault

5.1 Ansible Vault (theory)

Why use Ansible Vault?

Working with sensitive data is inevitable in automation:

  • API Tokens
  • Private SSH keys
  • Database identifiers
  • Application Passwords

Problem: Clear coding these values ​​in playbooks or variable files is a major security issue, especially in a team environment or when the code is versioned in Git.

Solution: Ansible Vault encrypts data to maintain security and automatically decrypts it when it is to be used by playbooks.

Advantages:

  • Mitigation of security risks, particularly in teams.
  • Secrets remain encrypted in code repositories.
  • Transparent decryption when running playbook.

5.2 Ansible Vault Commands

OrderDescription
ansible-vault create <file>Create a new encrypted file
ansible-vault encrypt <file>Encrypt an existing file in clear text
ansible-vault decrypt <file>Decrypt a file to its readable contents
ansible-vault view <file>View the contents of an encrypted file without decrypting it on disk
ansible-vault edit <file>Modify the contents of an encrypted file
ansible-vault rekey <file>Change the password of an encrypted file

Use a vault file in a playbook

Interactive option (requires password at runtime):

ansible-playbook playbook.yml --ask-vault-pass

Password file option (useful for CI/CD):

ansible-playbook playbook.yml --vault-password-file vault.key

Option with multiple vault IDs:

ansible-playbook playbook.yml \
  --vault-id prod@prompt \
  --vault-id dev@/path/to/dev.key

vault-id allows using vault files with different passwords in the same playbook.


5.3 Demo: Securing data with Ansible Vault

Create vault file

ansible-vault create secrets.yml
# Saisir le mot de passe : redhat
# Confirmer le mot de passe : redhat

File contents (vim editor opens automatically):

my_secret: "vault secret"

Save and exit — the file is now encrypted.

Create a playbook using the vault

vim secret.yml
---
- name: Show secret
  hosts: node1
  vars_files:
    - secrets.yml   # Fichier vault chiffré

  tasks:
    - name: Show the secret value
      debug:
        var: my_secret

Run playbook with vault

ansible-playbook secret.yml --ask-vault-pass
# Saisir le mot de passe : redhat

Result: The decrypted value vault secret is displayed.

Edit a vault file

ansible-vault edit secrets.yml
# Saisir le mot de passe pour déchiffrer
# Modifier le contenu, sauvegarder — le fichier est re-chiffré automatiquement

Change the password of a vault file

ansible-vault rekey secrets.yml
# Saisir l'ancien mot de passe : redhat
# Saisir le nouveau mot de passe : test
# Confirmer : test

Use password file (for automation)

# Créer le fichier de mot de passe
echo "test" > vault.key

# Sécuriser les permissions (lecture propriétaire uniquement)
chmod 600 vault.key

# Exécuter le playbook en lisant le mot de passe depuis le fichier
ansible-playbook secret.yml --vault-password-file vault.key

Best practice: Add vault.key to your .gitignore to never commit the password file!

Using multiple vaults with different passwords

Create two vault files with different passwords:

# Premier vault (mot de passe : redhat)
ansible-vault create first_secret.yml
first_secret: "this is my first secret"
# Deuxième vault (mot de passe : redhat1)
ansible-vault create second_secret.yml
second_secret: "this is my second secret"

Playbook using both vaults:

vim 2keys.yml
---
- name: Test with 2 keys
  hosts: node1
  vars_files:
    - first_secret.yml
    - second_secret.yml

  tasks:
    - name: Show the first secret
      debug:
        var: first_secret

    - name: Show the second secret
      debug:
        var: second_secret

Execution (requires both passwords):

ansible-playbook 2keys.yml --ask-vault-pass --ask-vault-pass
# Ou plus explicitement :
ansible-playbook 2keys.yml \
  --vault-id first@prompt \
  --vault-id second@prompt

Result: Both decrypted values ​​are displayed correctly.


6. Training summary

This training covered the fundamental elements for writing more powerful, dynamic and secure Ansible playbooks.

What you learned

Module 1: Building Advanced Playbooks

  • Structure of playbooks: plays, tasks, roles, tags.
  • Tags: control which parts of a playbook run with --tags and --skip-tags.
  • Conditional (when): execute tasks contextually according to variables, facts or results of previous tasks.
  • Loops: repeat tasks on lists of elements to write more concise and scalable playbooks.

Module 2: Ansible Facts and Variables

  • Variables: names, values, types (boolean, string, integer, list, dictionary).
  • Precedence hierarchy: understand which variable “wins” depending on its origin.
  • group_vars and host_vars: organize variables by group or by host.
  • Facts: system data automatically collected by the setup module, usable as variables in playbooks.

Module 3: Handlers and Templates

  • Handlers: tasks triggered only on change, executed only once at the end of play — ideal for service restarts.
  • Jinja2 Templates: dynamic configuration files integrating variables, loops and conditionals to adapt the content to each host.

Module 4: Ansible Vault

  • Encryption of secrets: protect sensitive data (passwords, tokens, keys).
  • Vault commands: create, encrypt, decrypt, view, edit, rekey.
  • Integration into playbooks: via vars_files with --ask-vault-pass or --vault-password-file.

Skills acquired

With all of these tools combined, you are now equipped to write Ansible playbooks:

  • Smarter thanks to conditionals and loops.
  • More flexible thanks to variables and facts.
  • More dynamic thanks to Jinja2 templates.
  • More efficient thanks to handlers.
  • More secure thanks to Ansible Vault.

7. Command cheat sheet

Basic Ansible commands

# Vérifier la version d'Ansible
ansible --version

# Commande ad hoc - ping
ansible all -i inventory -m ping

# Commande ad hoc - setup (facts)
ansible node1 -i inventory -m setup

# Exécuter un playbook
ansible-playbook playbook.yml

# Exécuter avec tags
ansible-playbook playbook.yml --tags "install,config"

# Ignorer des tags
ansible-playbook playbook.yml --skip-tags "user"

# Passer une extra variable
ansible-playbook playbook.yml -e "variable=valeur"

# Vérifier la syntaxe sans exécuter
ansible-playbook playbook.yml --syntax-check

# Simulation (dry-run)
ansible-playbook playbook.yml --check

Ansible Vault Commands

# Créer un fichier vault
ansible-vault create secrets.yml

# Chiffrer un fichier existant
ansible-vault encrypt fichier.yml

# Déchiffrer un fichier
ansible-vault decrypt fichier.yml

# Visualiser le contenu sans déchiffrer sur disque
ansible-vault view secrets.yml

# Éditer un fichier vault
ansible-vault edit secrets.yml

# Changer le mot de passe
ansible-vault rekey secrets.yml

# Exécuter avec mot de passe interactif
ansible-playbook playbook.yml --ask-vault-pass

# Exécuter avec fichier de mot de passe
ansible-playbook playbook.yml --vault-password-file vault.key

# Exécuter avec plusieurs vault IDs
ansible-playbook playbook.yml --vault-id prod@prompt --vault-id dev@dev.key

SSH configuration (initial setup)

# Générer une paire de clés SSH
ssh-keygen

# Copier la clé publique vers un hôte
ssh-copy-id <user>@<hostname>

# Configurer sudo sans mot de passe
echo "ansible ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible
chmod 440 /etc/sudoers.d/ansible

Ansible installation on RHEL

# Enregistrer le système
subscription-manager register

# Lister les dépôts disponibles
subscription-manager repos --list

# Vérifier la disponibilité du paquet
dnf list ansible-core

# Installer Ansible
dnf install ansible-core
mon_projet/
├── ansible.cfg              # Configuration Ansible
├── inventory                # Inventaire des hôtes
├── site.yml                 # Playbook principal
├── group_vars/
│   ├── all                  # Variables pour tous les hôtes
│   └── webservers           # Variables pour le groupe webservers
├── host_vars/
│   ├── node1                # Variables spécifiques à node1
│   └── node2                # Variables spécifiques à node2
├── vars/
│   └── common.yml           # Variables partagées
├── templates/
│   └── myconfig.j2          # Templates Jinja2
├── vault/
│   └── secrets.yml          # Fichiers vault chiffrés
└── roles/
    └── webserver/           # Exemple de role
        ├── tasks/
        ├── handlers/
        ├── templates/
        └── vars/

Typical ansible.cfg structure

[defaults]
inventory = inventory
remote_user = ansible
host_key_checking = False

[privilege_escalation]
become = true
become_method = sudo
become_user = root

Typical inventory structure

# Hôtes individuels
node1
node2

# Groupe avec hôtes
[webservers]
web1.example.com
web2.example.com

[dbservers]
db1.example.com

# Groupe de groupes
[production:children]
webservers
dbservers

# Variables de groupe inline (non recommandé, préférer group_vars/)
[webservers:vars]
http_port=80

Search Terms

ansible · playbooks · variables · handlers · infrastructure · ci/cd · devops · playbook · vault · facts · templates · theory · conditionals · loops · creation · variable · commands · execution · inventory · magic · setting · access · command · configuration

Interested in this course?

Contact us to book it or get a custom training plan for your team.