Introduction
ansible.builtin.shell executes commands through the shell (/bin/sh) on remote hosts. Unlike the command module, shell supports pipes, redirects, environment variables, and globbing. Use it when you need shell features — but prefer dedicated modules when possible.
Basic Usage
- name: Check disk usage
ansible.builtin.shell: df -h | grep /dev/sda1
register: disk_info
- name: Show result
debug:
msg: "{{ disk_info.stdout }}"
Parameters
| Parameter | Default | Description |
|---|---|---|
cmd | — | Command string to execute |
chdir | — | Change to directory before running |
creates | — | Skip if this file exists |
removes | — | Skip if this file doesn't exist |
executable | /bin/sh | Shell to use |
stdin | — | Data to pipe to stdin |
stdin_add_newline | true | Add newline to stdin |
warn | true | Show warnings for common mistakes |
Shell Features
Pipes
- name: Count running processes
ansible.builtin.shell: ps aux | wc -l
register: proc_count
- name: Find large files
ansible.builtin.shell: find /var/log -size +100M -exec ls -lh {} \;
- name: Get top CPU processes
ansible.builtin.shell: ps aux --sort=-%cpu | head -5
Redirects
- name: Save output to file
ansible.builtin.shell: dmesg > /tmp/dmesg.log 2>&1
- name: Append to log
ansible.builtin.shell: echo "Deployed at $(date)" >> /var/log/deploy.log
Environment Variables
- name: Run with env vars
ansible.builtin.shell: echo $MY_VAR
environment:
MY_VAR: hello
PATH: "/opt/myapp/bin:{{ ansible_env.PATH }}"
# Export and use
- name: Source profile and run
ansible.builtin.shell: source /etc/profile.d/myapp.sh && myapp --status
args:
executable: /bin/bash
Globbing
- name: Remove temp files
ansible.builtin.shell: rm -f /tmp/myapp-*.log
- name: Archive logs
ansible.builtin.shell: tar czf /backup/logs-$(date +%Y%m%d).tar.gz /var/log/myapp/*.log
Idempotent Patterns
creates / removes
# Only run if file doesn't exist
- name: Build application
ansible.builtin.shell: make && make install
args:
chdir: /opt/src/myapp
creates: /usr/local/bin/myapp
# Only run if file exists
- name: Clean build artifacts
ansible.builtin.shell: make clean
args:
chdir: /opt/src/myapp
removes: /opt/src/myapp/build
changed_when / failed_when
# Control changed status
- name: Check if service running
ansible.builtin.shell: systemctl is-active myapp
register: service_check
changed_when: false
failed_when: false
- name: Restart if not running
ansible.builtin.service:
name: myapp
state: started
when: service_check.rc != 0
# Custom changed detection
- name: Apply database migrations
ansible.builtin.shell: python manage.py migrate --check
args:
chdir: /opt/myapp
register: migration
changed_when: "'No migrations to apply' not in migration.stdout"
shell vs command
| Feature | shell | command |
|---|---|---|
Pipes (|) | ✅ | ❌ |
Redirects (>, >>) | ✅ | ❌ |
Environment variables ($VAR) | ✅ | ❌ |
Globbing (*.log) | ✅ | ❌ |
Semicolons (;) | ✅ | ❌ |
| Shell injection risk | ⚠️ Higher | ✅ Lower |
| Speed | Slightly slower | Slightly faster |
Rule: Use command when you don't need shell features. Use shell only when you need pipes, redirects, or glob.
# command is safer for simple commands
- ansible.builtin.command: uptime
# shell needed for pipes
- ansible.builtin.shell: cat /etc/passwd | grep deploy
Multiline Commands
# Using YAML literal block
- name: Complex script
ansible.builtin.shell: |
cd /opt/myapp
git pull origin main
pip install -r requirements.txt
python manage.py migrate
systemctl restart myapp
args:
executable: /bin/bash
# Using heredoc
- name: Database operations
ansible.builtin.shell: |
psql -U postgres <<EOF
CREATE DATABASE IF NOT EXISTS myapp;
GRANT ALL PRIVILEGES ON DATABASE myapp TO deploy;
EOF
Register and Parse Output
- name: Get memory info
ansible.builtin.shell: free -m | awk '/Mem:/ {print $3}'
register: memory_used
changed_when: false
- name: Alert on high memory
debug:
msg: "WARNING: {{ memory_used.stdout }}MB used"
when: memory_used.stdout | int > 1024
# Parse JSON output
- name: Get container info
ansible.builtin.shell: docker inspect myapp
register: container_info
changed_when: false
- set_fact:
container_ip: "{{ (container_info.stdout | from_json)[0].NetworkSettings.IPAddress }}"
Practical Examples
Application Deployment
- name: Deploy application
ansible.builtin.shell: |
set -e
cd /opt/myapp
git fetch origin
git checkout {{ app_version }}
pip install -r requirements.txt
python manage.py collectstatic --noinput
python manage.py migrate --noinput
args:
executable: /bin/bash
register: deploy
notify: Restart app
Log Rotation
- name: Rotate and compress logs
ansible.builtin.shell: |
cd /var/log/myapp
for f in *.log; do
if [ -f "$f" ] && [ $(stat -f%z "$f" 2>/dev/null || stat -c%s "$f") -gt 104857600 ]; then
mv "$f" "${f}.$(date +%Y%m%d)"
gzip "${f}.$(date +%Y%m%d)"
fi
done
args:
creates: /var/log/myapp/.rotated
Health Check Script
- name: Run health checks
ansible.builtin.shell: |
errors=0
# Check web server
curl -sf http://localhost:{{ app_port }}/health || errors=$((errors+1))
# Check database
pg_isready -h localhost -p 5432 || errors=$((errors+1))
# Check disk space
[ $(df --output=pcent / | tail -1 | tr -d ' %') -lt 90 ] || errors=$((errors+1))
echo "$errors errors found"
exit $errors
register: health
failed_when: health.rc > 0
changed_when: false
Security Considerations
# DANGEROUS — shell injection risk
- shell: "echo {{ user_input }}" # If user_input contains "; rm -rf /"
# SAFER — use quote filter
- shell: "echo {{ user_input | quote }}"
# SAFEST — use command module or dedicated modules
- command: "echo {{ user_input }}"
- user: { name: "{{ username }}", state: present }
Troubleshooting
Command Not Found
# Specify full path
- shell: /usr/local/bin/myapp --status
# Or set PATH
- shell: myapp --status
environment:
PATH: "/usr/local/bin:{{ ansible_env.PATH }}"
# Use bash for source/bashrc
- shell: source ~/.bashrc && nvm use 18 && node app.js
args:
executable: /bin/bash
Non-Zero Exit Code
# Ignore errors
- shell: some_command
ignore_errors: true
# Custom failure detection
- shell: grep "ERROR" /var/log/app.log
register: errors
failed_when: false
changed_when: false
Related Articles
Conclusion
Use ansible.builtin.shell when you need shell features — pipes, redirects, globbing, or environment variables. For simple commands, prefer command (safer, no shell injection risk). Always use changed_when and failed_when for idempotency. Use creates/removes to skip unnecessary runs. Quote user input with | quote to prevent injection. Prefer dedicated Ansible modules over shell commands whenever possible — they're idempotent by design and produce better output.