Ansible Shell Module: Execute Shell Commands on Target Systems
Ansible's shell module acts as a bridge between your playbooks and the command-line interface (CLI) of your managed systems. It allows you to execute shell commands on remote targets making it easier to perform a number of different system administration, configuration management, and automation tasks.
In this article, I'll discuss:
- The working of shell module
- Examples of using the shell module
- Taking action on shell module's outcome
- Difference between the shell and the command module
- Why the shell module is discouraged
What is the Shell Module?
At its core, the shell module is an Ansible building block designed to run commands within a shell environment on remote hosts be it for simple tasks like checking file permissions or complex operations like running custom scripts.
The shell module uses Ansible's connection plugins to establish a secure connection with your target machines. Once connected, it spawns a shell session (typically /bin/sh
on Unix-like systems) and executes the command you provide within that shell. The module then captures the command's output (stdout and stderr) and return code, making this information available for further processing in your playbook.
Ansible shell module parameters
The shell module has a number of different parameters. Some of the most commonly used ones are:
- cmd (or free form): The core of the module, this is where you specify the command you want to run.
- creates: It helps ensure idempotence. If the specified file exists, the command won't be executed.
- chdir: Allows you to change the working directory before running the command.
- executable: If you need a specific shell interpreter (like
/bin/bash
), use this parameter. - removes: The inverse of
creates
. If the specified file doesn't exist, the command won't run.
These are just a few of the available parameters Ansible's documentation provides a comprehensive list.
Using shell module: A practical example
In this example, we will create a backup of a directory on a remote host and copy the contents of /opt/myapp
to it, and then verify the completion of the backup by listing the contents of the backup directory. If the backup is successful, a message confirming it will be displayed.
---
- name: Create directory backup using shell module
hosts: all
become: true
tasks:
- name: Create backup directory
ansible.builtin.shell: |
mkdir -p /var/backups/myapp
- name: Copy files to backup directory
ansible.builtin.shell: |
cp -r /opt/myapp /var/backups/myapp
- name: Verify backup completion
ansible.builtin.shell: |
ls /var/backups/myapp
register: backup_result
- name: Display backup status
debug:
msg: "Backup completed successfully: {{ backup_result.stdout_lines }}"
when: backup_result.rc == 0
Running Multiple Shell Commands
While the shell module is designed for a single command, you can chain multiple commands using a multiline string or a single string with shell operators or create a shell script, and execute it with the shell module.
---
- name: Run multiple commands
hosts: all
become: true
tasks:
- name: Execute multiple commands using shell module
ansible.builtin.shell: |
echo "Starting setup..."
mkdir -p /opt/myapp
touch /opt/myapp/config.yaml
echo "Setup complete."
Using Changed_when and Failed_when with Shell Module
Shell module does not return the status changed
. To actually define when a command fails or a script changes something on the system, you can use the changed_when
and statements failed_when
.
The instruction changed_when
allows you to define when a task actually makes a change on the target.
- name: Install dependencies via Composer.
ansible.builtin.shell: "/usr/local/bin/composer global require phpunit/phpunit --prefer-dist"
register: composer
changed_when: "'Nothing to install or update' not in composer.stdout"
If the php modules are already present on the target, the composer command does nothing and just returns the message.
There may be cases where certain errors are acceptable. It is in this type of case that we will use the instruction failed_when
.
- name:
ansible.builtin.shell: "ls | grep wp-config.php"
register: thecommand
failed_when: thecommand.rc not in [0, 1]
It can also be used when starting a playbook to prevent an installation tool from crashing due to lack of resources.
- name: Making sure the /tmp has more than 2gb
ansible.builtin.shell: "df -h /tmp|grep -v Filesystem|awk '{print $4}'|cut -d G -f1"
register: tmpspace
failed_when: "tmpspace.stdout|float < 2"
Shell vs command module
There is a command module in Ansible that serves the same purpose; executing commands on targets.
Even if the purpose of these two modules is identical, there are differences:
- The Ansible shell module executes commands directly through the shell of target hosts. By default, the default shell module uses
sh
to execute commands (it is possible to define other shells via the optionexecutable
). With the modulecommand
, commands are not executed via the shell. - The module
command
does not support environment variables, pipes and operators such as<
,>
,&
,;
...
Therefore, it is more secure to use the module command
since it cannot be affected by the user's shell variables. On the other hand, as the operators are unavailable, you must process the values in your playbook via Jinja filters.
Why shell module is discouraged
While the shell module is powerful and gives you the flexibility of scripting, however, it is generally discouraged to use it and using dedicated modules is considered a better practice. There are multiple reasons why the use of shell module is discouraged:
- Idempotency: Ensuring idempotency (running the same playbook multiple times without causing side effects) is challenging using shell scripts.
- Readability and Maintenance: Shell scripts can be harder to read, understand, and maintain compared to using dedicated Ansible modules and they don't return what changed or happened.
- Harder to Debug: It's easier and less time-consuming to debug a task than a long chain of piped shell commands.
However, there are scenarios where using the shell module is better than using a dedicated module that uses too many assumptions and ends up being more difficult to use than shell scripts with similar functions.
Similarly, when a dedicated module doesn't exist for your specific task, the flexibility of shell scripts can come in handy.
Final words
Shell module in Ansible is a powerful tool for executing shell commands on remote hosts, but it comes with maintenance risks. It's generally better to use specific Ansible modules designed for the tasks you need to perform, reserving the shell module for situations where no suitable module exists.
If you are new to Ansible and want to learn it from scratch, our Ansible tutorial series will be of great help. It's written for RHCE exam but it helps you the same whether you are preparing for the exam or not.