Ansible

Using Ansible Shell Module to Execute Remote Commands

ansible run shell script

Spacelift and Ansible

Manage and orchestrate Ansible with Spacelift’s vibrant ecosystem and excellent GitOps flow. Create powerful custom workflows combining multiple IaC tools.

Book a demo

This blog post will dive deep into using the Ansible shell module and explore different ways of executing remote commands on nodes with Ansible as part of our automation efforts. We will review different options and modules for running remote commands and discuss their differences and when to use each.

If you are new to Ansible or interested in other Ansible concepts, these Ansible tutorials on Spacelift’s blog might be handy. 

We will cover:

  1. What is the Ansible shell module?
  2. Using Ansible shell module examples
  3. Other options to run remote commands with Ansible
  4. When to use the shell module vs. command module

What is the Ansible shell module?

The Ansible shell module is used to execute shell commands in the remote target machines. The shell module takes the command name followed by a list of space-delimited arguments. 

The shell module does not execute directly on the target but in a shell environment (/bin/sh) on the target. This makes it possible to use shell-specific features and functions such as pipe |, redirection <, >, >>, and so forth.

If you are targeting Windows nodes, use the ansible.windows.win_shell module.

Using Ansibe shell module examples

Let’s take a look at some examples of using the shell module in action.

Example 1: Ansible shell module to execute a single command

- name: Execute shell command
  ansible.builtin.shell: tail -n 10 /var/log/syslog > tail_syslog.txt

In this example, we are using the shell module to get the last ten lines of the /var/log/syslog file and pipe the output to a file tail_syslog.txt. The shell module is indeed here, as the > operator is a feature of the shell. 

Example 2: Run a command using the shell module if a file doesn’t exist

- name: This command will only run when file_to_check.txt doesn't exist
  ansible.builtin.shell: tail -n 10 /var/log/syslog > tail_syslog.txt
  args:
    creates: file_to_check.txt

The above example would run the command only if the file file_to_check.txt doesn’t exist. This is achieved with the creates parameter and is useful in cases where we need to check if commands that produce a specific artifact have already been executed.

Example 4: Show disk usage

- name: Check disk usage and grep for /dev/sda1
  ansible.builtin.shell: df -h | grep /dev/sda1
  register: disk_usage

- name: Display output
  ansible.builtin.debug:
    var: disk_usage.stdout_lines

In the above example, we use the df -h command to show disk usage and pipe it to filter only results for /dev/sda1. This is an example of a command that can’t be run correctly with the command module.

The command output is registered in the disk_usage variable and displayed in the next task.

ansible shell output

Example 5: Execute command in a specific directory

- name: Compile software in a specific directory
  ansible.builtin.shell: make install
  args:
    chdir: /path/to/source/code

That’s another example of using the args keyword with chdir to execute a shell command in a specific directory.

The make install command will be executed in the path/to/source/code directory in this case.

Example 6: Execute multiple commands

- name: Update system packages and clean up
  ansible.builtin.shell: |
    apt-get update &&
    apt-get upgrade -y &&
    apt-get clean

Another example would be to chain multiple commands together with the && operator, as shown above. In this case, the shell module executes the commands one by one from top to bottom.

Example 7: Check if a process is running

- name: Check if a process is running
  ansible.builtin.shell: ps aux | grep 'nginx' | grep -v grep
  register: process_status
  failed_when: process_status.rc != 0

- name: Display process status
  ansible.builtin.debug:
    var: process_status.stdout_lines

In this example, we use ps aux to list all the processes and then the pipe operator | to find the nginx process we’re interested in.

We also use a second pipe and grep to exclude our own grep command from the results.

Example 8: Select the shell used to execute the command

- name: Change shell to bash
  ansible.builtin.shell: cat /var/log/*log > logs_snaphot.txt
  args:
    executable: /bin/bash

In this example, we are using the executable parameter to specify the shell used to execute the command. This can be useful in cases where the default /bin/sh doesn’t support a feature we want to leverage.

Example 9: Use Templated Variable in the command

- name: Run the command using a templated variable to avoid injection
  ansible.builtin.shell: cat {{ logs_snapshot_file|quote }}
  register: logs

In the above example, we use a templated variable in a command. 

When using Ansible variables in commands, make sure to use {{ var | quote }} instead of {{ var }} to add quotes around the variable value, which helps to avoid injection and ensure the contents of the variable are treated as a single argument and not interpreted by the shell.

Alternative options to run remote commands with Ansible

We generally prefer using specialized Ansible modules over raw shell or command scripts. Task-specific Ansible modules are designed to be idempotent and to abstract away the underlying complexities of tasks, which makes them preferable to directly executing commands via the shell or command modules, for example.

Even more, specialized modules handle errors gracefully.

If something goes wrong, they often provide helpful error messages to aid in troubleshooting and are safer to use than arbitrary shell commands, which can inadvertently open up security risks. Ansible modules can report whether they made a change on the remote system, which helps to understand better changes performed to the targeted systems.

On some occasions, you might not be able to leverage any task-specific module to achieve your desired outcome. We can use Ansible to execute commands directly on remote hosts in these cases. Ansible provides several ways to execute commands on remote nodes.

We’ve already seen the shell module, and next, we will look at the command, expect, script, and raw modules for this purpose.

Ansible command module

The command module in Ansible executes commands on all selected hosts. It’s one of the most straightforward modules; it takes the command name followed by a list of space-delimited arguments. If you are targeting Windows nodes, use the ansible.windows.win_command instead.

It’s important to note that commands aren’t processed through the local shell with the command module. This means that variables like $HOSTNAME won’t work, and shell-specific functions like piping commands and redirection operators (<, >, >>, |, etc.) won’t be interpreted correctly. 

This behavior makes the command module safer and more predictable than the shell module that we will discuss later. When using the command module, you can be sure the command will execute exactly as you’ve written it, without any unexpected side effects from shell processing.

Here’s a basic example showcasing how to leverage the command module in a task:

 - name: Display list of files in /var/log
   ansible.builtin.command: ls /var/log
   register: log_files
 - name: Output list of log files
   ansible.builtin.debug:
     var: log_files.stdout_lines

In the above example, ls /var/log is the command being executed. The command output is registered in the log_files variable, which is then displayed using the debug module in the next task.

Here’s an example output of the above two tasks:

ansible shell register

Here’s another example of using the command module to check whether a package is installed in a target system:

- name: Check if NGINX is installed
  ansible.builtin.command: which nginx
  register: nginx_installed
  changed_when: false
  failed_when: false

- name: Display a message if NGINX is not installed
  ansible.builtin.debug:
    msg: "NGINX is not installed on this system."
  when: nginx_installed.rc != 0

Here, we check whether NGINX is installed by running the which nginx command and checking the return code(rc).

The changed_when: false line ensures that Ansible doesn’t report a change every time the Ansible playbook is executed, and failed_when: false makes the command succeed and not block the playbook from continuing even if the package isn’t found. 

Here’s an example output of the above two tasks:

ansible shell args

Finally, here’s an example with with_items to run multiple commands with one task. This approach could be helpful when running a sequence of commands as part of your playbook.

- name: Run multiple commands
  ansible.builtin.command: "{{ item }}"
  with_items:
    - ls /var/log
    - touch /tmp/tmp.txt
    - ps aux

Ansible expect module

The expect module in Ansible executes commands and responds to prompts. It’s often used to automate interactions with applications that require responses to prompts.

When using the expect module, you must specify the command that will be run and the responses. The responses are a mapping of expected string/regex and string to respond with.

Note that commands aren’t processed through the shell. 

Here’s an example usage of the expect module that responds to user input during the installation process of a package.

- name: Install software
  ansible.builtin.expect:
    command: /path/to/softare/install.sh
    responses:
      Continue\?: "yes"
      Please enter the installation directory: "/path/to/installation/directory"
      Enable automatic updates\?: "no"

Consider the security implications of automating prompt responses and handling sensitive data when using the expect module.

Since the expect module has been designed for simple use cases, consider using the shell or scripts module for more complex and advanced use cases.

Ansible script module

The script module executes a local script on remote nodes after transferring it. This module takes the script name followed by a list of space-delimited arguments. The given script will be processed through the shell environment on the remote node.

Note that if the path to the local script contains spaces, it needs to be quoted.

Here’s a basic usage of the script module:

- name: Run a script on a remote node
  ansible.builtin.script: /path/to/local/script.sh --flag some_value

Here’s a more advanced example using the args keyword to control the script environment.

- name: Run a script with custom environment variables
  ansible.builtin.script: /path/to/local/script.sh
  args:
    executable: /bin/bash
    chdir: /tmp/
    creates: /tmp/example.txt

In the above example, we run a script while setting the shell to use, the directory to change into before running the script, and checking if a file exists before running the script. 

As discussed previously, using or writing Ansible modules is usually preferable rather than running long scripts. Consider converting your script to an Ansible module to make your playbooks more readable and understandable.

Ansible raw module

The raw module executes raw commands on remote hosts, much like how you might execute commands over SSH. It executes low-down and dirty SSH commands, not going through the module subsystem.

This module does not require Python on the remote system, making it useful in scenarios where Python is not installed or when you’re dealing with devices that don’t support Python.

This module is also supported for Windows targets.

Here’s an example of using the raw module to install Python with yum:

- name: Install Python
  ansible.builtin.raw: yum install -y python3

It’s worth noting that the raw module is less safe, idempotent, and predictable than most other Ansible modules, and it doesn’t support advanced Ansible features such as variable substitutions, loops, conditionals, etc.

Its usage should be avoided unless there is no other way. It may be better to use the shell module to execute a command securely and predictably.

When to use the Ansible shell module vs command module

Previously, we discussed in detail the usage of shell and command modules and went through various examples. Next, let’s define some cases where we should prefer one over the other. 

Unlike the command module, shell module does not execute directly on the target but in a shell environment on the target. This makes it possible to use shell-specific features and functions such as pipe |, redirection <, >, >>, and others. If your commands contain any such shell-specific features, such as piping commands, variable substitution, redirecting output to files, you have to use the shell module.

Another use case for the shell module would be if your command involves a script that must be executed in the shell context. Since the shell module allows the use of shell functionalities, it might also make your playbooks less portable.

On the other hand, the command module is preferred in most cases as the most straightforward way to run a command on a remote host. Using the command module to execute a command is considered more secure and produces more predictable results.

Best practices when writing playbooks will follow the trend of using the command module unless using shell features is explicitly required.

Check out more Ansible best practices.

Key points

In this blog post, we explored how to leverage the Ansible shell module and other different options for executing remote commands with Ansible. We have gone through detailed examples and explained the intricacies and particularities of each different option.

Lastly, we saw examples of each module in action while discussing the different use cases that will make you select one over the other.

You can also explore how Spacelift’s vibrant ecosystem and excellent GitOps flow can greatly assist you in managing and orchestrating Ansible. By introducing Spacelift on top of Ansible, you can then easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization. Get started on your journey by creating a free trial account.

Thank you for reading, and I hope you enjoyed this as much as I did!

Continuous Integration and Deployment for your IaC

Spacelift allows you to automate, audit, secure, and continuously deliver your infrastructure. It helps overcome common state management issues and adds several must-have features for infrastructure management.

Start free trial