Ad majorem lulzis gloriam
Vomito ergo sum


Ansible et la génération magique de variables

Posted on

Ansible, c’est de la merde.

Schtroumpf grognon, 2022

Je n’aime pas Ansible. Venant à l’origine de SaltStack, je le trouve hyper limité et étriqué dans sa façon de fonctionner : ne favorise pas vraiment la réutilisabilité, les variables sont toutes mises dans le même sac à la fin, accéder à n’importe quelle variable est hyper confus, il n’y a pas de système de signaux permettant d’échanger avec le serveur.

Sans compter le fait que même l’exécution des modules de base inclus dans Ansible nécessite parfois l’installation de la moitié de pypi.

Mais il a néamoins deux ou trois trucs pour lui : pas d’agent (j’ai du mal à classer ça dans les avantages, mais dans certaines circonstances, ne pas avoir à se préoccuper de l’agent, c’est plutôt une bonne chose) et surtout une courbe d’apprentissage nettement moins raide que SaltStack (où tu rotes du sang régulièrement…).

Bref, laisse-moi te conter la mésaventure qui m’est arrivé au détour d’un role Ansible

Les données du problème

Admettons que j’ai un role dont le but est de tester le retour d’un GET HTTP. On lui done une liste avec pour chaque entrée :

Et ce role se charge d’aller faire la requête GET et de vérifier que le mot-clé est bien dans le résultat. Un truc plutôt basique.

Tu pourrais imaginer de faire un truc comme ça :

test_http:
  - uri: "https://blog.libertus.eu"
    urn: "/ne-m-appelez-plus-prive"
    result: "LOLILOL"
  - uri: "https://www.elysee.fr"
    urn: "/emmanuel-macron"
    result: "connard"

Le role va donc faire une boucle simple (probablement avec loop) sur le contenu de la variable test_http et renvoyer à chaque fois le résultat d’un assert.

Jusqu’ici tout va bien. Mais admettons que je doive générer cette liste. Elle n’est plus bêtement statique, je dois la générer. Mettons par exemple que je veuille vérifier que l’ensemble des serveurs dans un groupe donné sont bien enregistrés dans un consul ou un système de gestions de parc ou n’importe quoi d’autres.

This is SALTSTACK!!

Côté SaltStack, il n’y a pas de souci particulier : tous les fichiers sont interprétés en Jinja avant d’être interprétés en Yaml. On peut donc finalement templater un peu n’importe où, cela ne pose jamais de problème.

Pour résoudre un problème de ce type, on aurait probablement faire qqch du genre:

test_http:
{% for host in hosts %} # on admettra ici que la variable hosts contient ce qu’il faut
  - uri: "http://consul.server"
    urn: "/v1/catalog/nodes"
    result: "{{ host }}"
{% endfor %}

Tout ceci dans un pillar quelconque avec le state qui va bien derrière, rien de bien compliqué en soi.

Le très gros avantage d’avoir du Jinja à tous les étages, c’est qu’on peut finalement templater un peu n’importe quoi, un peu n’importe où sans trop se préoccuper. C’est ainsi qu’il n’existe pas vraiment d’équivalent de loop dans SaltStack : on te dira toujours te de débrouiller pour faire la même chose en Jinja et puis voilà.

Le Chad SaltStack / Le Virgin Ansible

Dans Ansible, on ne peut pas coller du Jinja n’importe où. L’ensemble des fichiers que manipulent Ansible (à l’exception du ansible.cfg et des fichiers d’inventaire qui peuvent être en INI), tout doit être du pur Yaml.

D’où un certain nombre d’aberrations genre loop et la tétrachiée de filters qu’il faut se trimballer pour arriver à l’utiliser (jetez juste un œil à cette page si vous voulez vous en convaincre…).

Mais, ça ne veut pas dire qu’on ne peut pas du tout utiliser des templates Jinja. On peut, simplement, il faut les limiter aux variables.

Une première approche consisterait donc à tenter de produire un template pour essayer de générer du Yaml :

hosts:
  - ta
  - mere
  - lol

test_http: |-
  {% for host in hosts %}
  - uri: "http://consul.server"
    urn: "/v1/catalog/nodes"
    result: "{{ host }}"
  {% endfor %}

Sauf que quand on essaie d’afficher cette variable, on obtient juste une chaîne de caractères :

TASK [display var] **********
ok: [localhost] => {
    "msg": "- uri: \"http://consul.server\"\n  urn: \"/v1/catalog/nodes\"\n  result: \"ta\"\n- uri: \"http://consul.server\"\n  urn: \"/v1/catalog/nodes\"\n  result: \"mere\"\n- uri: \"http://consul.server\"\n  urn: \"/v1/catalog/nodes\"\n  result: \"lol\"\n"
}

PERDU !

Si on remet les retours chariot un peu dans l’ordre, on se rend pourtant bien compte qu’on a effectivement généré ce qu’il faut, mais visiblement, Ansible n’a pas vraiment envie de s’en servir.

En vrai, il y a un astuce… Il ne faut pas templater du Yaml, il faut templater du JSON. Oui, c’est vraiment un truc de clown…

Donc en fait, si tu fais exactement la même chose mais avec cette syntaxe :

hosts:
  - ta
  - mere
  - lol

test_http: |-
  [
  {% for host in hosts %}
  {
    "uri": "http://consul.server",
    "urn": "/v1/catalog/nodes",
    "result": "{{ host }}"
  },
  {% endfor %}
  ]

Et là, le miracle s’accomplit :

TASK [display var] *********
ok: [localhost] => {
    "msg": [
        {
            "result": "ta",
            "uri": "http://consul.server",
            "urn": "/v1/catalog/nodes"
        },
        {
            "result": "mere",
            "uri": "http://consul.server",
            "urn": "/v1/catalog/nodes"
        },
        {
            "result": "lol",
            "uri": "http://consul.server",
            "urn": "/v1/catalog/nodes"
        }
    ]
}

Mais attention, hein ! Il ne faut surtout pas oublier la moindre virgule et en particulier celle qui se trouve en toute fin de variable },, sinon tout est de nouveau interprété comme une chaîne de caractères générée.

Foutaises

Donc, oui, on peut génerer des variables dans Ansible mais juste, c’est tellement bordélique qu’on peut quand même se poser des questions de pourquoi, c’est foutu comme ça. Générer une variable Yaml en passant par du JSON templaté en Jinja, c’est pas vraiment le truc le plus instinctif de la planète…