Aller au contenu principal

Découverte d'Ansible Event Driven

· 9 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Annoncé à l'ansibleFest 2022, ansible-rulebook apporte à Ansible la possibilité de déclencher des actions à partir d'un événement. En effet, jusqu'à maintenant déclencher des playbooks Ansible suite à la survenue d'un événement nous demandait d'écrire et de faire tourner régulièrement des jobs charger de les collecter et de déclencher les traitements adéquats.

Pour rappel, une des tâches qui nous incombent est de chercher à automatiser un maximum de tache sans valeurs ajoutées. Par exemple, on devrait ne pas intervenir sur des incidents simples ou la correction est connue, ou traiter de simples demandes en provenance de nos clients. Je suis sûr que comme moi, vous aimeriez pouvoir vous concentrer sur d'autres tâches à plus fortes valeurs ajoutées que de traiter des tickets en série. Par exemple, pour ce qui est du traitement des incidents, à défaut de pouvoir les résoudre automatiquement, nous devrions au moins pouvoir automatiser la collecte des informations nécessaires à l'identification de leurs causes premières.

Voyons ensemble comment fonctionne ce nouvel outil Ansible : ansible-rulebook.

Ansible-rulebook

Commençons par l'installer :

Installation d'ansible-rulebook

Pour tester cet outil, comme d'habitude, je le fais en utilisant une VM popé sur mon poste de travail avec Vagrant :

vagrant init generic/debian11
vagrant up
vagrant ssh

Maintenant en parcourant la documentation, nous voyons qu'il existe deux manières de l'installer : une avec Ansible et une manuelle, je vais prendre la seconde solution, car j'aime bien savoir ce qu'il se passe :

sudo apt install python3-pip openjdk-17-jdk
pip install ansible --user
export PATH=$PATH:$HOME/.local/bin
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PIP_NO_BINARY=jpy
pip install wheel ansible-rulebook ansible ansible-runner
ansible-galaxy collection install community.general ansible.eda

Voilà tout est prêt. Passons à l'écriture de nos premiers rulebooks :

Ecriture des rulebooks

La documentation est déjà disponible.

Un exemple de rulebook :

---
- name: Hello Events
  hosts: localhost
  sources:
    - ansible.eda.range:
        limit: 5
  rules:
    - name: Say Hello
      condition: event.i == 1
      action:
        run_playbook:
          name: ansible.eda.hello
...

Ansible donc yaml. J'en connais qui vont râler. Mais pour ceux qui codent des playbooks Ansible, la syntaxe sera simple à prendre en main.

Nous retrouvons trois sections : hosts, sources et rules. sources permet de définir d'où provient l'événement et rules pour les traitements à lancer. Pour qu'une action soit lancée, elle doit répondre à une condition de type booléen. Ces conditions sont les mêmes que celles que nous utilisons dans nos conditions when.

Dans l'exemple ci-dessus la source utilise un range python qui compte de 0 à 5. La règle définit une action qui se déclenche sur l'événement i==1, ici le lancement d'un playbook : ansible.eda.hello

Pour lancer l'exécution du rulebook, il faut définir un inventaire :

all:
  hosts:
    localhost:
      ansible_connection: local

Ici localhost avec la connexion de type local. On a tout ce qu'il faut pour lancer le rulebook 👍:

ansible-rulebook --rulebook rule1.yaml -i inventory.yml --verbose
INFO:ansible_rulebook.app:Starting sources
INFO:ansible_rulebook.app:Starting rules
INFO:ansible_rulebook.engine:run_ruleset
INFO:ansible_rulebook.rule_generator:{'all': [{'m': {'i': 1}}], 'run': <function make_fn.<locals>.fn at 0x7f6b194b8790>}
INFO:ansible_rulebook.engine:ruleset define: ('Hello Events', {'r_0': {'all': [{'m': {'i': 1}}], 'run': <function make_fn.<locals>.fn at 0x7f6b194b8790>}})
INFO:ansible_rulebook.engine:load source
INFO:ansible_rulebook.engine:load source filters
INFO:ansible_rulebook.engine:Calling main in ansible.eda.range
INFO:ansible_rulebook.engine:Waiting for event from Hello Events
INFO:ansible_rulebook.rule_generator:calling Say Hello
INFO:ansible_rulebook.engine:call_action run_playbook
INFO:ansible_rulebook.engine:substitute_variables [{'name': 'ansible.eda.hello'}] [{'event': {'i': 1}, 'fact': {'i': 1}}]
INFO:ansible_rulebook.engine:action args: {'name': 'ansible.eda.hello'}
INFO:ansible_rulebook.builtin:running Ansible playbook: ansible.eda.hello
INFO:ansible_rulebook.builtin:ruleset: Hello Events, rule: Say Hello
INFO:ansible_rulebook.builtin:Calling Ansible runner

PLAY [hello] *******************

TASK [debug] *******************
ok: [localhost] => {
    "msg": "hello"
}

PLAY RECAP *********************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
INFO:ansible_rulebook.engine:Canceling all ruleset tasks
INFO:ansible_rulebook.app:Cancelling event source tasks
INFO:ansible_rulebook.app:Main complete

Cela a produit le résultat attendu. Passons à un autre exemple avec l'utilisation d'un webhook.

---
- name: Listen for events on a webhook
  hosts: all

  ## Define our source for events

  sources:
    - ansible.eda.webhook:
        host: 0.0.0.0
        port: 5000

  ## Define the conditions we are looking for

  rules:
    - name: Say Hello
      condition: event.payload.message == "Ansible is super cool!"

  ## Define the action we should take should the condition be met

      action:
        run_playbook:
          name: ansible.eda.hello

La source est cette fois un webhook qui écoute sur le port 5000. Pour que l'exemple fonctionne, il faut installer au préalable le package python aiohttp.

pip install aiohttp

On peut lancer ansible-rulebook :

ansible-rulebook --rulebook rule2.yaml -i inventory.yml --verbose
INFO:ansible_rulebook.app:Starting sources
INFO:ansible_rulebook.app:Starting rules
INFO:ansible_rulebook.engine:run_ruleset
INFO:ansible_rulebook.rule_generator:{'all': [{'m': {'payload.message': 'Test'}}], 'run': <function make_fn.<locals>.fn at 0x7f5f6a28d820>}
INFO:ansible_rulebook.engine:ruleset define: ('Listen for events on a webhook', {'r_0': {'all': [{'m': {'payload.message': 'Test'}}], 'run': <function make_fn.<locals>.fn at 0x7f5f6a28d820>}})
INFO:ansible_rulebook.engine:load source
INFO:ansible_rulebook.engine:load source filters
INFO:ansible_rulebook.engine:Calling main in ansible.eda.webhook
INFO:ansible_rulebook.engine:Waiting for event from Listen for events on a webhook

Contrairement à tout à l'heure, ansible-rulebook attend un événement. Lançons cet évènement depuis un autre terminal :

vagrant ssh
curl -H 'Content-Type: application/json' -d '{"message": "Test"}' 127.0.0.1:5000/endpoint

Que s'est-il passé dans l'autre terminal. On peut y lire ceci 👍

INFO:aiohttp.access:127.0.0.1 [18/Oct/2022:06:43:27 +0000] "POST /endpoint HTTP/1.1" 200 158 "-" "curl/7.74.0"
INFO:ansible_rulebook.rule_generator:calling Say Hello
INFO:ansible_rulebook.engine:call_action run_playbook
INFO:ansible_rulebook.engine:substitute_variables [{'name': 'ansible.eda.hello'}] [{'event': {'payload': {'message': 'Test'}, 'meta': {'endpoint': 'endpoint', 'headers': {'Host': '127.0.0.1:5000', 'User-Agent': 'curl/7.74.0', 'Accept': '*/*', 'Content-Type': 'application/json', 'Content-Length': '19'}}}, 'fact': {'payload': {'message': 'Test'}, 'meta': {'endpoint': 'endpoint', 'headers': {'Host': '127.0.0.1:5000', 'User-Agent': 'curl/7.74.0', 'Accept': '*/*', 'Content-Type': 'application/json', 'Content-Length': '19'}}}}]
INFO:ansible_rulebook.engine:action args: {'name': 'ansible.eda.hello'}
INFO:ansible_rulebook.builtin:running Ansible playbook: ansible.eda.hello
INFO:ansible_rulebook.builtin:ruleset: Listen for events on a webhook, rule: Say Hello
INFO:ansible_rulebook.builtin:Calling Ansible runner
incorrect line lengths

PLAY [hello] *******************

TASK [debug] *******************
ok: [localhost] => {
    "msg": "hello"
}

PLAY RECAP *********************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Le traitement s'est lancé et ansible-rulebook attend à nouveau des événements à traiter. Lançons d'autres tests en changeant le contenu du message.

On peut aussi trouver un exemple sur le dépôt du projet ansible-rulebook dans le répertoire demo.

vagrant ssh
curl -H 'Content-Type: application/json' -d '{"message": "Test2"}' 127.0.0.1:5000/endpoint

Dans l'autre terminal juste une ligne apparaît avec aucune trace d'un traitement lancé :

INFO:aiohttp.access:127.0.0.1 [18/Oct/2022:06:45:43 +0000] "POST /endpoint HTTP/1.1" 200 158 "-" "curl/7.74.0"

Passage de variables

Comment récupérez des variables et les transmettre aux playbooks? Les facts ?

Une des réponses, je l'ai trouvé en utilisant l'action debug. Remplaçons notre action dans l'exemple précédent (d'ailleurs, on ne peut en mettre qu'une seule).

...

      action:
        debug:

Si on relance modifiant notre appel curl avec ajout d'un champ :

curl -H 'Content-Type: application/json' -d '{"message": "Test", "Host": "test2"}' 127.0.0.1:5000/endpoint

On voit

INFO:ansible_rulebook.engine:action args: {}
===========================================
===========================================
facts:
[]
===========================================
kwargs:
{'facts': {},
 'hosts': ['all'],
 'inventory': {'all': {'hosts': {'localhost': {'ansible_connection': 'local'}}}},
 'project_data_file': None,
 'ruleset': 'Listen for events on a webhook',
 'source_rule_name': 'Say Hello',
 'source_ruleset_name': 'Listen for events on a webhook',
 'variables': {'event': {'meta': {'endpoint': 'endpoint',
                                  'headers': {'Accept': '*/*',
                                              'Content-Length': '36',
                                              'Content-Type': 'application/json',
                                              'Host': '127.0.0.1:5000',
                                              'User-Agent': 'curl/7.74.0'}},
                         'payload': {'Host': 'test2', 'message': 'Test'}},
               'fact': {'meta': {'endpoint': 'endpoint',
                                 'headers': {'Accept': '*/*',
                                             'Content-Length': '36',
                                             'Content-Type': 'application/json',
                                             'Host': '127.0.0.1:5000',
                                             'User-Agent': 'curl/7.74.0'}},
                        'payload': {'Host': 'test2', 'message': 'Test'}}}}
===========================================

Donc essayons d'afficher un des champs de variables dans l'action debug :

      action:
        debug:
          test: "{{ fact.payload.Host }}"

On voit dans la sortie apparaitre notre variable :

 'source_ruleset_name': 'Listen for events on a webhook',
 'test': 'test2',
 'variables': {'event': {'meta': {'endpoint': 'endpoint',

Donc maintenant pour l'ajouter invoque un playbook avec un argument :

      action:
        run_playbook:
          name: test.yaml

Dans ce playbook, je demande à afficher simplement hostvars :

---
- name: test
  hosts: all
  gather_facts: true
  tasks:
    - name: debug
      ansible.builtin.debug:
        var: fact

Dans les hostvars, je relance et bingo, fact est transmis pas besoin de se prendre la tête. Je modifie et je teste directement fact.

TASK [debug] *******************
ok: [localhost] => {
    "fact": {
        "meta": {
            "endpoint": "endpoint",
            "headers": {
                "Accept": "*/*",
                "Content-Length": "36",
                "Content-Type": "application/json",
                "Host": "127.0.0.1:5000",
                "User-Agent": "curl/7.74.0"
            }
        },
        "payload": {
            "Host": "test2",
            "message": "Test"
        }
    }
}
PLAY RECAP *********************

Différents types types de sources

Si on étudie la documentation, on voit que pour le moment ansible-rulebook propose des plugins par défaut. Qui dit plugin dit possibilité d'en coder soit même.

Parmi ceux fournis :

  • alertmanager : traiter des événements via un webhook en provenance d'alertmanager
  • azure_service_bus - traiter des événements d'un service Azure
  • file - charge les facts à partir de fichiers YAML et recharge lorsqu'un fichier change ?
  • kafka - traiter des événements d'un stream kafka
  • range - un range python
  • tick - un index croissant i illimité
  • url_check - interroge un ensemble d'URL et envoie des événements avec leurs statuts
  • watchdog - surveille le système de fichiers et envoie des événements lorsqu'un statut d'un fichier change
  • none - vu dans l'exemple ci-dessus

D'autres types d'actions

  • run_module - lancement d'un simple module Ansible
  • run_playbook - lancement d'un playbook via ansible-runner
  • set_fact - définir des facts
  • retract_fact - efface un facts
  • post_event - envoie d'autres événements pour enchainer des traitements
  • debug - affiche l'événement et ses facts avec ses arguments
  • print_event - affiche l'événement
  • noop - ne fais rien
  • shutdown - arrête ansible-rulebook

Ils sont tous documentés ici

Plus loin avec ansible-rulebook

J'imagine déjà plein de cas d'usage pour cet outil, comme :

  • lancer et enchainer des playbooks sur des machines qui viennent d'être créé sur notre infrastructure. J'imagine simplifier pas mal certains playbooks enchaînant des tas de roles et tasks en les découpant.
  • traiter des demandes clientes depuis notre outil de ticketing ou des appels via curl (mais quid de la partie droit ?)
  • et bien traiter des incidents bien identifiés.
  • traiter le patch management avec reprise auto des erreurs connues.
  • ...

Mauvaise nouvelle, pour le moment l'extension vscode ansible ne prend pas en charge ce type de fichier.

Mais je vais approfondir longuement ce nouvel outil.