Ansible shell Module: Run Commands with Pipes & Redirects (Complete Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
How to run shell commands in Ansible with the shell module (ansible.builtin.shell). Use pipes, redirects, environment variables, chained commands.
Ansible Shell Module: Run Commands with Pipes & Redirects (Complete Guide)
The Ansible shell module (ansible.builtin.shell) executes commands through the shell (/bin/sh) on remote hosts, enabling pipes, redirects, environment variables, and all shell features. This guide covers when and how to use it effectively.
See also: Ansible shell Module: Run Shell Commands with Pipes & Redirects (Guide)
Shell vs Command Module: Key Difference
| Feature | shell | command |
|---------|-------|---------|
| Pipes (|) | ✅ Yes | ❌ No |
| Redirects (>, >>) | ✅ Yes | ❌ No |
| Environment variables ($HOME) | ✅ Yes | ❌ No |
| Wildcards (.log) | ✅ Yes | ❌ No |
| Chaining (&&, ||) | ✅ Yes | ❌ No |
| Safer (no injection risk) | ❌ No | ✅ Yes |
Rule of thumb: Use command by default. Use shell only when you need shell features.
Basic Usage
---
- name: Shell module examples
hosts: all
tasks:
- name: Run a simple command
ansible.builtin.shell: echo "Hello from $(hostname)"
register: result
- name: Display output
ansible.builtin.debug:
var: result.stdout
See also: Ansible win_shell Module: Run PowerShell Commands on Windows (Guide)
Pipes and Redirects
- name: Count running processes
ansible.builtin.shell: ps aux | wc -l
register: process_count
changed_when: false
- name: Find large files
ansible.builtin.shell: find /var/log -type f -size +100M | sort -rn
register: large_files
changed_when: false
- name: Write output to file
ansible.builtin.shell: df -h > /tmp/disk_report.txt
- name: Append to log file
ansible.builtin.shell: echo "Backup completed at $(date)" >> /var/log/backup.log
Multi-Line Commands
Use YAML literal block scalar (|) for complex commands:
- name: Complex multi-line shell command
ansible.builtin.shell: |
cd /opt/app
git pull origin main
npm install --production
npm run build
systemctl restart myapp
args:
chdir: /opt/app
See also: Three options to Safely Limit Ansible Playbooks Execution to a Single Machine
Environment Variables
- name: Use environment variables
ansible.builtin.shell: |
export PYTHONPATH=/opt/lib/python
python3 /opt/scripts/deploy.py --env {{ env_name }}
environment:
DB_HOST: "{{ db_host }}"
API_KEY: "{{ vault_api_key }}"
Idempotency with creates and removes
Make shell commands idempotent:
- name: Build application (skip if already built)
ansible.builtin.shell: |
cd /opt/app && make && make install
args:
creates: /opt/app/bin/myapp
- name: Clean temporary files (only if they exist)
ansible.builtin.shell: rm -rf /tmp/build-*
args:
removes: /tmp/build-cache
Controlling changed_when
Prevent shell tasks from always showing "changed":
- name: Check disk usage (read-only command)
ansible.builtin.shell: df -h / | tail -1 | awk '{print $5}'
register: disk_usage
changed_when: false
- name: Conditionally mark as changed
ansible.builtin.shell: /opt/scripts/update-config.sh
register: config_update
changed_when: "'Updated' in config_update.stdout"
Choosing a Different Shell
- name: Run with bash
ansible.builtin.shell: |
shopt -s globstar
for f in /var/log/**/*.log; do
gzip "$f"
done
args:
executable: /bin/bash
- name: Run with zsh
ansible.builtin.shell: echo $ZSH_VERSION
args:
executable: /usr/bin/zsh
Error Handling
- name: Run command and handle failure
ansible.builtin.shell: /opt/scripts/risky-operation.sh
register: result
failed_when: result.rc not in [0, 1]
ignore_errors: false
- name: Check return code explicitly
ansible.builtin.shell: grep -q "READY" /var/run/app.status
register: app_status
failed_when: false
changed_when: false
- name: Act based on result
ansible.builtin.debug:
msg: "App is {{ 'ready' if app_status.rc == 0 else 'not ready' }}"
Passing stdin Data
- name: Pass input via stdin
ansible.builtin.shell: mysql -u root -p{{ db_password }} < /tmp/schema.sql
no_log: true
- name: Use heredoc
ansible.builtin.shell: |
cat << 'EOF' > /etc/motd
Welcome to {{ inventory_hostname }}
Managed by Ansible
EOF
Security Best Practices
1. Prefer command over shell when possible
# BAD — vulnerable to shell injection
- ansible.builtin.shell: "cat {{ user_file }}"
# GOOD — no shell injection risk
- ansible.builtin.command: "cat {{ user_file }}"
2. Use no_log for sensitive commands
- name: Set database password
ansible.builtin.shell: |
echo "ALTER USER admin PASSWORD '{{ vault_db_pass }}';" | psql
no_log: true
3. Quote variables in shell commands
- name: Safe variable usage
ansible.builtin.shell: 'grep -r "{{ search_term | quote }}" /var/log/'
Common Patterns
Check-then-act
- name: Check if service is running
ansible.builtin.shell: systemctl is-active myapp
register: service_check
changed_when: false
failed_when: false
- name: Start service if not running
ansible.builtin.systemd:
name: myapp
state: started
when: service_check.rc != 0
Pipeline with error checking
- name: Pipeline with pipefail
ansible.builtin.shell: |
set -o pipefail
journalctl -u myapp --since "1 hour ago" | grep -c ERROR
args:
executable: /bin/bash
register: error_count
changed_when: false
failed_when: error_count.rc > 1
FAQ
When should I use Ansible shell vs command module?
Use ansible.builtin.shell when you need shell features like pipes (|), redirects (>), environment variable expansion ($HOME), or wildcards (.log). Use ansible.builtin.command for everything else — it's safer because it doesn't process shell metacharacters.
How do I make Ansible shell commands idempotent?
Use the creates parameter to skip the command if a file already exists, or removes to only run if a file exists. You can also use changed_when with conditions based on the command output to accurately report changes.
Why does Ansible shell always show "changed"?
The shell module defaults to changed: true because it can't know if the command actually modified anything. Use changed_when: false for read-only commands, or changed_when: "'specific text' in result.stdout" for conditional change detection.
Can I use bash-specific features in Ansible shell?
Yes, set executable: /bin/bash in the args section to use bash features like shopt, [[ ]] tests, process substitution, and associative arrays that aren't available in /bin/sh.
How do I prevent shell injection in Ansible?
Use the command module instead of shell when possible. If you must use shell, apply the quote filter to user-supplied variables: "{{ user_input | quote }}". Never pass untrusted data directly into shell commands.
Conclusion
The Ansible shell module is powerful for running complex commands with pipes, redirects, and shell features on remote hosts. Always prefer the command module for simple operations, and use shell only when you genuinely need shell capabilities. Apply changed_when, creates, and removes for idempotent automation.
Related Articles
• Ansible command Module: Run Shell Commands on Remote Hosts • Ansible shell vs command vs raw: When to Use Each Module • Ansible script Module: Run Local Scripts on Remote HostsCategory: installation