Aller au contenu
Infrastructure as Code medium

Jinja dans Salt : variables, boucles, filtres et macros

8 min de lecture

Un state figé se répète. Jinja le rend générique : il s'exécute avant le YAML, injecte des variables, déroule des boucles et prend des décisions selon le minion. Ce module apprend à générer automatiquement une configuration Nginx qui s'adapte à chaque serveur. Public visé : intermédiaires à l'aise avec les states et les grains.

  • Utiliser variables, conditions et boucles dans un state.
  • Appliquer des filtres Jinja courants.
  • Factoriser du code avec des macros.
  • Générer un fichier de configuration depuis des données.

Point clé souvent mal compris : Salt rend le Jinja d'abord, puis interprète le YAML résultant. Les balises {% ... %} portent la logique (boucles, conditions), les balises {{ ... }} insèrent une valeur. Le contexte donne accès aux grains, aux pillars et aux fonctions Salt.

# le paquet dépend du système, via grain
{% if grains['os_family'] == 'Debian' %}
apache2:
pkg.installed: []
{% elif grains['os_family'] == 'RedHat' %}
httpd:
pkg.installed: []
{% endif %}

Les boucles évitent de recopier vingt fois le même bloc. Combinées aux pillars, elles génèrent des states à partir de données : une liste de paquets, de vhosts ou d'utilisateurs décrite une fois, déroulée par Jinja.

# installer une liste de paquets issue d'un pillar
{% for paquet in salt['pillar.get']('paquets_web', []) %}
install-{{ paquet }}:
pkg.installed:
- name: {{ paquet }}
{% endfor %}

state.show_sls rend le state sans l'appliquer : c'est le moyen de vérifier qu'une boucle produit bien les états attendus. À partir d'un pillar paquets_web: [nginx, certbot], Jinja génère deux états pkg.installed :

Fenêtre de terminal
salt web1 state.show_sls paquets
install-certbot:
pkg: ...
install-nginx:
pkg: ...

Les filtres transforment une valeur avec la syntaxe valeur | filtre : default pour une valeur de repli, upper, join, ou les filtres réseau et yaml/json propres à Salt. Les macros factorisent un fragment réutilisable, comme un bloc de configuration paramétré, appelé plusieurs fois avec des arguments différents.

Pour appeler une fonction Salt depuis un template, utilisez la syntaxe mapping avec des crochets : salt['pillar.get']('paquets_web') ou salt['grains.get']('os_family'). C'est la forme recommandée par la documentation officielle, plus robuste que la notation pointée salt.pillar.get(...). Le deuxième argument fournit une valeur de repli : salt['pillar.get']('port', 80) évite un rendu qui échoue sur une donnée manquante.

{# macro de bloc serveur réutilisable #}
{% macro vhost(nom, port) -%}
server {
listen {{ port }};
server_name {{ nom }};
root /var/www/{{ nom }};
}
{%- endmacro %}

Retenez une règle de sécurité que la documentation Salt martèle : le rendu Jinja a lieu avant l'exécution du state, et il ignore test=True. Le mode simulation n'arrête donc jamais une commande lancée depuis Jinja. Concrètement, si un template appelle depuis Jinja une commande qui modifie le système (créer un fichier, installer un paquet), l'action s'exécute même en dry-run, et vous perdez à la fois le simulateur et l'idempotence.

La frontière est simple : Jinja lit et met en forme, il n'agit pas. Toute opération qui change l'état appartient au state (pkg.installed, file.managed, service.running), pas au template. Une lecture via salt['pillar.get'](...) est sans effet de bord et donc légitime ; un salt['cmd.run']('...') qui écrit quelque chose ne l'est pas.

Centraliser les spécificités OS dans un map.jinja

Section intitulée « Centraliser les spécificités OS dans un map.jinja »

Quand un template doit gérer plusieurs distributions, ne dispersez pas les noms de paquet et les chemins de configuration dans des {% if %} répétés. La documentation recommande de rassembler ces valeurs spécifiques à l'OS dans un fichier map.jinja réutilisable. La fonction grains.filter_by choisit le bon jeu de valeurs selon la famille d'OS du minion :

Fenêtre de terminal
salt web1 grains.filter_by \
'{"Debian": {"paquet": "nginx", "conf": "/etc/nginx/nginx.conf"}}' grain=os_family
web1:
----------
conf:
/etc/nginx/nginx.conf
paquet:
nginx

Un state importe ensuite ces valeurs avec {% from 'nginx/map.jinja' import nginx with context %}, puis référence nginx.paquet et nginx.conf. La logique de plateforme vit à un seul endroit, ce qui rend le state refactorable.

Assemblons boucle, macro et pillar. Une liste de vhosts vit dans un pillar, un template la déroule avec une macro par site, et file.managed le déploie avec l'option template: jinja.

/srv/pillar/web.sls
sites:
- nom: blog.exemple.fr
port: 80
- nom: api.exemple.fr
port: 8080

Le template combine la macro et la boucle sur le pillar :

{% macro vhost(nom, port) -%}
server {
listen {{ port }};
server_name {{ nom }};
root /var/www/{{ nom }};
}
{%- endmacro -%}
{%- for site in salt['pillar.get']('sites', []) %}
{{ vhost(site.nom, site.port) }}
{% endfor -%}

Le state le rend et l'écrit sur la cible :

/etc/nginx/conf.d/sites-generes.conf:
file.managed:
- source: salt://sites.conf.j2
- template: jinja
- mode: "0644"

Après application, le fichier généré contient un bloc par site, sans qu'on ait écrit deux fois le même modèle :

Fenêtre de terminal
salt web1 state.apply vhosts
salt web1 cmd.run 'cat /etc/nginx/conf.d/sites-generes.conf'
server {
listen 80;
server_name blog.exemple.fr;
root /var/www/blog.exemple.fr;
}
server {
listen 8080;
server_name api.exemple.fr;
root /var/www/api.exemple.fr;
}

Le second passage ne modifie rien : le rendu est déterministe, file.managed idempotent.

Comment: File /etc/nginx/conf.d/sites-generes.conf is in the correct state

Ajouter un site revient désormais à ajouter une entrée dans le pillar, pas à éditer la configuration : c'est tout l'intérêt de générer depuis des données.

  1. Salt rend le Jinja avant le YAML : {% %} pour la logique, {{ }} pour les valeurs.
  2. Les boucles sur des pillars génèrent des states à partir de données ; state.show_sls vérifie le rendu sans appliquer.
  3. Les filtres transforment les valeurs ; default évite les états indéfinis.
  4. Les macros factorisent les fragments répétés.
  5. Appelez Salt en syntaxe mapping : salt['pillar.get']('foo:bar'), jamais salt.pillar.get(...).
  6. Jinja ne modifie jamais le système : il ignore test=True, donc une commande d'écriture lancée depuis un template casse simulation et idempotence.
  7. Centralisez les spécificités OS dans un map.jinja via grains.filter_by.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn