Aller au contenu

Les filtres Jinja avancés

Mise à jour :

logo ansible

Dans ce guide, nous allons explorer l’utilisation des filtres Jinja dans Ansible, qui sont essentiels pour manipuler, transformer et valider les variables dans vos playbooks et templates. Ces filtres permettent d’adapter les données selon vos besoins, que ce soit pour formater des chaînes de caractères, gérer des listes, ou sécuriser des informations sensibles. Grâce à eux, vous pouvez gérer les configurations dynamiques de manière plus flexible et efficace.

Dans notre précédent guide sur les templates Jinja, nous avons vu comment utiliser des concepts comme les inclusions, les conditions et les boucles dans Ansible. Ici, nous allons approfondir l’utilisation des filtres, afin de vous montrer comment optimiser le traitement des données dans vos playbooks. Qu’il s’agisse de gérer des valeurs par défaut, d’utiliser des filtres conditionnels ou de transformer des données complexes, les filtres Jinja offrent une large palette d’outils pour rendre vos configurations Ansible plus puissantes.

Pourquoi les filtres sont-ils importants ?

Les filtres Jinja permettent non seulement de formater et transformer des données, mais aussi de s’assurer que celles-ci sont correctement manipulées avant d’être utilisées dans vos tâches Ansible. Par exemple, ils vous permettent de valider des valeurs, de convertir des types, ou encore de combiner des données issues de différentes sources. Ils sont essentiels pour éviter les erreurs liées aux variables non définies ou mal configurées.

Gestion des valeurs non définies

Dans vos playbooks Ansible, il arrive souvent que certaines variables ne soient pas définies ou ne contiennent aucune valeur. Cela peut entraîner des erreurs lors de l’exécution, comme des échecs liés à des variables manquantes ou des comportements inattendus. Heureusement, les filtres Jinja offrent des moyens simples de gérer ces situations, en fournissant des valeurs par défaut, en rendant des variables facultatives, ou en définissant certaines variables comme obligatoires.

Définir des valeurs par défaut

L’une des solutions les plus simples pour éviter les erreurs liées aux variables non définies est d’utiliser le filtre default. Ce filtre permet de spécifier une valeur par défaut à utiliser si la variable n’est pas définie ou est vide. Cela évite les interruptions dues à des variables manquantes tout en assurant que le playbook continue de s’exécuter normalement.

{{ variable | default('valeur_par_defaut') }}

Si la variable variable n’est pas définie ou vide, Ansible utilisera automatiquement la valeur par défaut spécifiée. Cela est particulièrement utile pour les configurations où certaines valeurs peuvent varier selon l’environnement, mais où vous souhaitez éviter d’écrire des valeurs spécifiques pour chaque scénario.

Prenons l’exemple d’un fichier de configuration où vous devez définir un port pour un service. Si la variable correspondant au port n’est pas fournie, Ansible utilisera le port par défaut 8080 :

server {
listen {{ port | default(8080) }};
}

Si la variable port n’est pas définie dans le playbook ou l’inventaire, Ansible définira automatiquement le port à 8080 dans le fichier de configuration.

Valeur par défaut dans les fichiers de rôle

Une autre approche consiste à définir des valeurs par défaut dans les fichiers defaults/main.yml d’un rôle. Cela permet de centraliser la définition des variables par défaut au niveau du rôle et d’éviter d’avoir à les spécifier dans les templates ou les tâches.

Exemple dans defaults/main.yml :

port: 8080

Cela permet de toujours avoir une valeur de repli pour la variable port, même si elle n’est pas définie ailleurs dans le playbook ou l’inventaire.

Rendre les variables facultatives

Dans certains cas, vous pouvez souhaiter que certaines variables soient facultatives. Si une variable facultative n’est pas définie, vous pouvez utiliser le filtre omit pour ignorer complètement une option ou un paramètre dans une tâche.

Le filtre omit permet de ne pas inclure un paramètre dans une tâche si la variable n’est pas définie. Cela est particulièrement utile lorsque vous ne voulez pas qu’Ansible utilise une valeur par défaut, mais plutôt qu’il n’applique pas du tout le paramètre concerné.

Voici un exemple où le mode d’un fichier est optionnel. Si la variable mode n’est pas définie pour un fichier particulier, Ansible ignore le paramètre :

- name: Touch files with an optional mode
ansible.builtin.file:
dest: "{{ item.path }}"
state: touch
mode: "{{ item.mode | default(omit) }}"
loop:
- path: /tmp/foo
- path: /tmp/bar
- path: /tmp/baz
mode: "0444"

Dans cet exemple, seuls les fichiers ayant un mode explicitement défini l’appliqueront. Le fichier /tmp/foo et /tmp/bar seront créés sans qu’un mode spécifique soit appliqué, tandis que /tmp/baz aura le mode 0444.

Définition des valeurs obligatoires

Dans certains cas, vous pouvez avoir des variables qui doivent être définies pour que le playbook fonctionne correctement. Ansible permet de marquer certaines variables comme obligatoires en utilisant le filtre mandatory. Si la variable n’est pas définie, Ansible générera une erreur et arrêtera l’exécution du playbook.

Le filtre mandatory force la validation d’une variable. Si la variable n’est pas définie, Ansible renverra une erreur explicite et interrompra l’exécution.

{{ variable | mandatory }}

Cela garantit que la variable variable est définie. Si elle ne l’est pas, Ansible retournera une erreur “variable mandatory not defined”, vous aidant ainsi à identifier rapidement les problèmes liés aux variables non spécifiées.

Imaginons un playbook où la configuration du mot de passe d’une base de données est obligatoire. Le filtre mandatory s’assurera que cette variable est correctement définie avant de procéder à la tâche :

- name: Configurer la base de données
template:
src: db.conf.j2
dest: /etc/myapp/db.conf
vars:
db_password: "{{ db_password | mandatory }}"

Si la variable db_password n’est pas définie, le playbook s’arrêtera immédiatement, et Ansible vous alertera qu’une variable critique est manquante.

Définition de valeurs ternaires (vrai / faux / nul)

Pour éviter l’utilisation excessive de conditions if/else ou de blocs set_fact, vous pouvez utiliser le filtre ternary dans les templates ou les playbooks. Ce filtre permet de choisir entre deux valeurs en fonction d’une condition, et offre une manière concise de gérer des choix bivalents (comme vrai/faux).

Le filtre ternary vous permet de choisir entre deux options en fonction de la valeur d’une variable ou d’une expression :

{{ (status == 'needs_restart') | ternary('restart', 'continue') }}

Dans cet exemple, si le statut est égal à needs_restart, Ansible renverra restart. Sinon, il renverra continue. Cela permet de traiter des cas simples de manière élégante et concise sans écrire des conditions complexes.

Gestion des types de données

En travaillant avec Ansible, vous serez souvent confronté à des types de données variés comme les chaînes de caractères, les entiers, les listes, ou les dictionnaires. Bien que les données soient généralement bien gérées par Ansible, il est parfois nécessaire de manipuler ou forcer un type de donnée pour s’assurer que les variables sont utilisées correctement dans les tâches et les templates. Les filtres Jinja permettent d’inspecter et de transformer ces types de données selon vos besoins.

Dans cette section, nous allons explorer comment Ansible et Jinja vous permettent de gérer et de manipuler efficacement les types de données à travers plusieurs filtres spécifiques.

Afficher le type d’une variable

Avant de manipuler une variable, il peut être utile de connaître son type de donnée exact. Cela est particulièrement utile lorsque vous travaillez avec des variables provenant de sources externes, des inventaires dynamiques ou des modules, où le type peut ne pas être immédiatement évident.

Pour ce faire, le filtre type_debug vous permet d’afficher le type Python sous-jacent d’une variable.

{{ my_var | type_debug }}

Ce filtre retournera un résultat comme "str", "int", "list", ou "dict" en fonction du type réel de la variable my_var. Cela permet de valider facilement si une variable est du type attendu avant de l’utiliser dans des opérations spécifiques.

Si vous travaillez avec une liste d’adresses IP et que vous n’êtes pas sûr que chaque variable soit effectivement une chaîne de caractères, vous pouvez utiliser type_debug pour valider le type :

- name: Vérifier le type des adresses IP
debug:
msg: "Type de la variable: {{ ip_address | type_debug }}"
vars:
ip_address: "192.168.1.1"

Dans cet exemple, Ansible affichera que la variable ip_address est de type str (chaîne de caractères).

Forçage du type

Il est parfois nécessaire de convertir explicitement le type d’une variable pour assurer une exécution correcte des tâches. Ansible propose plusieurs filtres Jinja pour forcer le type des variables, notamment int, float, bool, string, et list. Cela est particulièrement utile lorsque les données sont issues de fichiers de configuration, d’inventaires externes, ou de systèmes tiers, et qu’elles peuvent ne pas avoir le format attendu.

Le filtre bool est couramment utilisé pour convertir une variable en type booléen (vrai ou faux). Cela est utile lorsque vous devez vérifier qu’une chaîne de caractères ou un nombre se comporte comme un booléen dans des conditions.

- debug:
msg: "La variable est vraie"
when: some_string_value | bool

Dans cet exemple, Ansible convertira la chaîne some_string_value en un booléen avant de vérifier la condition. Les chaînes comme "yes", "true", "1", ou tout autre valeur non nulle seront interprétées comme true, tandis que les chaînes comme "no", "false", "0", ou des valeurs nulles seront interprétées comme false.

Conversion en entier avec int

Si vous devez utiliser des entiers dans des calculs ou des conditions, le filtre int vous permet de convertir une variable en nombre entier.

- debug:
msg: "Valeur de la variable : {{ some_var | int }}"

Ce filtre est particulièrement utile lorsque vous travaillez avec des variables qui pourraient être des chaînes représentant des nombres. Par exemple, une variable "42" sera convertie en entier 42.

Conversion en liste avec list

Le filtre list est pratique lorsque vous devez convertir une chaîne de caractères ou une autre structure de données en une liste utilisable.

- debug:
msg: "{{ some_string | list }}"

Ce filtre prend chaque caractère de la chaîne et le place dans une liste. Par exemple, la chaîne "hello" deviendra ['h', 'e', 'l', 'l', 'o']. Il est également utile pour convertir des tuples ou des ensembles en listes.

Convertir des dictionnaires en listes et vice-versa

Dans Ansible, il est courant de travailler avec des dictionnaires et des listes. Parfois, il peut être nécessaire de convertir un dictionnaire en liste ou inversement, notamment lorsque vous devez itérer sur des éléments ou manipuler des données dans un format spécifique.

Convertir un dictionnaire en liste : dict2items

Le filtre dict2items permet de transformer un dictionnaire en une liste de paires clé/valeur. Cela est utile lorsque vous devez parcourir ou manipuler un dictionnaire comme une liste.

Exemple de dictionnaire dans un fichier Ansible :

tags:
Application: payment
Environment: dev

Pour convertir ce dictionnaire en liste avec dict2items :

{{ tags | dict2items }}

Le résultat sera une liste de paires clé/valeur comme ceci :

- key: Application
value: payment
- key: Environment
value: dev

Cela est particulièrement utile pour parcourir des dictionnaires de manière itérative dans vos templates ou vos tâches.

Convertir une liste en dictionnaire : items2dict

Inversement, vous pouvez utiliser le filtre items2dict pour convertir une liste de paires clé/valeur en un dictionnaire. Cela est utile lorsque vous travaillez avec des données sous forme de liste, mais que vous devez les utiliser comme un dictionnaire dans Ansible.

Exemple d’utilisation :

- key: Application
value: payment
- key: Environment
value: dev

En appliquant items2dict, cela devient :

tags:
Application: payment
Environment: dev

Quelques Filtres sur les chaines de caractères

Les filtres Jinja offrent plusieurs outils pour manipuler du texte dans Ansible, que ce soit pour ajouter des commentaires à des fichiers, formater des chaînes, ou encoder/décoder des valeurs.

Expressions régulières

Les expressions régulières sont indispensables pour manipuler et valider des chaînes dans Ansible. Les trois filtres principaux sont regex_search, regex_findall, et regex_replace.

Le filtre regex_search vous permet de rechercher une correspondance dans une chaîne de caractères. Vous pouvez spécifier des options comme multiline ou ignorecase.

{{ 'foo\nBAR' | regex_search("^bar", multiline=True, ignorecase=True) }}

Le filtre regex_findall extrait toutes les occurrences d’un motif dans une chaîne. Par exemple, pour extraire toutes les adresses IP d’une chaîne de texte :

{{ 'Some DNS servers are 8.8.8.8 and 8.8.4.4' | regex_findall('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}

Gestion des chemins de fichiers

Ansible offre des filtres pour manipuler les chemins de fichiers sur différents systèmes.

Travailler avec des chemins Windows

  • win_basename : Récupère le nom de fichier à partir d’un chemin Windows.
  • win_splitdrive : Sépare la lettre du lecteur du chemin de fichier.
{{ path | win_splitdrive | first }}
# => 'C'

Travailler avec des chemins Linux

  • dirname : Récupère le répertoire à partir d’un chemin.
  • expanduser : Remplace le caractère tilde (~) par le chemin de l’utilisateur.
{{ path | expanduser }}
  • expandvars : Remplace les variables d’environnement dans un chemin.
{{ path | expandvars }}

Convertir une chaîne en objet date

Le filtre to_datetime convertit une chaîne en un objet date que vous pouvez ensuite manipuler.

{{ (("2023-09-14 20:00:12" | to_datetime) - ("2023-01-01" | to_datetime('%Y-%m-%d'))).days }}

Le filtre strftime formate une date ou une heure selon un modèle spécifique.

{{ '%Y-%m-%d' | strftime }}
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}

Ajouter des commentaires à un fichier

Le filtre comment permet d’ajouter des commentaires à un texte. Par défaut, Ansible utilise le symbole # pour les commentaires, mais vous pouvez personnaliser le style de commentaire.

{{ "Plain style (default)" | comment }}

Résultat :

#
# Plain style (default)
#

Il est possible de choisir d’autres styles de commentaire :

{{ "C style" | comment('c') }}
{{ "XML style" | comment('xml') }}

Ajouter des guillemets à une commande shell

Le filtre quote permet d’ajouter des guillemets à une commande shell pour s’assurer qu’elle est correctement interprétée.

- name: Run a shell command
ansible.builtin.shell: echo {{ string_value | quote }}

Encodage et décodage Base64

Les filtres b64encode et b64decode permettent d’encoder et décoder des chaînes en base64.

{{ encoded | b64decode }}
{{ decoded | string | b64encode }}

un filtre pour les URL

Le filtre urlsplit permet de décomposer une URL en ses différentes parties (schéma, hôte, chemin, etc.).

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit }}

Résultat :

{
"fragment": "fragment",
"hostname": "www.acme.com",
"netloc": "user:password@www.acme.com:9000",
"password": "password",
"path": "/dir/index.html",
"port": 9000,
"query": "query=term",
"scheme": "http",
"username": "user"
}

Vous pouvez extraire une seule partie de l’URL en la spécifiant dans le filtre :

{{ "http://www.acme.com:9000/dir/index.html" | urlsplit('hostname') }}
# => 'www.acme.com'

Quelques filtres mathématiques

En plus de manipuler des chaînes et des listes, Ansible permet également d’effectuer des calculs mathématiques simples à l’aide de filtres spécifiques.

  • Logarithme : Calcule le logarithme naturel ou basé sur un autre nombre.

    {{ myvar | log }}
    {{ myvar | log(10) }}
  • Puissance : Calcule la puissance d’un nombre.

    {{ myvar | pow(2) }}
  • Racine carrée : Calcule la racine carrée.

    {{ myvar | root }}

Travailler avec les listes

Les listes sont une structure de données courante dans Ansible, et les filtres Jinja permettent de les manipuler efficacement. Voici quelques filtres qui vous aideront à gérer vos listes de manière dynamique.

Obtenir un élément aléatoire d’une liste

Le filtre random vous permet de sélectionner un élément aléatoire d’une liste.

"{{ ['a', 'b', 'c'] | random }}"
# => 'c'

Vous pouvez également générer un nombre aléatoire à partir d’une plage d’entiers :

{{ 101 | random(step=10) }}
# => 70

Pour générer des nombres aléatoires reproductibles (idempotents), vous pouvez initialiser le générateur aléatoire avec une graine (seed), comme le nom de l’inventaire :

"{{ 60 | random(seed=inventory_hostname) }} * * * * root /script/from/cron"

Obtenir les valeurs minimum et maximum d’une liste

Les filtres min et max permettent de récupérer les valeurs minimale et maximale d’une liste.

{{ list1 | min }}
{{ list1 | max }}

Aplatir une liste

Le filtre flatten permet de décomposer les sous-listes imbriquées en une seule liste. Vous pouvez également spécifier un niveau d’imbrication à aplatir.

{{ [3, [4, [2]]] | flatten(levels=1) }}

Mélanger une liste

Le filtre shuffle mélange les éléments d’une liste. Comme pour random, vous pouvez définir une graine pour que le résultat soit reproductible.

{{ ['a', 'b', 'c'] | shuffle }}
# => ['c', 'a', 'b']
{{ ['a', 'b', 'c'] | shuffle(seed=inventory_hostname) }}

Obtenir les valeurs uniques d’une liste

Le filtre unique supprime les doublons d’une liste.

# list1: [1, 2, 5, 1, 3, 4, 10]
{{ list1 | unique }}
# => [1, 2, 5, 3, 4, 10]

Opérations sur les listes

Ansible permet de combiner, croiser et comparer des listes à l’aide de plusieurs filtres spécifiques.

  • Union de listes : Combine deux listes en une seule sans enlever les doublons.

    {{ list1 | union(list2) }}
    # => [1, 2, 5, 1, 3, 4, 10, 11, 99]
  • Intersection de listes : Récupère les éléments communs aux deux listes.

    {{ list1 | intersect(list2) }}
    # => [1, 2, 5, 3, 4]
  • Différence de listes : Renvoie les éléments présents dans la première liste mais pas dans la seconde.

    {{ list1 | difference(list2) }}
    # => [10]
  • Différence symétrique : Renvoie les éléments qui ne sont présents que dans l’une des deux listes.

    {{ list1 | symmetric_difference(list2) }}
    # => [10, 11, 99]

Concaténer une liste en une chaîne

Vous pouvez utiliser le filtre join pour concaténer une liste en une chaîne de caractères avec un séparateur spécifique.

{{ list | join(" ") }}

Combiner et sélectionner des données

Ansible vous permet de manipuler et transformer des données complexes, qu’il s’agisse de listes, de dictionnaires, ou d’autres structures de données imbriquées. Grâce aux filtres Jinja, vous pouvez non seulement combiner plusieurs variables en une seule structure, mais aussi sélectionner ou extraire des éléments spécifiques à partir de ces données. Cela vous donne une grande flexibilité pour gérer des jeux de données variés et adapter vos playbooks à des environnements complexes.

Combiner différentes variables en une liste

Le filtre zip est utilisé pour combiner deux ou plusieurs listes en une seule, en associant chaque élément de la première liste avec celui de la seconde. Cela permet de traiter des données provenant de différentes variables ou sources de manière simultanée.

Le filtre zip prend deux listes ou plus et les regroupe en une seule. Cela est utile lorsque vous voulez lier les éléments de plusieurs listes entre eux pour les traiter ensemble.

Exemple :

- name: Combiner deux listes
debug:
msg: "{{ [1, 2, 3] | zip(['a', 'b', 'c']) | list }}"

Ce playbook associera chaque élément des deux listes et produira le résultat suivant :

[
[1, "a"],
[2, "b"],
[3, "c"]
]

Si les listes ont des longueurs différentes, le filtre zip ne conservera que le nombre d’éléments correspondant à la plus petite liste. Si vous souhaitez inclure des éléments manquants avec des valeurs null, vous pouvez utiliser zip_longest à la place.

Le filtre zip_longest permet de gérer des listes de longueurs différentes, en ajoutant des valeurs nulles lorsque l’une des listes est plus courte.

- name: Combiner deux listes de longueurs différentes
debug:
msg: "{{ [1, 2, 3] | zip_longest(['a', 'b'], fillvalue=null) | list }}"

Ce playbook génère le résultat suivant, où le troisième élément de la première liste est associé à null :

[
[1, "a"],
[2, "b"],
[3, null]
]

Combiner des objets avec leurs sous-éléments

Lorsqu’on travaille avec des objets complexes, comme des utilisateurs ayant plusieurs groupes ou des machines associées à plusieurs services, le filtre subelements permet de combiner chaque élément avec ses sous-éléments. Cela est utile lorsque vous devez itérer sur chaque objet principal et traiter chacun de ses éléments associés séparément.

Le filtre subelements prend une liste d’objets et permet d’extraire et de combiner les sous-éléments définis à l’intérieur de ces objets. Cela permet de parcourir des relations un à plusieurs, comme des utilisateurs ayant plusieurs autorisations ou appartenant à plusieurs groupes.

Exemple d’une liste d’utilisateurs avec des groupes associés :

users:
- name: alice
authorized:
- /tmp/alice/onekey.pub
- /tmp/alice/twokey.pub
groups:
- wheel
- docker
- name: bob
authorized:
- /tmp/bob/id_ed25519.pub
groups:
- docker

Dans ce cas, vous pouvez utiliser subelements pour parcourir chaque utilisateur et ses groupes associés :

- name: Traiter les utilisateurs et leurs groupes
debug:
msg: "{{ users | subelements('groups', skip_missing=True) }}"

Le résultat affichera chaque utilisateur et le groupe auquel il appartient, sous forme de paires :

[
[
{
"name": "alice",
"authorized": [
"/tmp/alice/onekey.pub",
"/tmp/alice/twokey.pub"
],
"groups": ["wheel", "docker"]
},
"wheel"
],
[
{
"name": "alice",
"authorized": [
"/tmp/alice/onekey.pub",
"/tmp/alice/twokey.pub"
],
"groups": ["wheel", "docker"]
},
"docker"
],
[
{
"name": "bob",
"authorized": [
"/tmp/bob/id_ed25519.pub"
],
"groups": ["docker"]
},
"docker"
]
]

Cela vous permet de traiter chaque combinaison utilisateur/groupe de manière individuelle dans votre playbook, facilitant ainsi la gestion des relations complexes.

Combiner des dictionnaires

Les dictionnaires sont des structures de données couramment utilisées dans Ansible pour stocker des paires clé/valeur. Le filtre combine vous permet de fusionner plusieurs dictionnaires en un seul. Vous pouvez également utiliser des options comme recursive pour effectuer des fusions plus profondes, ou list_merge pour contrôler la manière dont les listes sont combinées.

Utilisation du filtre combine

Le filtre combine fusionne deux ou plusieurs dictionnaires. Si des clés identiques sont présentes dans plusieurs dictionnaires, la valeur du dernier dictionnaire écrasera celles des précédents, sauf si vous utilisez des options pour spécifier un comportement différent.

Exemple simple de combinaison de dictionnaires :

vars:
defaults:
app: payment
env: dev
overrides:
env: prod

En appliquant le filtre combine :

- name: Fusionner des dictionnaires
debug:
msg: "{{ defaults | combine(overrides) }}"

Cela produira le résultat suivant, où la valeur de env dans overrides écrase celle de defaults :

{
"app": "payment",
"env": "prod"
}

Option recursive pour des combinaisons profondes

Si vos dictionnaires contiennent des sous-dictionnaires, vous pouvez utiliser l’option recursive=True pour fusionner également leurs sous-niveaux.

vars:
defaults:
app:
name: payment
version: 1.0
overrides:
app:
version: 2.0

En utilisant recursive=True :

- name: Fusion récursive de dictionnaires
debug:
msg: "{{ defaults | combine(overrides, recursive=True) }}"

Le résultat sera :

{
"app": {
"name": "payment",
"version": "2.0"
}
}

Ici, le filtre combine les sous-dictionnaires au lieu de simplement écraser le dictionnaire app dans son ensemble.

Option list_merge pour gérer les listes

Lorsque vous combinez des dictionnaires contenant des listes, l’option list_merge vous permet de contrôler comment les listes sont combinées. Par défaut, Ansible remplace les listes, mais vous pouvez les fusionner de plusieurs façons : replace, append, ou keep.

Exemple avec l’option list_merge: 'append' :

vars:
defaults:
services:
- nginx
overrides:
services:
- mysql

Avec list_merge: 'append' :

- name: Fusionner des listes dans des dictionnaires
debug:
msg: "{{ defaults | combine(overrides, list_merge='append') }}"

Le résultat sera :

{
"services": ["nginx", "mysql"]
}

Les listes ont été combinées sans remplacer les valeurs précédentes.

Sélectionner des valeurs de listes ou de dictionnaires

Le filtre extract permet d’extraire des éléments spécifiques à partir de listes ou de dictionnaires. Il est particulièrement utile lorsque vous travaillez avec des tableaux de données complexes et que vous avez besoin de sélectionner des éléments précis en fonction de certaines clés ou indices.

Exemple d’extraction de valeurs spécifiques dans une liste :

- name: Extraire des éléments à partir d'une liste
debug:
msg: "{{ [0, 2] | map('extract', ['x', 'y', 'z']) | list }}"

Ce playbook extrait les éléments à la position 0 et 2 de la liste ['x', 'y', 'z'], générant ainsi le résultat suivant :

["x", "z"]

Vous pouvez également utiliser extract avec des dictionnaires pour extraire des valeurs basées sur des clés spécifiques.

Permutations et combinaisons à partir de listes

Les filtres combinations et permutations permettent de générer des combinais

ons et permutations à partir de listes d’éléments. Ces filtres sont utiles lorsque vous avez besoin d’explorer toutes les combinaisons ou permutations possibles entre plusieurs valeurs.

- name: Générer des combinaisons
debug:
msg: "{{ [1, 2, 3] | combinations(2) | list }}"

Cela générera toutes les combinaisons possibles de 2 éléments à partir de la liste [1, 2, 3] :

[
[1, 2],
[1, 3],
[2, 3]
]
- name: Générer des permutations
debug:
msg: "{{ range(1, 2 + 1) | permutations | list }}"

Ce playbook générera toutes les permutations possibles des nombres 1 et 2 :

[
[1, 2],
[2, 1]
]

Produit cartésien

Le filtre product permet de créer un produit cartésien entre deux listes. Cela est utile lorsque vous avez besoin de combiner chaque élément d’une liste avec chaque élément d’une autre.

Supposons que vous ayez une liste de noms de domaines et que vous souhaitiez ajouter un suffixe comme “.com” à chaque nom.

- name: Générer un produit cartésien
debug:
msg: "{{ ['foo', 'bar'] | product(['com']) | map('join', '.') | join(',') }}"

Ce playbook génère le résultat suivant, combinant chaque nom avec le suffixe .com :

"foo.com,bar.com"

Utilisation du filtre json_query pour extraire des données JSON

Le format JSON est couramment utilisé par les API REST pour échanger des données, et il est essentiel de savoir comment les manipuler efficacement avec Ansible. Pour rechercher des éléments spécifiques dans une variable JSON, Ansible propose le filtre json_query, qui s’appuie sur JMESPath, un langage de requête conçu pour parcourir et analyser des documents JSON.

Transformer des variables au format JSON

Avant d’extraire des informations depuis une API, il est utile de savoir comment transformer des variables en JSON. Ansible propose deux filtres pour cette opération : to_json et to_nice_json. Ces filtres transforment une variable en chaîne JSON, avec une mise en forme brute ou plus lisible.

Prenons un exemple avec deux applications partageant certaines configurations grâce à l’utilisation des ancres YAML et des alias.

---
- hosts: localhost
vars:
apps:
- name: app1
jvm: &jvm_opts
opts: '-Xms1G -Xmx2G'
port: 1000
path: /usr/lib/app1
- name: app2
jvm:
<<: *jvm_opts
path: /usr/lib/app2
tasks:
- name: Transformer en JSON
debug:
msg: "{{ apps | to_json }}"
- name: Transformer en JSON lisible
debug:
msg: "{{ apps | to_nice_json(indent=2) }}"

Ici, nous utilisons l’ancre &jvm_opts pour partager les mêmes options JVM entre app1 et app2. Grâce à l’alias *jvm_opts, les propriétés opts et port sont héritées par app2, tandis que path est fusionné pour chaque application. Avec to_json, les données sont converties au format JSON, tandis que to_nice_json ajoute un indentation pour rendre le résultat plus lisible.

Pour illustrer l’utilisation du filtre json_query, utilisons un exemple concret avec les données JSON provenant de l’API jsonplaceholder. Prenons comme exemple un schéma JSON représentant des utilisateurs.

Exemple de données JSON retournées par l’API :

[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
...
]

Vous pouvez utiliser le module uri pour obtenir ces données et les stocker dans une variable.

---
- name: Parse JSON
hosts: localhost
gather_facts: no
tasks:
- name: Récupérer les utilisateurs via l'API
uri:
url: https://jsonplaceholder.typicode.com/users
return_content: yes
register: jsondata
- name: Afficher les données
debug:
msg: "{{ jsondata.json }}"

Extraire un champ particulier d’un JSON

Supposons que vous souhaitiez obtenir la liste des noms d’utilisateurs présents dans les données JSON. Pour ce faire, vous pouvez utiliser le filtre json_query avec une requête JMESPath pour extraire les noms de la liste.

Avant de pouvoir utiliser json_query, vous devez installer le module Python JMESPath :

Terminal window
pip install jmespath --user

Nous allons modifier notre playbook pour extraire uniquement les noms des utilisateurs.

---
- name: Parse JSON et extraire les noms
hosts: localhost
gather_facts: no
tasks:
- name: Récupérer les utilisateurs via l'API
uri:
url: https://jsonplaceholder.typicode.com/users
return_content: yes
register: jsondata
- name: Extraire les noms des utilisateurs
vars:
jmesquery: "[*].{Name: name}"
debug:
msg: "{{ jsondata.json | json_query(jmesquery) }}"

Le filtre json_query est ici utilisé pour parcourir tous les objets JSON et en extraire le champ name.

Retrouver un élément particulier

Il est également possible d’extraire un élément spécifique du JSON. Par exemple, pour retrouver l’utilisateur ayant l’ID 1, vous pouvez utiliser une requête JMESPath filtrant les résultats en fonction de ce critère.

---
- name: Retrouver un utilisateur spécifique
hosts: localhost
gather_facts: no
tasks:
- name: Récupérer les utilisateurs via l'API
uri:
url: https://jsonplaceholder.typicode.com/users
return_content: yes
register: jsondata
- name: Extraire l'utilisateur avec l'ID 1
vars:
jmesquery: "[? id==`1`]"
debug:
msg: "{{ jsondata.json | json_query(jmesquery) }}"

Cette requête JMESPath permet de filtrer les utilisateurs en fonction de leur ID, ne renvoyant que l’utilisateur correspondant à ID 1.

Vous pouvez combiner des conditions OU (||) et ET (&&) dans votre requête JMESPath pour filtrer plusieurs utilisateurs ou ajouter des critères supplémentaires.

Exemple d’extraction des noms des entreprises pour les utilisateurs ayant ID 1 ou ID 5 :

- name: Extraire les noms d'entreprises
vars:
jmesquery: "[? id==`1` || id==`5`].{Company: company.name}"
debug:
msg: "{{ jsondata.json | json_query(jmesquery) }}"

Vous pouvez tester vos expressions JMESPath ici

Combiner les données extraites

En plus d’extraire des données, Ansible vous permet de combiner les résultats extraits pour créer de nouvelles structures de données. Le filtre combine peut être utilisé pour associer des informations, comme lier un nom à une adresse e-mail.

Dans cet exemple, nous allons combiner le nom et l’email des utilisateurs ID 1 et ID 5 pour créer une nouvelle structure de données.

- name: Associer nom et email
set_fact:
my_data: "{{ my_data | default({}) | combine({item.name: item.email}) }}"
with_items: "{{ jsondata.json | json_query('[? id==`1` || id==`5`]') }}"
- name: Afficher les données combinées
debug:
msg: "{{ my_data }}"

Ce playbook associe chaque nom d’utilisateur à son adresse e-mail, créant ainsi une structure clé/valeur avec les noms comme clés et les e-mails comme valeurs.

Manipuler des données YAML

Les filtres to_yaml et to_nice_yaml dans Ansible permettent de convertir des données (variables, objets ou structures complexes) au format YAML. Ce format est couramment utilisé pour rendre les configurations et les fichiers de données plus lisibles et faciles à interpréter. Ces filtres sont particulièrement utiles lorsque vous devez afficher ou manipuler des données dans un fichier de sortie ou générer des fichiers de configuration.

Utilisation du filtre to_yaml

Le filtre to_yaml permet de convertir des variables au format YAML brut. Cela est utile lorsque vous travaillez avec des structures de données complexes et que vous souhaitez les afficher dans un format facilement compréhensible ou lorsque vous devez écrire des données directement dans un fichier YAML.

- name: Convertir une variable en YAML
debug:
msg: "{{ variable | to_yaml }}"

Dans cet exemple, la variable variable sera convertie en texte YAML. Ce filtre est surtout utile pour convertir des objets JSON ou des dictionnaires en YAML sans mise en forme particulière.

Utilisation du filtre to_nice_yaml

Le filtre to_nice_yaml est une version améliorée de to_yaml, permettant de générer un YAML lisible avec une mise en forme plus agréable. Il ajoute des indentations et organise les données de manière plus claire. Cela le rend plus adapté pour l’affichage humain ou l’écriture dans des fichiers de configuration.

- name: Convertir une variable en YAML lisible
debug:
msg: "{{ variable | to_nice_yaml(indent=4) }}"

Ici, le paramètre indent=4 spécifie que chaque niveau d’imbrication dans la structure YAML sera indéterminé avec 4 espaces. Ce format est souvent utilisé pour améliorer la lisibilité lorsque vous travaillez avec des structures complexes dans des fichiers de configuration.

Le filtre to_nice_yaml propose plusieurs options supplémentaires, telles que l’ajustement de l’indentation ou le contrôle de la largeur des lignes.

- name: Convertir en YAML avec une largeur de ligne
debug:
msg: "{{ variable | to_nice_yaml(width=120) }}"

Ce paramètre définit la largeur maximale des lignes, utile pour éviter que les lignes ne dépassent une certaine longueur dans les fichiers YAML générés.

Exemple complet : Écrire dans un fichier YAML

Voici un exemple où les données d’un inventaire sont converties au format YAML lisible, puis écrites dans un fichier.

- name: Convertir et écrire en YAML
copy:
content: "{{ inventory_data | to_nice_yaml(indent=2) }}"
dest: "/path/to/inventory.yaml"

Dans cet exemple, le contenu de inventory_data est converti en YAML bien formaté, avec une indentation de 2 espaces, puis est écrit dans un fichier inventory.yaml.

Sécuriser les données avec des filtres

Ansible propose des filtres pour sécuriser les données sensibles, notamment pour la gestion des mots de passe et le chiffrement de chaînes de caractères.

Générer des mots de passe

Le filtre password_hash permet de générer des mots de passe sécurisés à l’aide d’algorithmes comme SHA-512 ou SHA-256. Vous pouvez ajouter des salts et définir des rounds pour renforcer la sécurité.

{{ 'secretpassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}
{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=10000) }}

Chiffrer des chaînes

Les filtres hash permettent de chiffrer une chaîne avec divers algorithmes comme MD5, SHA1, ou Blowfish.

{{ 'test2' | hash('blowfish') }}
{{ 'test1' | hash('md5') }}
{{ 'test1' | hash('sha1') }}

Calculer un checksum

Le filtre checksum calcule un hash à partir d’une chaîne de caractères.

{{ 'test2' | checksum }}

Bonnes pratiques pour l’utilisation des filtres Jinja dans Ansible

L’utilisation des filtres Jinja dans Ansible est un outil puissant pour automatiser vos configurations. Toutefois, il est essentiel de suivre certaines bonnes pratiques pour garantir que vos playbooks restent lisibles, faciles à maintenir, et efficaces. L’une des premières règles à retenir est de garder les choses simples. La complexité excessive rend les configurations plus difficiles à comprendre et plus coûteuses à maintenir.

Première règle : La simplicité

Le principe de base dans Ansible, et plus largement dans l’automatisation, est de faire simple. Un code complexe est non seulement plus difficile à lire mais aussi à maintenir. Des configurations trop imbriquées ou l’utilisation excessive de filtres complexes peuvent rapidement rendre vos playbooks difficiles à déboguer.

  • Utilisez des filtres uniquement lorsque c’est nécessaire. Ne complexifiez pas votre code avec des filtres Jinja sophistiqués si une solution plus simple peut être utilisée. Par exemple, utilisez des conditions when basiques au lieu d’imbriquer plusieurs filtres pour rendre le code plus clair.
  • N’utilisez pas des filtres pour des transformations excessives. Si une donnée doit être transformée avec plusieurs filtres à la suite, il est souvent préférable de préparer cette donnée en amont dans une tâche distincte avec des variables intermédiaires.

Éviter la duplication de code

L’une des sources les plus courantes de complexité est la duplication de code. Si vous répétez des blocs de configuration ou des appels de filtres dans plusieurs tâches, cela rend la maintenance plus difficile. Les modifications devront être faites à plusieurs endroits, ce qui accroît le risque d’erreurs.

  • Centralisez vos transformations de données en utilisant des variables set_fact. Si un filtre ou une transformation complexe doit être utilisé à plusieurs reprises, il est plus efficace de définir une variable avec le résultat intermédiaire.
- name: Préparer les données avec un filtre complexe
set_fact:
data_transformed: "{{ data | some_filter | another_filter }}"

Cela rend le playbook plus lisible et plus facile à ajuster en cas de changement de logique.

Séparation des responsabilités

Les playbooks bien structurés sont faciles à comprendre et à maintenir. Une bonne pratique consiste à séparer les tâches en fonction de leurs responsabilités. Par exemple, les tâches de récupération de données ne devraient pas inclure des filtres Jinja complexes pour transformer ces données dans la même étape.

  • Séparez la récupération des données de leur transformation. Collectez d’abord les données dans une tâche, puis traitez-les avec des filtres dans une autre tâche. Cela permet une meilleure lisibilité et facilite le débogage si quelque chose ne fonctionne pas comme prévu.
- name: Récupérer les données
uri:
url: "http://api.example.com/data"
return_content: yes
register: api_data
- name: Transformer les données avec des filtres
set_fact:
transformed_data: "{{ api_data.json | json_query('...') }}"

Utiliser des variables intermédiaires

Lorsque vous travaillez avec plusieurs filtres enchaînés, il peut être tentant d’appliquer directement les transformations dans une seule tâche. Cela rend cependant le code plus difficile à lire et augmente le risque d’erreurs. Une bonne pratique consiste à utiliser des variables intermédiaires pour séparer les étapes de transformation.

  • Décomposez les filtres sur plusieurs lignes en utilisant des variables. Cela permet de suivre plus facilement la transformation des données à chaque étape.
- name: Extraire les utilisateurs
set_fact:
users: "{{ api_data.json | json_query('users') }}"
- name: Filtrer les utilisateurs actifs
set_fact:
active_users: "{{ users | selectattr('status', 'equalto', 'active') | list }}"

Privilégier la lisibilité

Les playbooks lisibles sont plus faciles à maintenir et à transmettre à d’autres membres de l’équipe. Utilisez des noms de variables explicites et évitez d’imbriquer trop de transformations dans une seule tâche.

  • Choisissez des noms de variables clairs et descriptifs. Cela aide à comprendre ce que fait une tâche sans avoir à lire toute la logique en profondeur.
  • Commentez vos filtres complexes. Lorsque vous devez utiliser des filtres Jinja avancés, expliquez dans un commentaire ce que vous faites et pourquoi cela est nécessaire.

Testez les transformations étape par étape

Avant de vous lancer dans des filtres complexes, il est conseillé de tester chaque étape de la transformation. Vous pouvez utiliser des tâches debug pour afficher des résultats intermédiaires et vous assurer que chaque transformation se déroule comme prévu.

  • Utilisez le module debug pour afficher le contenu des variables à différents points de votre playbook.
- name: Vérifier les données transformées
debug:
msg: "{{ transformed_data }}"

Cela permet de valider chaque étape de la transformation et de repérer plus facilement les erreurs.

Conclusion

En résumé, l’utilisation des filtres Jinja dans Ansible est une compétence clé pour rendre vos playbooks plus flexibles et dynamiques. Nous avons exploré une grande variété de filtres, allant de la manipulation des listes et des textes, aux filtres plus avancés pour la sécurisation des données, la gestion des formats JSON/YAML, et les calculs mathématiques. Les filtres comme to_yaml et to_nice_yaml simplifient l’écriture des fichiers de configuration, tandis que d’autres filtres comme json_query permettent de manipuler des données complexes issues des API.

Cependant, la complexité peut rapidement alourdir vos configurations, rendant la maintenance plus difficile. C’est pourquoi il est essentiel de suivre les bonnes pratiques que nous avons abordées : faire simple, éviter la duplication, et tester étape par étape pour assurer la robustesse et la clarté de vos playbooks.

L’objectif final est de maintenir des configurations lisibles, efficaces, et faciles à comprendre, même pour des équipes élargies ou des projets à long terme. En suivant ces principes, vous garantissez que vos playbooks Ansible restent non seulement fonctionnels, mais également pérennes et maintenables.