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:
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.
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.
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.
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:
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:
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.
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.
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 easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization.
With Spacelift, you get:
- Better playbook automation – Manage the execution of Ansible playbooks from one central location.
- Inventory observability – View all Ansible-managed hosts and related playbooks, with clear visual indicators showing the success or failure of recent runs.
- Playbook run insights – Audit Ansible playbook run results with detailed insights to pinpoint problems and simplify troubleshooting.
- Policies – Control what kind of resources engineers can create, what parameters they can have, how many approvals you need for a run, what kind of task you execute, what happens when a pull request is open, and where to send your notifications
- Stack dependencies – Build multi-infrastructure automation workflows with dependencies, having the ability to build a workflow that, for example, generates your EC2 instances using Terraform and combines it with Ansible to configure them
- Self-service infrastructure via Blueprints, or Spacelift’s Kubernetes operator – Enable your developers to do what matters – developing application code while not sacrificing control
- Creature comforts such as contexts (reusable containers for your environment variables, files, and hooks), and the ability to run arbitrary code
- Drift detection and optional remediation
If you want to learn more about using Spacelift with Ansible, check our documentation, read our Ansible guide, or book a demo with one of our engineers.
Would you like to see this in action – or just want a tl;dr? Check out this video I put together showing you Spacelift’s new Ansible functionality:
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.
Thank you for reading, and I hope you enjoyed this as much as I did!
Manage Ansible Better with Spacelift
Managing large-scale playbook execution is hard. Spacelift enables you to automate Ansible playbook execution with visibility and control over resources, and seamlessly link provisioning and configuration workflows.