In most cases, ansible inbuilt modules are enough to do the job. However, in some cases, it’s either highly complex or impossible to achieve the goal using ansible inbuilt modules. In such cases, many users write their scripts(Eg: bash or python
,Etc.), which is fine for most cases. However, there are some downsides to using scripts.
In this post, I will cover the following:
1. An example showing why using scripts may lead to uncertain behavior by the playbook.
2. Discuss a better alternative for scripts.
A sample script is called my_script.sh
#!/bin/bash
rm -rf /tmp/foo
echo "This is start of my script!"
ls -lrt /tmp/foo
echo "This is end of my script!"
A sample playbook calling the above sample script
Note that, in the below playbook, we call the above bash script using the script
module. The playbook consists of two plays, 1st play is executing against the local host, and the 2nd play runs against remote hosts over SSH.
---
- name: Sample play to target localhost
hosts: localhost
tasks:
- name: "Check for file presence"
ansible.builtin.script: my_script.sh
register: script_out
- name: "print script output"
debug:
msg: "{{ script_out }}"
- name: Other play to target remote hosts
hosts: webservers
tasks:
- name: "Check for file presence"
ansible.builtin.script: my_script.sh
register: script_out
- name: "print script output"
debug:
msg: "{{ script_out }}"
The result:
PLAY [Sample play to target localhost] *********************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************************
ok: [127.0.0.1]
TASK [Check for file presence] *****************************************************************************************************************************************************************************
changed: [127.0.0.1]
TASK [print script output] *********************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": {
"changed": true,
"failed": false,
"rc": 0,
"stderr": "ls: cannot access '/tmp/foo': No such file or directory\n",
"stderr_lines": [
"ls: cannot access '/tmp/foo': No such file or directory"
],
"stdout": "This is start of my script!\nThis is end of my script!\n",
"stdout_lines": [
"This is start of my script!",
"This is end of my script!"
]
}
}
PLAY [Other play to target remote hosts] *******************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************************
ok: [192.168.122.83]
TASK [Check for file presence] *****************************************************************************************************************************************************************************
changed: [192.168.122.83]
TASK [print script output] *********************************************************************************************************************************************************************************
ok: [192.168.122.83] => {
"msg": {
"changed": true,
"failed": false,
"rc": 0,
"stderr": "Shared connection to 192.168.122.83 closed.\r\n",
"stderr_lines": [
"Shared connection to 192.168.122.83 closed."
],
"stdout": "This is start of my script!\r\nls: cannot access '/tmp/foo': No such file or directory\r\nThis is end of my script!\r\n",
"stdout_lines": [
"This is start of my script!",
"ls: cannot access '/tmp/foo': No such file or directory",
"This is end of my script!"
]
}
}
PLAY RECAP *************************************************************************************************************************************************************************************************
127.0.0.1 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.122.83 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The Problem:
If you notice, the stderr generated by the ls
command is merged into the stdout. Worse, this only happens for the tasks executed over SSH.
cannot access '/tmp/foo': No such file or directory
"stdout": "This is start of my script!\r\nls: cannot access '/tmp/foo': No such file or directory\r\nThis is end of my script!\r\n",
"stdout_lines": [
"This is start of my script!",
"ls: cannot access '/tmp/foo': No such file or directory",
"This is end of my script!"
]
Reason: The following snippet from ansible documentation
NOTES:
* It is usually preferable to write Ansible modules rather than pushing scripts. Convert your script to an Ansible module for bonus points!
* The `ssh' connection plugin will force pseudo-tty allocation via `-tt' when scripts are executed. Pseudo-ttys do not have a stderr channel and all
stderr is sent to stdout. If you depend on separated stdout and stderr result keys, please switch to a copy+command set of tasks instead of using
script.
* If the path to the local script contains spaces, it needs to be quoted.
* This module is also supported for Windows targets.
Better approach?
Consider converting your scripts into custom modules. They are more ansible native and plugged into the ansible ecosystem. You can find a simple module example on this page.