Using the Shell Module in Ansible

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_whenand 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 shto execute commands (it is possible to define other shells via the option executable). With the module command, commands are not executed via the shell.
  • The module commanddoes not support environment variables, pipes and operators such as <, >, &, ;...

Therefore, it is more secure to use the module commandsince 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.

RHCE Ansible EX294 Exam Preparation Course
Learn all the essentials of Ansible with this hands-on tutorial series. It is ideal for RHCE Ansible EX294 exam preparation.
✍️
Author: Talha Khalid is a freelance web developer and technical writer.