Aller au contenu

Utiliser Jinja avec Ansible

Mise à jour :

logo devops

Dans ce guide, je vais vous exposer l’utilisation de Jinja avec Ansible pour construire des fichiers de configurations dynamiques. Comme vu dans mon cours sur Python, Jinja est un moteur de template puissant qui permet de manipuler des variables, d’utiliser des boucles et des conditions.

En plus de sa capacité à générer des fichiers de configuration, Jinja peut également être utilisé directement dans les tâches Ansible pour manipuler des données à l’aide de filtres. Ces filtres permettent de transformer les valeurs des variables, par exemple en modifiant des chaînes de caractères, en formattant des dates ou en effectuant des opérations sur des listes. Cela permet d’adapter les valeurs des variables avant de les utiliser dans d’autres tâches, et donc de simplifier la gestion des données complexes.

Je vais me concentrer sur des cas d’utilisation concrets, en montrant comment tirer parti des fonctionnalités de Jinja pour optimiser vos workflows avec Ansible.

Utilisation du module template

Le module template d’Ansible permet de générer des fichiers de configuration dynamiques à partir de templates Jinja. En combinant ce module avec les fonctionnalités puissantes de Jinja, vous pouvez non seulement personnaliser les fichiers en fonction de vos variables Ansible, mais aussi vous assurer que ces fichiers sont créés de manière sécurisée, notamment en gérant les permissions et en masquant les informations sensibles.

Le module template : présentation et syntaxe de base

Le module template d’Ansible permet de copier un fichier Jinja (généralement avec une extension .j2) depuis votre machine de contrôle ou votre dépôt Ansible vers l’hôte distant, en le transformant dynamiquement grâce aux variables Jinja.

Voici la syntaxe de base du module template :

- name: Créer un fichier de configuration Nginx
template:
src: ssh_config.j2
dest: /etc/ssh/ssh_config
owner: root
group: root
mode: '0644'

Dans cet exemple, le fichier ssh_config.j2 est transformé en /etc/ssh/ssh_config sur l’hôte cible, avec les permissions adéquates. Le propriétaire du fichier sera root, et les permissions définies avec le mode 0640, ce qui signifie que seul le propriétaire peut écrire dans le fichier, mais tout le monde ne peut pas le lire.

Protégez vos données sensibles

Lors de la génération de fichiers de configuration, il est fréquent d’utiliser des informations sensibles comme des mots de passe, des clés API ou des certificats. Ansible propose plusieurs méthodes pour manipuler ces données de manière sécurisée, notamment via les variables chiffrées avec Ansible Vault.

Supposons que vous génériez une configuration pour une base de données où un mot de passe utilisateur est nécessaire. Le mot de passe est stocké dans une variable Ansible chiffrée avec Ansible Vault :

- name: Créer le fichier de configuration sécurisé pour la base de données
template:
src: db.conf.j2
dest: /etc/myapp/db.conf
owner: root
group: root
mode: '0600' # Lecture et écriture uniquement pour le propriétaire
vars:
db_user: myuser
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
31386137623237613266326139623033...
db_host: 127.0.0.1

Ici, la configuration est générée avec les permissions 0600, garantissant que seul root peut lire et écrire dans le fichier, empêchant ainsi tout autre utilisateur d’accéder à des informations sensibles comme le mot de passe.

Ecriture des templates Jinja

Un template Jinja pour Ansible est un fichier texte qui contient des variables et des instructions de contrôle (comme des conditions ou des boucles) permettant de générer du contenu dynamique. Il est utilisé pour créer des fichiers de configuration ou d’autres documents en adaptant leur contenu en fonction des données fournies par Ansible.

Ces templates utilisent la syntaxe Jinja, un moteur de template qui permet de manipuler des variables, d’effectuer des transformations avec des filtres, et d’ajouter des éléments dynamiques comme des boucles ou des conditions. Lors de l’exécution d’un playbook, Ansible remplace les variables dans le template par leurs valeurs réelles et génère un fichier prêt à être utilisé. Cela permet de créer des configurations personnalisées, adaptées à chaque machine ou environnement, de manière flexible et automatisée.

Voici un exemple simple de fichier de template pour la configuration du service sshd :

# Fichier de configuration SSH personnalisé
Port {{ ssh_port | default(22) }}
PermitRootLogin {{ permit_root_login | default('no') }}
PasswordAuthentication {{ password_auth | default('no') }}
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding {{ x11_forwarding | default('no') }}
AllowUsers {{ allowed_users | join(' ') }}
# Configurations supplémentaires pour sécuriser SSH
PermitEmptyPasswords no
ClientAliveInterval 300
ClientAliveCountMax 2

Explication des variables :

  • ssh_port : Le port sur lequel SSH écoute (par défaut 22).
  • permit_root_login : Autorise ou interdit la connexion de l’utilisateur root via SSH (par défaut ‘no’ pour des raisons de sécurité).
  • password_auth : Active ou désactive l’authentification par mot de passe (par défaut ‘no’ pour forcer l’utilisation des clés SSH).
  • x11_forwarding : Active ou désactive le forwarding X11 (par défaut ‘no’).
  • allowed_users : Liste des utilisateurs autorisés à se connecter via SSH.

Plus d’infos sur la configuration du service SSH

Voici un exemple de playbook manipulant ce template :

- name: Générer un fichier de configuration SSH sécurisé
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
vars:
ssh_port: 2222
permit_root_login: 'no'
password_auth: 'no'
x11_forwarding: 'no'
allowed_users:
- user1
- user2

Une fois le playbook executé le fichier suivant sera déposé sur le serveur cible :

Terminal window
# Fichier de configuration SSH personnalisé
Port 2222
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
AllowUsers user1 user2
# Configurations supplémentaires pour sécuriser SSH
PermitEmptyPasswords no
ClientAliveInterval 300
ClientAliveCountMax 2

Les filtres Jinja dans Ansible

Les filtres dans Jinja sont des outils puissants qui permettent de transformer et manipuler les variables avant leur utilisation dans les templates ou les tâches Ansible. Que ce soit pour adapter des chaînes de caractères, manipuler des structures de données, ou encore définir des valeurs par défaut, les filtres offrent une flexibilité supplémentaire dans la gestion des variables.

Les filtres s’intègrent facilement dans Ansible via les Les filtres s’intègrent facilement dans Ansible via les templates Jinja, mais aussi directement dans les taches de playbooks. Ils permettent de nettoyer ou d’adapter des données avant de les injecter dans des tâches, ce qui est particulièrement utile pour manipuler les sorties de modules ou ajuster des paramètres spécifiques selon les besoins.

Les filtres de contrôles des variables

  • Le filtre default permet de définir une valeur par défaut si une variable n’est pas définie ou est vide.

    port: {{ nginx_port | default(80) }}
  • mandatory : Ce filtre force la vérification qu’une variable est définie et non vide. Si la variable est absente, Ansible arrête l’exécution avec une erreur.

    {{ my_var | mandatory }}
  • defined : Vérifie si une variable est définie (sans s’assurer qu’elle n’est pas vide).

    {% if my_var is defined %}
    Variable is defined
    {% endif %}

Les filtres manipulant des chaines de caractères

Supposons que vous vouliez configurer un fichier avec des variables d’environnement, et que vous deviez mettre les noms des serveurs en majuscules. Vous pouvez utiliser le filtre upper pour transformer une chaîne de caractères.

server_name: {{ ansible_hostname | upper }}

Cela convertira la variable ansible_hostname en majuscules avant de l’injecter dans votre fichier de configuration.

Jinja propose de très nombreux filtres utiles. Voici quelques exemples courants utilisés avec Ansible :

  • default : Ce filtre permet de définir une valeur par défaut si une variable n’est pas définie ou est vide.

    Exemple :

    port: {{ nginx_port | default(80) }}

    Ici, si la variable nginx_port n’est pas définie, elle prendra la valeur 80.

  • replace : Ce filtre remplace des sous-chaînes spécifiques dans une chaîne de caractères.

    Exemple :

    path: {{ "/usr/local/bin" | replace("/usr", "/opt") }}

    Ce template changera le chemin /usr/local/bin en /opt/local/bin.

  • lower et upper : Ces filtres permettent de convertir une chaîne en minuscules ou en majuscules.

    Exemple :

    user: {{ ansible_user | lower }}

Les Filtres manipulant des listes et des dictionnaires

Jinja permet aussi de travailler avec des listes ou des dictionnaires de manière flexible. Voici quelques exemples de filtres très utiles pour les administrateurs systèmes.

  • join : Ce filtre permet de convertir une liste en une chaîne de caractères séparée par un délimiteur donné.

    Exemple :

    packages: {{ packages_list | join(", ") }}

    Si packages_list contient ["nginx", "mysql", "php"], cette ligne générera la chaîne suivante : "nginx, mysql, php".

  • sort : Ce filtre trie les éléments d’une liste.

    Exemple :

    sorted_users: {{ users | sort }}
  • map : Ce filtre permet d’appliquer une transformation à chaque élément d’une liste.

    Exemple :

    users_shells: {{ users | map(attribute='shell') | join(', ') }}

    Cela permet d’extraire les valeurs de la clé shell pour chaque utilisateur dans une liste de dictionnaires.

Plus de filtres

Des Filtres dans les tâches Ansible

Les filtres ne sont pas seulement utiles dans les templates, mais peuvent aussi être directement utilisés dans les tâches Ansible pour traiter des données avant qu’elles ne soient utilisées dans les modules. Cela permet de gérer plus facilement les données retournées par les modules ou les inventaires dynamiques.

Voici un exemple où le filtre join est utilisé dans une tâche Ansible pour générer une liste d’utilisateurs à partir d’une variable.

- name: Create a comma-separated list of users
debug:
msg: "{{ users_list | join(', ') }}"

Dans cet exemple, la tâche debug affiche une liste d’utilisateurs sous forme de chaîne, séparée par des virgules.

Développer des Filtres Personnalisés

En plus des nombreux filtres fournis par Jinja, il est possible de créer des filtres personnalisés pour répondre à des besoins spécifiques. Cela peut se faire en combinant des filtres existants ou en définissant des filtres Ansible dans des plugins personnalisés.

Dans des environnements complexes, créer vos propres filtres vous permettra d’adapter Jinja à vos besoins spécifiques, par exemple en manipulant des formats de données spécifiques à votre infrastructure.

Les conditions dans Jinja

Les conditions dans Jinja permettent d’adapter dynamiquement le contenu des templates en fonction de la valeur des variables Ansible. En utilisant des instructions conditionnelles, vous pouvez contrôler le rendu du template et inclure ou exclure certaines sections selon des critères spécifiques. Cela vous permet de générer des fichiers de configuration ou d’exécuter des tâches de manière plus flexible et contextuelle, en fonction des besoins réels de votre infrastructure.

Syntaxe de base des conditions

Les conditions Jinja utilisent la syntaxe if, elif et else, similaire à celle de Python. Voici la structure de base d’une condition Jinja :

{% if enable_feature %}
feature_enabled = true
{% else %}
feature_enabled = false
{% endif %}

Dans cet exemple, si la variable enable_feature est définie et a une valeur “vraie” (comme true ou yes), la fonctionnalité est activée. Sinon, elle est désactivée.

Comparaison et opérateurs logiques

Vous pouvez utiliser des opérateurs de comparaison (comme ==, !=, <, >, <=, >=) et des opérateurs logiques (comme and, or, not) pour créer des conditions plus complexes.

Comparaison de variables

{% if env == 'production' %}
enable_ssl = true
{% else %}
enable_ssl = false
{% endif %}

Dans cet exemple, la variable env est comparée à la chaîne 'production'. Si elle correspond, la configuration SSL est activée, sinon elle est désactivée.

Opérateurs logiques

{% if env == 'production' and ssl_enabled %}
enable_ssl = true
{% else %}
enable_ssl = false
{% endif %}

Ici, la condition vérifie deux critères : que env est égal à 'production' et que ssl_enabled est activé. Si les deux conditions sont remplies, le SSL est activé.

Vérification des variables définies

Il est souvent nécessaire de vérifier si une variable Ansible est définie avant de l’utiliser dans un template, afin d’éviter des erreurs. Jinja propose des tests comme is defined et is not defined pour cela.

{% if my_var is defined %}
Variable is defined: {{ my_var }}
{% else %}
Variable is not defined
{% endif %}

Conditions imbriquées

{% if user is defined %}
{% if user == 'admin' %}
access_level = 'full'
{% else %}
access_level = 'limited'
{% endif %}
{% else %}
access_level = 'guest'
{% endif %}

Dans cet exemple, on vérifie d’abord si la variable user est définie. Si c’est le cas, on adapte les niveaux d’accès en fonction de l’utilisateur (admin ou non). Si user n’est pas défini, un accès invité est attribué.

Utilisation des conditions avec des listes ou des dictionnaires

Les conditions Jinja peuvent aussi être utilisées pour parcourir des listes ou des dictionnaires et appliquer des logiques conditionnelles à chaque élément.

Exemple : Itération avec conditions

{% for service in services %}
{% if service.status == 'active' %}
- Service {{ service.name }} is running
{% else %}
- Service {{ service.name }} is not running
{% endif %}
{% endfor %}

Ici, pour chaque service dans la liste services, on vérifie son statut et affiche un message différent selon qu’il est actif ou non.

Conditions avec filtres

Les filtres Jinja peuvent être utilisés dans des conditions pour manipuler des données avant de les comparer. Par exemple, vous pouvez vérifier si une variable correspond à un certain modèle en utilisant des filtres de texte.

{% if url | regex_search('^https://') %}
secure_connection = true
{% else %}
secure_connection = false
{% endif %}

Dans cet exemple, le filtre regex_search est utilisé pour vérifier si l’URL commence par “https”. Si c’est le cas, la connexion est marquée comme sécurisée.

Utilisation des filtres dans les conditions des tâches Ansible

En combinant les filtres avec les conditions dans les tâches, vous pouvez vérifier si une variable remplit certains critères ou adapter des actions selon la forme des données. Voici quelques cas d’utilisation où les filtres Jinja peuvent enrichir vos conditions de tâches Ansible.

La directive when permet d’exécuter une tâche uniquement si une condition est vraie. Vous pouvez y inclure des filtres Jinja pour manipuler les variables avant de les comparer.

Exemple : Vérifier si une variable contient une certaine chaîne

Si vous souhaitez exécuter une tâche uniquement lorsque la variable hostname contient la chaîne “web”, vous pouvez utiliser le filtre search pour vérifier cela :

- name: Redémarrer le service web sur les serveurs web
service:
name: apache2
state: restarted
when: "'web' in inventory_hostname"

Ici, la tâche redémarre Apache uniquement si le hostname contient “web”. Cela peut être utile pour exécuter certaines tâches sur un sous-ensemble de serveurs.

Utilisation des filtres de texte dans les conditions

Les filtres de texte comme regex_search et replace sont particulièrement utiles pour valider ou adapter des chaînes de caractères avant de décider si une tâche doit être exécutée.

Vous pouvez utiliser regex_search pour vérifier si une variable correspond à un modèle spécifique, comme une adresse IP, avant d’exécuter une tâche.

- name: Vérifier et utiliser une IP valide
debug:
msg: "L'adresse IP est valide : {{ my_ip }}"
when: my_ip is defined and my_ip | regex_search('^(\d{1,3}\.){3}\d{1,3}$')

Ici, la tâche debug ne s’exécutera que si my_ip est définie et correspond à une adresse IP valide au format IPv4.

Les boucles et itérations avec Jinja

Les boucles et les itérations sont des éléments clés lorsque vous travaillez avec des données dans Jinja et Ansible. Elles permettent de parcourir des listes, des dictionnaires ou d’autres structures de données complexes pour générer automatiquement du contenu répétitif dans des fichiers de configuration, ou pour exécuter des tâches répétitives dans vos playbooks. Jinja facilite ces itérations à l’aide de la syntaxe {% for %}, ce qui permet de rendre vos templates Ansible dynamiques et adaptables à différents scénarios.

Syntaxe de base de la boucle for

La boucle for dans Jinja fonctionne de manière similaire aux boucles en Python. Elle permet d’itérer sur des listes, des dictionnaires ou d’autres structures de données, tout en exécutant des actions spécifiques à chaque itération. Voici la syntaxe de base pour utiliser une boucle dans un template Jinja :

{% for item in items %}
{{ item }}
{% endfor %}

Cette boucle parcourra chaque élément de la liste items et l’affichera. Utilisée dans un contexte Ansible, cette syntaxe peut par exemple générer des lignes de configuration à partir d’une liste de serveurs ou d’utilisateurs.

Jinja permet aussi d’itérer sur des dictionnaires, en accédant à la fois aux clés et aux valeurs à chaque itération. C’est particulièrement utile pour des configurations qui nécessitent à la fois des noms et des paramètres associés.

Voici un exemple où l’on itère sur un dictionnaire de services pour générer une configuration de démarrage automatique.

{% for service, status in services %}
service {{ service }} {{ status }}
{% endfor %}

Si la variable services est un dictionnaire de ce type :

services:
nginx: enabled
mysql: disabled
php-fpm: enabled

Le template générera le contenu suivant :

Terminal window
service nginx enabled
service mysql disabled
service php-fpm enabled

Variables spéciales dans les boucles

Jinja fournit plusieurs variables spéciales dans les boucles pour rendre l’itération plus flexible. Ces variables permettent d’accéder à des informations supplémentaires sur la boucle elle-même, comme l’index actuel, le nombre total d’éléments, ou encore la parité de l’élément (pair ou impair).

Variables spéciales courantes :

  • loop.index : Donne l’index 1-based de l’élément actuel (1, 2, 3, …).
  • loop.index0 : Donne l’index 0-based de l’élément actuel (0, 1, 2, …).
  • loop.revindex : Donne l’index inverse de la boucle (décompte depuis la fin).
  • loop.length : Donne la longueur totale de la liste ou du dictionnaire itéré.
  • loop.first : Renvoie True si c’est le premier élément de la boucle.
  • loop.last : Renvoie True si c’est le dernier élément de la boucle.

Voici un exemple où on utilise des variables de boucle pour afficher l’index de chaque élément et ajouter une ligne spéciale pour le dernier élément.

{% for host in hosts %}
{{ loop.index }}. Host: {{ host }}
{% if loop.last %}
(Last host in the list)
{% endif %}
{% endfor %}

Si la variable hosts contient une liste de serveurs, ce template générera :

Terminal window
1. Host: webserver1
2. Host: dbserver1
(Last host in the list)

Les inclusions dans les templates Jinja

L’utilisation des inclusions dans les templates Jinja permet de diviser vos fichiers de configuration en blocs réutilisables et modulaires. Plutôt que d’avoir un seul grand template contenant toutes les directives, vous pouvez organiser et réutiliser des parties de code commun en les séparant dans différents fichiers. Cela améliore la lisibilité, facilite la maintenance, et évite la duplication de code.

Dans Ansible, les inclusions sont particulièrement utiles pour des configurations complexes où certaines parties peuvent être communes à plusieurs systèmes ou services. L’instruction include de Jinja vous permet d’intégrer un autre fichier template directement à l’endroit où vous en avez besoin dans le fichier principal.

Syntaxe de base de l’inclusion

La directive include de Jinja s’utilise pour insérer un autre template à l’intérieur du fichier principal. La syntaxe est la suivante :

{% include 'chemin_du_template.j2' %}

Le template spécifié sera inséré à cet endroit lors du rendu. Vous pouvez inclure des templates multiples, et ils peuvent contenir eux-mêmes des variables, des boucles, ou d’autres logiques Jinja.

# Fichier principal (main_config.j2)
server {
listen 80;
server_name {{ server_name }};
{% include 'ssl_config.j2' %}
}

Dans cet exemple, le fichier ssl_config.j2 est inclus à l’intérieur du fichier principal main_config.j2, ajoutant une configuration SSL commune à plusieurs fichiers de configuration serveur.

Avantages de l’inclusion

L’inclusion dans les templates présente plusieurs avantages pour les administrateurs systèmes :

  • Modularité : Vous pouvez diviser vos fichiers de configuration en plusieurs morceaux logiques (par exemple, configuration réseau, configuration SSL, etc.), ce qui rend la gestion plus simple.
  • Réutilisabilité : Vous pouvez réutiliser des parties de templates dans plusieurs fichiers sans avoir à dupliquer le code. Cela est utile si vous avez des blocs de configuration identiques pour différents services.
  • Maintenance facilitée : Lorsque vous avez besoin de changer une partie commune de la configuration, vous pouvez modifier un seul template inclus plutôt que de devoir modifier plusieurs fichiers de configuration.

Conclusion

Dans ce guide, j’ai exploré l’utilisation de Jinja avec Ansible, en mettant l’accent sur des fonctionnalités telles que les filtres, les boucles, les conditions, et l’inclusion de templates. Ces outils permettent de générer des fichiers de configuration dynamiques et modulaires, tout en assurant une gestion flexible et optimisée de vos infrastructures. Grâce à ces techniques, vous pouvez simplifier et sécuriser la création de vos configurations automatisées, tout en réduisant la duplication de code.

Pour une compréhension plus approfondie de Jinja, notamment sa syntaxe et ses fonctionnalités avancées, je vous invite à vous référer au cours Python, où j’ai déjà détaillé le fonctionnement de ce moteur de templates de manière plus complète. Cela vous permettra de maîtriser pleinement les aspects de Jinja applicables à d’autres contextes, en dehors d’Ansible.