What it is?
Redhat says:
Event-Driven Ansible is a new way to enhance and expand automation. It improves IT speed and agility, while enabling consistency and resilience. The Event-Driven Ansible technology was developed by Red Hat and is available as a developer preview. Community input is essential. Since we are building a solution to best meet your needs, we’re providing an opportunity for you to advocate for those needs.
How does it work?
In Ansible you write Playbooks. In Event Driven Ansible you write rulebook. But instead of just defining what tasks should be run, you define several more things:
First you define some sources that Event-Driven Ansible listens to. This can be webooks, alertmanagers, changed URLs or files.
Then you define rules. These rules state what should happen (actions) and when (conditions) it should happen.
Conditions can be a triggered alert from the Alertmanager or an unreachable website, a newly created file or just a message that was received from the webook.
Actions define, what should happen after a condition is triggered. Probably the most common action will be run_playbook which executes a defined playbook. Other actions can be running a module (instead of a whole playbook) or setting a fact.
Here’s an example.
This rulebook listens starts a webhook on port 5000 and listens for incoming connections. When an incoming connection has a payload that says Ansible is super cool, the „Say Hello“ rule’s condition will be met. Then the action will be executed, in this case the playbook say-what.yml will be executed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
--- - 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: say-what.yml |
Why?
Traditionally Ansible follows a push-based model where you trigger runs that change something.
With Event Driven Ansible you can perform these changes automatically based on your defined conditions. This can make some manual tasks superfluous:
Imagine getting a ticket to create a new user in your organization. Event Driven Ansible can check for this new ticket and act on it automatically.
It can also help in troubleshooting issues. When your monitoring notices that some systems failed, Event Driven Ansible can watch for these failures and collect valuable information on the failed systems.
Now let’s take a look into it!
Installation
The Installation is simple – I just followed the official documentation and installed java, ansible, ansible-rulebook and some collections.
1 2 3 4 5 6 7 8 9 |
apt-get --assume-yes install build-essential maven openjdk-17-jdk python3-dev python3-pip export JDK_HOME=/usr/lib/jvm/java-17-openjdk-amd64 export JAVA_HOME=$JDK_HOME export PIP_NO_BINARY=jpy export PATH=$PATH:~/.local/bin pip3 install -U Jinja2 pip3 install ansible ansible-rulebook ansible-runner wheel ansible-galaxy collection install community.general ansible.eda |
Running
When first running ansible-rulebook you get zero output:
1 2 |
> ansible-rulebook > |
Running with -h is better and provides the help:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
> ansible-rulebook -h usage: ansible-rulebook [-h] [--rulebook RULEBOOK] [--vars VARS] [--env-vars ENV_VARS] [--debug] [--verbose] [--version] [--redis-host-name REDIS_HOST_NAME] [--redis-port REDIS_PORT] [-S SOURCE_DIR] [-i INVENTORY] [--websocket-address WEBSOCKET_ADDRESS] [--id ID] [--worker] [--project-tarball PROJECT_TARBALL] options: -h, --help show this help message and exit --rulebook RULEBOOK The rulebook file or rulebook from a collection --vars VARS Variables file --env-vars ENV_VARS Comma separated list of variables to import from the environment --debug Show debug logging --verbose Show verbose logging --version Show the version and exit --redis-host-name REDIS_HOST_NAME Redis host name --redis-port REDIS_PORT Redis port -S SOURCE_DIR, --source-dir SOURCE_DIR Source dir -i INVENTORY, --inventory INVENTORY Inventory --websocket-address WEBSOCKET_ADDRESS Connect the event log to a websocket --id ID Identifier --worker Enable worker mode --project-tarball PROJECT_TARBALL A tarball of the project |
Thinking that it should work somehow like ansible-playbook I just passed my aforementioned rulebook:
1 2 3 4 5 6 7 |
> ansible-rulebook webhook-rule.yml usage: ansible-rulebook [-h] [--rulebook RULEBOOK] [--vars VARS] [--env-vars ENV_VARS] [--debug] [--verbose] [--version] [--redis-host-name REDIS_HOST_NAME] [--redis-port REDIS_PORT] [-S SOURCE_DIR] [-i INVENTORY] [--websocket-address WEBSOCKET_ADDRESS] [--id ID] [--worker] [--project-tarball PROJECT_TARBALL] ansible-rulebook: error: unrecognized arguments: webhook-rule.yml |
No dice. However, with –rulebook I got one step further.
1 2 3 |
> ansible-rulebook --rulebook webhook-rule.yml Error: inventory is required |
Creating a simple inventory and using it finally worked:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
> ansible-rulebook --rulebook webhook-rule.yml -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.engine:ruleset define: {"name": "Listen for events on a webhook", "hosts": ["all"], "sources": [{"EventSource": {"name": "ansible.eda.webhook", "source_name": "ansible.eda.webhook", "source_args": {"host": "0.0.0.0", "port": 5000}, "source_filters": []}}], "rules": [{"Rule": {"name": "Say Hello", "condition": {"AllCondition": [{"EqualsExpression": {"lhs": {"Event": "payload.message"}, "rhs": {"String": "Ansible is super cool!"}}}]}, "action": {"Action": {"action": "run_playbook", "action_args": {"name": "say-what.yml"}}}, "enabled": true}}]} 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 |
So now there is a webhook listening locally on port 5000. Let’s curl it:
1 2 |
> curl http://localhost:5000 404: Not Found |
Okay, as I have to send a payload to the webhook, that was expected. Let’s POST some simple message to the webhook.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
> curl -X --data "foo" http://localhost:5000 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <meta type="copyright" content="Copyright (C) 1996-2021 The Squid Software Foundation and contributors"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>ERROR: The requested URL could not be retrieved</title> <style type="text/css"><!-- /* |
This time I got another error. It seems ansible-rulebook uses a squid proxy internally. The ansible-rulebook-process also threw an error.
1 2 3 4 5 6 7 8 9 10 11 |
ERROR:aiohttp.server:Error handling request Traceback (most recent call last): File "/home/segu/.local/lib/python3.10/site-packages/aiohttp/web_protocol.py", line 334, in data_received messages, upgraded, tail = self._request_parser.feed_data(data) File "aiohttp/_http_parser.pyx", line 551, in aiohttp._http_parser.HttpParser.feed_data aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'Invalid method encountered'" |
After consulting the documentation I found the correct URL to POST to and also what to data to post: A JSON-Payload to localhost:5000/endpoint:
1 |
> curl -H 'Content-Type: application/json' -d "{\"message\": \"Ansible is super cool\"}" http://127.0.0.1:5000/endpoint |
Now we’re talking! The ansible-rulebook picked up the message and acted on it by executing the defined playbook. You can spot the familiar ansible-playbook output.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
INFO:ansible_rulebook.engine:action args: {'name': 'say-what.yml'} INFO:ansible_rulebook.builtin:running Ansible playbook: say-what.yml INFO:ansible_rulebook.builtin:ruleset: Listen for events on a webhook, rule: Say Hello INFO:ansible_rulebook.builtin:Calling Ansible runner PLAY [say thanks] ************************************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [debug] ******************************************************************* ok: [localhost] => { "msg": "say what" } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 |
Here’s the playbook that was executed:
1 2 3 4 5 |
- hosts: localhost connection: local tasks: - debug: msg: "Thank you, my friend!" |
A cool thing: The playbook is dynamically loaded, so it can be changed without needing to restart the rulebook. Changes to the rulebook do require a restart of ansible-rulebook htough.
The first simple example worked! Let’s check out further what we can to with Event Driven Ansible.
Event Sources
As explained above Event Driven Ansible has different event sources and I demonstrated the webhook source. Let’s try the url_check source. The official documentation for this source module is lacking (for now). So let’s look at the source code:
An ansible-rulebook event source plugin that polls a set of URLs and sends events with their status.
1 2 3 4 5 6 7 8 9 10 |
Arguments: urls - a list of urls to poll delay - the number of seconds to wait between polling Example: - name: check web server ansible.eda.url_check: urls: - http://44.201.5.56:8000/docs delay: 10 |
This seems quite easy to implement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- name: Ruleset for URL check source test hosts: all sources: - name: check site ansible.eda.url_check: urls: - https://www.wikipedia.org rules: - name: Check if endpoint is responding condition: event.url_check.status == "up" action: run_module: name: ansible.builtin.debug module_args: msg: SUCCESS |
Here I’m using another action, the run_module. This module does not execute an Ansible Playbook but rather just a single task. In this example it should just pring SUCCESS.
The condition checks if the status of the website is up.
And after some considerable waiting it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
> ansible-rulebook --rulebook url-check-rule.yml -i inventory.yml --debug > DEBUG:asyncio:Using selector: EpollSelector DEBUG:ansible_rulebook.app:Loading rules from the file system url-check-rule.yml INFO:ansible_rulebook.app:Starting sources INFO:ansible_rulebook.app:Starting rules INFO:ansible_rulebook.engine:run_ruleset INFO:ansible_rulebook.engine:ruleset define: {"name": "Ruleset for URL check source test", "hosts": ["all"], "sources": [{"EventSource": {"name": "check site", "source_name": "ansible.eda.url_check", "source_args": {"urls": ["https://www.wikipedia.org"]}, "source_filters": []}}], "rules": [{"Rule": {"name": "Check if endpoint is responding", "condition": {"AllCondition": [{"EqualsExpression": {"lhs": {"Event": "url_check.status"}, "rhs": {"String": "up"}}}]}, "action": {"Action": {"action": "run_module", "action_args": {"name": "ansible.builtin.debug", "module_args": {"msg": "SUCCESS"}}}}, "enabled": true}}]} INFO:ansible_rulebook.engine:load source INFO:ansible_rulebook.engine:load source filters INFO:ansible_rulebook.engine:Calling main in ansible.eda.url_check INFO:ansible_rulebook.engine:Waiting for event from Ruleset for URL check source test INFO:ansible_rulebook.rule_generator:calling Check if endpoint is responding DEBUG:ansible_rulebook.engine:None INFO:ansible_rulebook.engine:call_action run_module INFO:ansible_rulebook.engine:substitute_variables [{'name': 'ansible.builtin.debug', 'module_args': {'msg': 'SUCCESS'}}] [{'event': {'url_check': {'status_code': 200, 'url': 'https://www.wikipedia.org', 'status': 'up'}}, 'fact': {'url_check': {'status_code': 200, 'url': 'https://www.wikipedia.org', 'status': 'up'}}}] INFO:ansible_rulebook.engine:action args: {'name': 'ansible.builtin.debug', 'module_args': {'msg': 'SUCCESS'}} DEBUG:ansible_rulebook.builtin:private data dir /tmp/run_moduley9v5opta DEBUG:ansible_rulebook.builtin:variables {'event': {'url_check': {'status_code': 200, 'url': 'https://www.wikipedia.org', 'status': 'up'}}, 'fact': {'url_check': {'status_code': 200, 'url': 'https://www.wikipedia.org', 'status': 'up'}}} DEBUG:ansible_rulebook.builtin:facts {} DEBUG:ansible_rulebook.builtin:project_data_file: None INFO:ansible_rulebook.builtin:Calling Ansible runner localhost | SUCCESS => { "msg": "SUCCESS" } ... | SUCCESS => { "msg": "SUCCESS" } |
Now you could use this to monitor some websites and in case of an error do some automated recovery or collect troubleshooting information. However, at the time of this writing, the module isn’t really stable.
file_watch
Let’s take a look at another event source, the file_watch. This module is so new, it isn’t even mentioned on the documentation yet. The source code however provides some docs:
1 2 3 4 5 6 7 8 9 10 11 |
An ansible-rulebook event source plugin for watching file system changes. Arguments: path: The directory to watch for changes. ignore_regexes: A list of regular expressions to ignore changes recursive: Recursively watch the path if true Example: - name: file_watch file_watch: path: "{{src_path}}" recursive: true ignore_regexes: ['.*\\.pytest.*', '.*__pycache__.*', '.*/.git.*'] |
Implementing it seems easy at first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- name: Ruleset for file watching hosts: all sources: - name: check files ansible.eda.file_watch: path: "./" rules: - name: Check if file exists condition: event.file_watch == true action: run_module: name: ansible.builtin.debug module_args: msg: SUCCESS |
But I don’t know what condition I can use. The above does not work and I wasn’t able to find out what conditions are accepted. To fix this I created an issue.
Actions
In the previous examples I already used two different actions:
- the run_playbook action executes a playbook
- the run_module action executes a module.
There are other actions and the one I used to find out more about payloads and events is the debug action.
Here’s how to use it:
1 2 3 4 5 6 7 8 9 10 11 12 |
- name: Ruleset for URL check source test hosts: all sources: - name: check site ansible.eda.url_check: urls: - https://www.sbk.org rules: - name: Check if endpoint is responding condition: event.url_check.status == "up" action: debug: |
And here it is in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
> ansible-rulebook --rulebook debug-rule.yml -i inventory.yml --debug DEBUG:asyncio:Using selector: EpollSelector DEBUG:ansible_rulebook.app:Loading rules from the file system debug-rule.yml INFO:ansible_rulebook.app:Starting sources INFO:ansible_rulebook.app:Starting rules INFO:ansible_rulebook.engine:run_ruleset INFO:ansible_rulebook.engine:ruleset define: {"name": "Ruleset for URL check source test", "hosts": ["all"], "sources": [{"EventSource": {"name": "check site", "source_name": "ansible.eda.url_check", "source_args": {"urls": ["https://www.sbk.org"]}, "source_filters": []}}], "rules": [{"Rule": {"name": "Check if endpoint is responding", "condition": {"AllCondition": [{"EqualsExpression": {"lhs": {"Event": "url_check.status"}, "rhs": {"String": "up"}}}]}, "action": {"Action": {"action": "debug", "action_args": {}}}, "enabled": true}}]} INFO:ansible_rulebook.engine:load source INFO:ansible_rulebook.engine:load source filters INFO:ansible_rulebook.engine:Calling main in ansible.eda.url_check INFO:ansible_rulebook.engine:Waiting for event from Ruleset for URL check source test INFO:ansible_rulebook.rule_generator:calling Check if endpoint is responding DEBUG:ansible_rulebook.engine:None INFO:ansible_rulebook.engine:call_action debug INFO:ansible_rulebook.engine:substitute_variables [{}] [{'event': {'url_check': {'status_code': 200, 'url': 'https://www.sbk.org', 'status': 'up'}}, 'fact': {'url_check': {'status_code': 200, 'url': 'https://www.sbk.org', 'status': 'up'}}}] INFO:ansible_rulebook.engine:action args: {} ===================================================================================================================================================================================================================== kwargs: {'facts': {}, 'hosts': ['all'], 'inventory': 'localhost', 'project_data_file': None, 'ruleset': 'Ruleset for URL check source test', 'source_rule_name': 'Check if endpoint is responding', 'source_ruleset_name': 'Ruleset for URL check source test', 'variables': {'event': {'url_check': {'status': 'up', 'status_code': 200, 'url': 'https://www.wikipedia.org'}}, 'fact': {'url_check': {'status': 'up', 'status_code': 200, 'url': 'https://www.wikipedia .org'}}}} ===================================================================================================================================================================================================================== |
You can see that this outputs all variables used in the event, so I can see what actually happened. This is also nice to see what events are supported.
The Event Driven Ansible Server
What Ansible AWX is for Ansible, the Event Driven Ansible Server is for the Ansible-rulebook.
The eda-server provides a web-UI where you can view your rulebooks, watch its runs and even execute one-off jobs.
The eda-server is in early development but already has the core functionality implemented.
I installed the eda-server with the provided docker-compose-file. After creating an admin-user and logging in to the webpage, I could add my project (basically a git-repository with your rulebooks and playbooks) and execute a job.
But that’s all what worked for me for now. Adding an inventory file via the web or deleting it isn’t implemented and the automatic rulebook activation did not work for me (probably because of some issue with docker).
There’s much to do and implement but looking at the speed of development I think we’ll have a usable product soon!
In the meantime I’ll continue checking out Event Driven Ansible.
Sources
- Github-repository for Event Driven Ansible
- Github-repository for ansible-rulebook
- Documentation
- EDA-Server
Be part of the Telekom MMS and check out our job offers as:
> (Senior) DevOps Engineer (m/w/d) (Job advertisement in German language)
Linux und Open Source Enthusiast. Aus dem traditionellen Betrieb kommend, schlage ich jetzt Brücken zwischen Betrieb und Entwicklung und tauche nebenbei in die Cloud-Native Landschaft ein.