Table of Contents
- 2.1 Structure of an Ansible Playbook
- 2.2 Components of a Play
- 2.3 Tasks
- 2.4 Les Roles
- 2.5 Tags
- 2.6 Demo: Setting up the lab environment
- 2.7 Demo: Playbooks with multiple plays
- 2.8 Conditionals and Loops (theory)
- 2.9 Demo: Conditionals and Loops
- 3.1 Ansible Variables (theory)
- 3.2 Variable precedence hierarchy
- 3.3 Define variables in a playbook
- 3.4 Variables in inventory
- 3.5 Group_vars and host_vars folders
- 3.6 Dictionaries and Magic Variables
- 3.7 Demo: Working with Variables
- 3.8 Ansible Facts (theory)
- 3.9 Demo: Working with Facts
- 4.1 Ansible Handlers (theory)
- 4.2 Demo: Using Handlers
- 4.3 Ansible Templates (theory)
- 4.4 Jinja2 syntax in the Templates
- 4.5 Demo: Using Ansible Templates
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.
| Component | Description |
|---|---|
| Plays | High-level sections that associate tasks with target hosts. |
| Tasks | Individual actions (install a package, edit a file, etc.). |
| Variables | Make playbooks flexible by avoiding hard-coding. |
| Handlers | Specialized tasks that run only when notified (e.g.: restarting a service after a config change). |
| Roles | Allows you to organize tasks, variables, templates and other elements into reusable units. |
| Tags | Allows 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:
- Target hosts (
hosts): the systems where the tasks will run. - Tasks: the list of actions to perform on these systems.
- Variables: defined directly in the play or via external files.
- 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
--tagsoption 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:
-
Task 1 (
Create a user using a conditional): the userandreiis created becausecreate_user = true. -
Task 2 (
Create users from a list): three iterations —itemsuccessively takes the values user1,user2,user3. Three users created. -
Task 3 (
Create users from a list variable skipping the user guest): the usersanaandjimare created, butguestis ignored thanks to the conditionalwhen: item != "guest". -
Task 4 (
Integer comparison): the message “We are creating more than one user” is displayed becauseuser_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:
| Priority | Source of variables | Description |
|---|---|---|
| 1 (lowest) | Group variables in inventory | Defined directly in the inventory file |
| 2 | group_vars/ directory | Variable files by group |
| 3 | Host variables in inventory | Set directly in inventory per host |
| 4 | host_vars/ directory | Variable files per host |
| 5 | Host facts | Automatically collected by Ansible at runtime |
| 6 | Play-level variables (vars:) | Defined in playbook, override inventory values |
| 7 | Task level variables | Declared 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:
| Varies | Description |
|---|---|
inventory_hostname | Current host name in inventory |
group_names | List of groups to which the current host belongs |
groups | Dictionary of all groups and their member hosts |
hostvars | Dictionary 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
aliceandbobare created (webserversgroup). - On node1: user
dev1is created (specific host_vars). - On node2: user
ops1is created (specific host_vars). - On both nodes: only
adminis created fromuser_list(create: true) —guestis 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
setupmodule 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: falseand your playbook usesansible_factsvariables, 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:
- A task modifies something and reports a
changedstatus. - The task notifies a handler via the
notifydirective. - The handler is put in queue (no immediate execution).
- 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
| Characteristic | Description |
|---|---|
| Conditional trigger | A handler only runs if a task reports changed. If nothing changes, the handler does not rotate. |
| Uniqueness of execution | Even if multiple tasks notify the same handler, it only executes once per play. |
| Unique name | Each handler must have a unique name to be referenced by notify. |
| Positioning | Defined separately, at the bottom of the playbook or inside a role. |
| Delayed execution | All 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 thetasks:section.
Execution and behavior
First run:
ansible-playbook handlers.yml
Result:
- httpd is installed (
changedorokif already present). - httpd is started.
- The line is added to the configuration file (
changed). - The
restart httpdhandler 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
templatemodule in Ansible. - Allow dynamic content generation.
- Conventional extension:
.j2(for Jinja2).
Why use templates?
Resolved issue: Personalization.
| Use cases | Description |
|---|---|
| Custom configurations per server | Each server has its own configurations: hostnames, IP addresses, open ports. Templates generate these files dynamically based on variables and facts. |
| Multi-environments | A dev server can use a lightweight database, while the prod server connects to a hardened external database. Same base file, different injected values. |
| Reusability | Write 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 ofgroup['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
templatemodule deploys the.j2file 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
| Order | Description |
|---|---|
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-idallows 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.keyto your.gitignoreto 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
--tagsand--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
setupmodule, 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_fileswith--ask-vault-passor--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
Recommended project structure
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